summaryrefslogtreecommitdiffstats
path: root/src/lib/dhcpsrv/tests
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/dhcpsrv/tests')
-rw-r--r--src/lib/dhcpsrv/tests/Makefile.am188
-rw-r--r--src/lib/dhcpsrv/tests/Makefile.in2564
-rw-r--r--src/lib/dhcpsrv/tests/alloc_engine4_unittest.cc4980
-rw-r--r--src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc5603
-rw-r--r--src/lib/dhcpsrv/tests/alloc_engine_expiration_unittest.cc2323
-rw-r--r--src/lib/dhcpsrv/tests/alloc_engine_hooks_unittest.cc660
-rw-r--r--src/lib/dhcpsrv/tests/alloc_engine_utils.cc655
-rw-r--r--src/lib/dhcpsrv/tests/alloc_engine_utils.h631
-rw-r--r--src/lib/dhcpsrv/tests/callout_handle_store_unittest.cc74
-rw-r--r--src/lib/dhcpsrv/tests/callout_library.cc25
-rw-r--r--src/lib/dhcpsrv/tests/callout_params_library.cc25
-rw-r--r--src/lib/dhcpsrv/tests/cb_ctl_dhcp_unittest.cc1884
-rw-r--r--src/lib/dhcpsrv/tests/cfg_db_access_unittest.cc338
-rw-r--r--src/lib/dhcpsrv/tests/cfg_duid_unittest.cc280
-rw-r--r--src/lib/dhcpsrv/tests/cfg_expiration_unittest.cc433
-rw-r--r--src/lib/dhcpsrv/tests/cfg_host_operations_unittest.cc113
-rw-r--r--src/lib/dhcpsrv/tests/cfg_hosts_unittest.cc1204
-rw-r--r--src/lib/dhcpsrv/tests/cfg_iface_unittest.cc1065
-rw-r--r--src/lib/dhcpsrv/tests/cfg_mac_source_unittest.cc84
-rw-r--r--src/lib/dhcpsrv/tests/cfg_multi_threading_unittest.cc117
-rw-r--r--src/lib/dhcpsrv/tests/cfg_option_def_unittest.cc400
-rw-r--r--src/lib/dhcpsrv/tests/cfg_option_unittest.cc1148
-rw-r--r--src/lib/dhcpsrv/tests/cfg_rsoo_unittest.cc100
-rw-r--r--src/lib/dhcpsrv/tests/cfg_shared_networks4_unittest.cc381
-rw-r--r--src/lib/dhcpsrv/tests/cfg_shared_networks6_unittest.cc391
-rw-r--r--src/lib/dhcpsrv/tests/cfg_subnets4_unittest.cc2087
-rw-r--r--src/lib/dhcpsrv/tests/cfg_subnets6_unittest.cc2008
-rw-r--r--src/lib/dhcpsrv/tests/cfgmgr_unittest.cc1066
-rw-r--r--src/lib/dhcpsrv/tests/client_class_def_parser_unittest.cc1454
-rw-r--r--src/lib/dhcpsrv/tests/client_class_def_unittest.cc806
-rw-r--r--src/lib/dhcpsrv/tests/csv_lease_file4_unittest.cc650
-rw-r--r--src/lib/dhcpsrv/tests/csv_lease_file6_unittest.cc724
-rw-r--r--src/lib/dhcpsrv/tests/d2_client_unittest.cc1240
-rw-r--r--src/lib/dhcpsrv/tests/d2_udp_unittest.cc504
-rw-r--r--src/lib/dhcpsrv/tests/dhcp4o6_ipc_unittest.cc599
-rw-r--r--src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc3176
-rw-r--r--src/lib/dhcpsrv/tests/dhcp_queue_control_parser_unittest.cc213
-rw-r--r--src/lib/dhcpsrv/tests/duid_config_parser_unittest.cc224
-rw-r--r--src/lib/dhcpsrv/tests/expiration_config_parser_unittest.cc252
-rw-r--r--src/lib/dhcpsrv/tests/free_lease_queue_unittest.cc613
-rw-r--r--src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.cc4145
-rw-r--r--src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.h694
-rw-r--r--src/lib/dhcpsrv/tests/host_cache_unittest.cc996
-rw-r--r--src/lib/dhcpsrv/tests/host_data_source_factory_unittest.cc202
-rw-r--r--src/lib/dhcpsrv/tests/host_mgr_unittest.cc166
-rw-r--r--src/lib/dhcpsrv/tests/host_reservation_parser_unittest.cc1468
-rw-r--r--src/lib/dhcpsrv/tests/host_reservations_list_parser_unittest.cc388
-rw-r--r--src/lib/dhcpsrv/tests/host_unittest.cc1310
-rw-r--r--src/lib/dhcpsrv/tests/ifaces_config_parser_unittest.cc392
-rw-r--r--src/lib/dhcpsrv/tests/ip_range_permutation_unittest.cc140
-rw-r--r--src/lib/dhcpsrv/tests/ip_range_unittest.cc63
-rw-r--r--src/lib/dhcpsrv/tests/lease_file_loader_unittest.cc881
-rw-r--r--src/lib/dhcpsrv/tests/lease_mgr_factory_unittest.cc31
-rw-r--r--src/lib/dhcpsrv/tests/lease_mgr_unittest.cc545
-rw-r--r--src/lib/dhcpsrv/tests/lease_unittest.cc1385
-rw-r--r--src/lib/dhcpsrv/tests/memfile_lease_limits_unittest.cc600
-rw-r--r--src/lib/dhcpsrv/tests/memfile_lease_mgr_unittest.cc2807
-rw-r--r--src/lib/dhcpsrv/tests/multi_threading_config_parser_unittest.cc192
-rw-r--r--src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc1706
-rw-r--r--src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc1161
-rw-r--r--src/lib/dhcpsrv/tests/ncr_generator_unittest.cc691
-rw-r--r--src/lib/dhcpsrv/tests/network_state_unittest.cc784
-rw-r--r--src/lib/dhcpsrv/tests/network_unittest.cc777
-rw-r--r--src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc1674
-rw-r--r--src/lib/dhcpsrv/tests/pgsql_lease_mgr_unittest.cc1160
-rw-r--r--src/lib/dhcpsrv/tests/pool_unittest.cc751
-rw-r--r--src/lib/dhcpsrv/tests/resource_handler_unittest.cc509
-rw-r--r--src/lib/dhcpsrv/tests/run_unittests.cc20
-rw-r--r--src/lib/dhcpsrv/tests/sanity_checks_unittest.cc378
-rw-r--r--src/lib/dhcpsrv/tests/shared_network_parser_unittest.cc1023
-rw-r--r--src/lib/dhcpsrv/tests/shared_network_unittest.cc1527
-rw-r--r--src/lib/dhcpsrv/tests/shared_networks_list_parser_unittest.cc116
-rw-r--r--src/lib/dhcpsrv/tests/srv_config_unittest.cc2241
-rw-r--r--src/lib/dhcpsrv/tests/subnet_unittest.cc1862
-rw-r--r--src/lib/dhcpsrv/tests/test_get_callout_handle.cc24
-rw-r--r--src/lib/dhcpsrv/tests/test_get_callout_handle.h38
-rw-r--r--src/lib/dhcpsrv/tests/test_libraries.h.in35
-rw-r--r--src/lib/dhcpsrv/tests/timer_mgr_unittest.cc484
78 files changed, 74648 insertions, 0 deletions
diff --git a/src/lib/dhcpsrv/tests/Makefile.am b/src/lib/dhcpsrv/tests/Makefile.am
new file mode 100644
index 0000000..804a497
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/Makefile.am
@@ -0,0 +1,188 @@
+SUBDIRS = .
+
+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += $(BOOST_INCLUDES)
+AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/lib/dhcpsrv/tests\"
+AM_CPPFLAGS += -DDHCP_DATA_DIR=\"$(abs_top_builddir)/src/lib/dhcpsrv/tests\"
+AM_CPPFLAGS += -DKEA_LFC_BUILD_DIR=\"$(abs_top_builddir)/src/bin/lfc\"
+AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
+
+AM_CXXFLAGS = $(KEA_CXXFLAGS)
+
+if USE_STATIC_LINK
+AM_LDFLAGS = -static
+endif
+
+CLEANFILES = *.gcno *.gcda
+DISTCLEANFILES = test_libraries.h
+
+TESTS_ENVIRONMENT = \
+ $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
+
+TESTS =
+if HAVE_GTEST
+# Build shared libraries for testing. The libtool way to create a shared library
+# is to specify "-avoid-version -export-dynamic -module" in the library LDFLAGS
+# (see http://www.gnu.org/software/libtool/manual/html_node/Link-mode.html).
+# Use of these switches will guarantee that the .so files are created in the
+# .libs folder and they can be dlopened.
+# Note that the shared libraries with callouts should not be used together with
+# the --enable-static-link option. With this option, the bind10 libraries are
+# statically linked with the program and if the callout invokes the methods
+# which belong to these libraries, the library with the callout will get its
+# own copy of the static objects (e.g. logger, ServerHooks) and that will lead
+# to unexpected errors. For this reason, the --enable-static-link option is
+# ignored for unit tests built here.
+
+noinst_LTLIBRARIES = libco1.la libco2.la libco3.la
+
+# -rpath /nowhere is a hack to trigger libtool to not create a
+# convenience archive, resulting in shared modules
+
+libco1_la_SOURCES = callout_library.cc
+libco1_la_CXXFLAGS = $(AM_CXXFLAGS)
+libco1_la_CPPFLAGS = $(AM_CPPFLAGS)
+libco1_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere
+
+libco2_la_SOURCES = callout_library.cc
+libco2_la_CXXFLAGS = $(AM_CXXFLAGS)
+libco2_la_CPPFLAGS = $(AM_CPPFLAGS)
+libco2_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere
+
+libco3_la_SOURCES = callout_params_library.cc
+libco3_la_CXXFLAGS = $(AM_CXXFLAGS)
+libco3_la_CPPFLAGS = $(AM_CPPFLAGS)
+libco3_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere
+
+TESTS += libdhcpsrv_unittests
+
+libdhcpsrv_unittests_SOURCES = run_unittests.cc
+libdhcpsrv_unittests_SOURCES += alloc_engine_utils.cc alloc_engine_utils.h
+libdhcpsrv_unittests_SOURCES += alloc_engine_expiration_unittest.cc
+libdhcpsrv_unittests_SOURCES += alloc_engine_hooks_unittest.cc
+libdhcpsrv_unittests_SOURCES += alloc_engine4_unittest.cc
+libdhcpsrv_unittests_SOURCES += alloc_engine6_unittest.cc
+libdhcpsrv_unittests_SOURCES += callout_handle_store_unittest.cc
+libdhcpsrv_unittests_SOURCES += cb_ctl_dhcp_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_db_access_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_duid_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_expiration_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_host_operations_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_hosts_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_iface_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_mac_source_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_multi_threading_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_option_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_option_def_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_rsoo_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_shared_networks4_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_shared_networks6_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_subnets4_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfg_subnets6_unittest.cc
+libdhcpsrv_unittests_SOURCES += cfgmgr_unittest.cc
+libdhcpsrv_unittests_SOURCES += client_class_def_unittest.cc
+libdhcpsrv_unittests_SOURCES += client_class_def_parser_unittest.cc
+libdhcpsrv_unittests_SOURCES += csv_lease_file4_unittest.cc
+libdhcpsrv_unittests_SOURCES += csv_lease_file6_unittest.cc
+libdhcpsrv_unittests_SOURCES += d2_client_unittest.cc
+libdhcpsrv_unittests_SOURCES += d2_udp_unittest.cc
+libdhcpsrv_unittests_SOURCES += dhcp_queue_control_parser_unittest.cc
+libdhcpsrv_unittests_SOURCES += dhcp4o6_ipc_unittest.cc
+libdhcpsrv_unittests_SOURCES += duid_config_parser_unittest.cc
+libdhcpsrv_unittests_SOURCES += expiration_config_parser_unittest.cc
+libdhcpsrv_unittests_SOURCES += free_lease_queue_unittest.cc
+libdhcpsrv_unittests_SOURCES += host_cache_unittest.cc
+libdhcpsrv_unittests_SOURCES += host_data_source_factory_unittest.cc
+libdhcpsrv_unittests_SOURCES += host_mgr_unittest.cc
+libdhcpsrv_unittests_SOURCES += host_unittest.cc
+libdhcpsrv_unittests_SOURCES += host_reservation_parser_unittest.cc
+libdhcpsrv_unittests_SOURCES += host_reservations_list_parser_unittest.cc
+libdhcpsrv_unittests_SOURCES += ifaces_config_parser_unittest.cc
+libdhcpsrv_unittests_SOURCES += ip_range_unittest.cc
+libdhcpsrv_unittests_SOURCES += ip_range_permutation_unittest.cc
+libdhcpsrv_unittests_SOURCES += lease_file_loader_unittest.cc
+libdhcpsrv_unittests_SOURCES += lease_unittest.cc
+libdhcpsrv_unittests_SOURCES += lease_mgr_factory_unittest.cc
+libdhcpsrv_unittests_SOURCES += lease_mgr_unittest.cc
+libdhcpsrv_unittests_SOURCES += generic_lease_mgr_unittest.cc generic_lease_mgr_unittest.h
+libdhcpsrv_unittests_SOURCES += memfile_lease_limits_unittest.cc
+libdhcpsrv_unittests_SOURCES += memfile_lease_mgr_unittest.cc
+libdhcpsrv_unittests_SOURCES += multi_threading_config_parser_unittest.cc
+libdhcpsrv_unittests_SOURCES += dhcp_parsers_unittest.cc
+libdhcpsrv_unittests_SOURCES += ncr_generator_unittest.cc
+if HAVE_MYSQL
+libdhcpsrv_unittests_SOURCES += mysql_lease_mgr_unittest.cc
+libdhcpsrv_unittests_SOURCES += mysql_host_data_source_unittest.cc
+endif
+if HAVE_PGSQL
+libdhcpsrv_unittests_SOURCES += pgsql_lease_mgr_unittest.cc
+libdhcpsrv_unittests_SOURCES += pgsql_host_data_source_unittest.cc
+endif
+libdhcpsrv_unittests_SOURCES += pool_unittest.cc
+libdhcpsrv_unittests_SOURCES += resource_handler_unittest.cc
+libdhcpsrv_unittests_SOURCES += sanity_checks_unittest.cc
+libdhcpsrv_unittests_SOURCES += shared_network_parser_unittest.cc
+libdhcpsrv_unittests_SOURCES += shared_network_unittest.cc
+libdhcpsrv_unittests_SOURCES += shared_networks_list_parser_unittest.cc
+libdhcpsrv_unittests_SOURCES += srv_config_unittest.cc
+libdhcpsrv_unittests_SOURCES += subnet_unittest.cc
+libdhcpsrv_unittests_SOURCES += test_get_callout_handle.cc test_get_callout_handle.h
+libdhcpsrv_unittests_SOURCES += timer_mgr_unittest.cc
+libdhcpsrv_unittests_SOURCES += network_state_unittest.cc
+libdhcpsrv_unittests_SOURCES += network_unittest.cc
+
+libdhcpsrv_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
+if HAVE_MYSQL
+libdhcpsrv_unittests_CPPFLAGS += $(MYSQL_CPPFLAGS)
+endif
+if HAVE_PGSQL
+libdhcpsrv_unittests_CPPFLAGS += $(PGSQL_CPPFLAGS)
+endif
+
+libdhcpsrv_unittests_CXXFLAGS = $(AM_CXXFLAGS)
+
+libdhcpsrv_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS)
+if HAVE_MYSQL
+libdhcpsrv_unittests_LDFLAGS += $(MYSQL_LIBS)
+endif
+if HAVE_PGSQL
+libdhcpsrv_unittests_LDFLAGS += $(PGSQL_LIBS)
+endif
+
+libdhcpsrv_unittests_LDADD = $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la
+
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/process/libkea-process.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/http/libkea-http.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/dhcp/tests/libdhcptest.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
+
+if HAVE_MYSQL
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/mysql/libkea-mysql.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/mysql/testutils/libmysqltest.la
+endif
+if HAVE_PGSQL
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/pgsql/libkea-pgsql.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/pgsql/testutils/libpgsqltest.la
+endif
+
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/database/testutils/libdatabasetest.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/database/libkea-database.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
+libdhcpsrv_unittests_LDADD += $(LOG4CPLUS_LIBS) $(CRYPTO_LIBS)
+libdhcpsrv_unittests_LDADD += $(BOOST_LIBS) $(GTEST_LDADD)
+endif
+
+noinst_PROGRAMS = $(TESTS)
diff --git a/src/lib/dhcpsrv/tests/Makefile.in b/src/lib/dhcpsrv/tests/Makefile.in
new file mode 100644
index 0000000..864e5ea
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/Makefile.in
@@ -0,0 +1,2564 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+TESTS = $(am__EXEEXT_1)
+@HAVE_GTEST_TRUE@am__append_1 = libdhcpsrv_unittests
+@HAVE_GTEST_TRUE@@HAVE_MYSQL_TRUE@am__append_2 = \
+@HAVE_GTEST_TRUE@@HAVE_MYSQL_TRUE@ mysql_lease_mgr_unittest.cc \
+@HAVE_GTEST_TRUE@@HAVE_MYSQL_TRUE@ mysql_host_data_source_unittest.cc
+@HAVE_GTEST_TRUE@@HAVE_PGSQL_TRUE@am__append_3 = \
+@HAVE_GTEST_TRUE@@HAVE_PGSQL_TRUE@ pgsql_lease_mgr_unittest.cc \
+@HAVE_GTEST_TRUE@@HAVE_PGSQL_TRUE@ pgsql_host_data_source_unittest.cc
+@HAVE_GTEST_TRUE@@HAVE_MYSQL_TRUE@am__append_4 = $(MYSQL_CPPFLAGS)
+@HAVE_GTEST_TRUE@@HAVE_PGSQL_TRUE@am__append_5 = $(PGSQL_CPPFLAGS)
+@HAVE_GTEST_TRUE@@HAVE_MYSQL_TRUE@am__append_6 = $(MYSQL_LIBS)
+@HAVE_GTEST_TRUE@@HAVE_PGSQL_TRUE@am__append_7 = $(PGSQL_LIBS)
+@HAVE_GTEST_TRUE@@HAVE_MYSQL_TRUE@am__append_8 = $(top_builddir)/src/lib/mysql/libkea-mysql.la \
+@HAVE_GTEST_TRUE@@HAVE_MYSQL_TRUE@ $(top_builddir)/src/lib/mysql/testutils/libmysqltest.la
+@HAVE_GTEST_TRUE@@HAVE_PGSQL_TRUE@am__append_9 = $(top_builddir)/src/lib/pgsql/libkea-pgsql.la \
+@HAVE_GTEST_TRUE@@HAVE_PGSQL_TRUE@ $(top_builddir)/src/lib/pgsql/testutils/libpgsqltest.la
+noinst_PROGRAMS = $(am__EXEEXT_2)
+subdir = src/lib/dhcpsrv/tests
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4macros/ax_boost_for_kea.m4 \
+ $(top_srcdir)/m4macros/ax_cpp11.m4 \
+ $(top_srcdir)/m4macros/ax_crypto.m4 \
+ $(top_srcdir)/m4macros/ax_find_library.m4 \
+ $(top_srcdir)/m4macros/ax_gssapi.m4 \
+ $(top_srcdir)/m4macros/ax_gtest.m4 \
+ $(top_srcdir)/m4macros/ax_isc_rpath.m4 \
+ $(top_srcdir)/m4macros/ax_sysrepo.m4 \
+ $(top_srcdir)/m4macros/libtool.m4 \
+ $(top_srcdir)/m4macros/ltoptions.m4 \
+ $(top_srcdir)/m4macros/ltsugar.m4 \
+ $(top_srcdir)/m4macros/ltversion.m4 \
+ $(top_srcdir)/m4macros/lt~obsolete.m4 \
+ $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES = test_libraries.h
+CONFIG_CLEAN_VPATH_FILES =
+@HAVE_GTEST_TRUE@am__EXEEXT_1 = libdhcpsrv_unittests$(EXEEXT)
+am__EXEEXT_2 = $(am__EXEEXT_1)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libco1_la_LIBADD =
+am__libco1_la_SOURCES_DIST = callout_library.cc
+@HAVE_GTEST_TRUE@am_libco1_la_OBJECTS = libco1_la-callout_library.lo
+libco1_la_OBJECTS = $(am_libco1_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+libco1_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CXXLD) $(libco1_la_CXXFLAGS) \
+ $(CXXFLAGS) $(libco1_la_LDFLAGS) $(LDFLAGS) -o $@
+@HAVE_GTEST_TRUE@am_libco1_la_rpath =
+libco2_la_LIBADD =
+am__libco2_la_SOURCES_DIST = callout_library.cc
+@HAVE_GTEST_TRUE@am_libco2_la_OBJECTS = libco2_la-callout_library.lo
+libco2_la_OBJECTS = $(am_libco2_la_OBJECTS)
+libco2_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CXXLD) $(libco2_la_CXXFLAGS) \
+ $(CXXFLAGS) $(libco2_la_LDFLAGS) $(LDFLAGS) -o $@
+@HAVE_GTEST_TRUE@am_libco2_la_rpath =
+libco3_la_LIBADD =
+am__libco3_la_SOURCES_DIST = callout_params_library.cc
+@HAVE_GTEST_TRUE@am_libco3_la_OBJECTS = \
+@HAVE_GTEST_TRUE@ libco3_la-callout_params_library.lo
+libco3_la_OBJECTS = $(am_libco3_la_OBJECTS)
+libco3_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CXXLD) $(libco3_la_CXXFLAGS) \
+ $(CXXFLAGS) $(libco3_la_LDFLAGS) $(LDFLAGS) -o $@
+@HAVE_GTEST_TRUE@am_libco3_la_rpath =
+am__libdhcpsrv_unittests_SOURCES_DIST = run_unittests.cc \
+ alloc_engine_utils.cc alloc_engine_utils.h \
+ alloc_engine_expiration_unittest.cc \
+ alloc_engine_hooks_unittest.cc alloc_engine4_unittest.cc \
+ alloc_engine6_unittest.cc callout_handle_store_unittest.cc \
+ cb_ctl_dhcp_unittest.cc cfg_db_access_unittest.cc \
+ cfg_duid_unittest.cc cfg_expiration_unittest.cc \
+ cfg_host_operations_unittest.cc cfg_hosts_unittest.cc \
+ cfg_iface_unittest.cc cfg_mac_source_unittest.cc \
+ cfg_multi_threading_unittest.cc cfg_option_unittest.cc \
+ cfg_option_def_unittest.cc cfg_rsoo_unittest.cc \
+ cfg_shared_networks4_unittest.cc \
+ cfg_shared_networks6_unittest.cc cfg_subnets4_unittest.cc \
+ cfg_subnets6_unittest.cc cfgmgr_unittest.cc \
+ client_class_def_unittest.cc \
+ client_class_def_parser_unittest.cc \
+ csv_lease_file4_unittest.cc csv_lease_file6_unittest.cc \
+ d2_client_unittest.cc d2_udp_unittest.cc \
+ dhcp_queue_control_parser_unittest.cc dhcp4o6_ipc_unittest.cc \
+ duid_config_parser_unittest.cc \
+ expiration_config_parser_unittest.cc \
+ free_lease_queue_unittest.cc host_cache_unittest.cc \
+ host_data_source_factory_unittest.cc host_mgr_unittest.cc \
+ host_unittest.cc host_reservation_parser_unittest.cc \
+ host_reservations_list_parser_unittest.cc \
+ ifaces_config_parser_unittest.cc ip_range_unittest.cc \
+ ip_range_permutation_unittest.cc lease_file_loader_unittest.cc \
+ lease_unittest.cc lease_mgr_factory_unittest.cc \
+ lease_mgr_unittest.cc generic_lease_mgr_unittest.cc \
+ generic_lease_mgr_unittest.h memfile_lease_limits_unittest.cc \
+ memfile_lease_mgr_unittest.cc \
+ multi_threading_config_parser_unittest.cc \
+ dhcp_parsers_unittest.cc ncr_generator_unittest.cc \
+ mysql_lease_mgr_unittest.cc mysql_host_data_source_unittest.cc \
+ pgsql_lease_mgr_unittest.cc pgsql_host_data_source_unittest.cc \
+ pool_unittest.cc resource_handler_unittest.cc \
+ sanity_checks_unittest.cc shared_network_parser_unittest.cc \
+ shared_network_unittest.cc \
+ shared_networks_list_parser_unittest.cc srv_config_unittest.cc \
+ subnet_unittest.cc test_get_callout_handle.cc \
+ test_get_callout_handle.h timer_mgr_unittest.cc \
+ network_state_unittest.cc network_unittest.cc
+@HAVE_GTEST_TRUE@@HAVE_MYSQL_TRUE@am__objects_1 = libdhcpsrv_unittests-mysql_lease_mgr_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@@HAVE_MYSQL_TRUE@ libdhcpsrv_unittests-mysql_host_data_source_unittest.$(OBJEXT)
+@HAVE_GTEST_TRUE@@HAVE_PGSQL_TRUE@am__objects_2 = libdhcpsrv_unittests-pgsql_lease_mgr_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@@HAVE_PGSQL_TRUE@ libdhcpsrv_unittests-pgsql_host_data_source_unittest.$(OBJEXT)
+@HAVE_GTEST_TRUE@am_libdhcpsrv_unittests_OBJECTS = \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-run_unittests.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-alloc_engine_utils.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-alloc_engine_expiration_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-alloc_engine_hooks_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-alloc_engine4_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-alloc_engine6_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-callout_handle_store_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cb_ctl_dhcp_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_db_access_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_duid_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_expiration_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_host_operations_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_hosts_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_iface_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_mac_source_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_multi_threading_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_option_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_option_def_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_rsoo_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_shared_networks4_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_shared_networks6_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_subnets4_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfg_subnets6_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-cfgmgr_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-client_class_def_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-client_class_def_parser_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-csv_lease_file4_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-csv_lease_file6_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-d2_client_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-d2_udp_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-dhcp4o6_ipc_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-duid_config_parser_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-expiration_config_parser_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-free_lease_queue_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-host_cache_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-host_data_source_factory_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-host_mgr_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-host_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-host_reservation_parser_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-host_reservations_list_parser_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-ifaces_config_parser_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-ip_range_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-ip_range_permutation_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-lease_file_loader_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-lease_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-lease_mgr_factory_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-lease_mgr_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-generic_lease_mgr_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-memfile_lease_limits_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-memfile_lease_mgr_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-multi_threading_config_parser_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-dhcp_parsers_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-ncr_generator_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ $(am__objects_1) $(am__objects_2) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-pool_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-resource_handler_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-sanity_checks_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-shared_network_parser_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-shared_network_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-shared_networks_list_parser_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-srv_config_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-subnet_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-test_get_callout_handle.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-timer_mgr_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-network_state_unittest.$(OBJEXT) \
+@HAVE_GTEST_TRUE@ libdhcpsrv_unittests-network_unittest.$(OBJEXT)
+libdhcpsrv_unittests_OBJECTS = $(am_libdhcpsrv_unittests_OBJECTS)
+am__DEPENDENCIES_1 =
+@HAVE_GTEST_TRUE@libdhcpsrv_unittests_DEPENDENCIES = $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/process/libkea-process.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/eval/libkea-eval.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/stats/libkea-stats.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/config/libkea-cfgclient.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/http/libkea-http.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcp/tests/libdhcptest.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/hooks/libkea-hooks.la \
+@HAVE_GTEST_TRUE@ $(am__append_8) $(am__append_9) \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/database/testutils/libdatabasetest.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/database/libkea-database.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/testutils/libkea-testutils.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/cc/libkea-cc.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/asiolink/libkea-asiolink.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dns/libkea-dns++.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/log/libkea-log.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/util/libkea-util.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/exceptions/libkea-exceptions.la \
+@HAVE_GTEST_TRUE@ $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) \
+@HAVE_GTEST_TRUE@ $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1)
+libdhcpsrv_unittests_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CXXLD) \
+ $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) \
+ $(libdhcpsrv_unittests_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/libco1_la-callout_library.Plo \
+ ./$(DEPDIR)/libco2_la-callout_library.Plo \
+ ./$(DEPDIR)/libco3_la-callout_params_library.Plo \
+ ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine4_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine6_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine_expiration_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine_hooks_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine_utils.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-callout_handle_store_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cb_ctl_dhcp_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_db_access_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_duid_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_expiration_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_host_operations_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_hosts_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_iface_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_mac_source_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_multi_threading_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_option_def_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_option_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_rsoo_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks4_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks6_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_subnets4_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfg_subnets6_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-cfgmgr_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-client_class_def_parser_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-client_class_def_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-csv_lease_file4_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-csv_lease_file6_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-d2_client_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-d2_udp_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-dhcp4o6_ipc_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-dhcp_parsers_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-duid_config_parser_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-expiration_config_parser_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-free_lease_queue_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-generic_lease_mgr_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-host_cache_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-host_data_source_factory_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-host_mgr_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-host_reservation_parser_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-host_reservations_list_parser_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-host_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-ifaces_config_parser_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-ip_range_permutation_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-ip_range_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-lease_file_loader_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-lease_mgr_factory_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-lease_mgr_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-lease_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-memfile_lease_limits_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-memfile_lease_mgr_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-multi_threading_config_parser_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-mysql_host_data_source_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-mysql_lease_mgr_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-ncr_generator_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-network_state_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-network_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-pgsql_host_data_source_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-pgsql_lease_mgr_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-pool_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-resource_handler_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-run_unittests.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-sanity_checks_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-shared_network_parser_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-shared_network_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-shared_networks_list_parser_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-srv_config_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-subnet_unittest.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-test_get_callout_handle.Po \
+ ./$(DEPDIR)/libdhcpsrv_unittests-timer_mgr_unittest.Po
+am__mv = mv -f
+CXXCOMPILE = $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) \
+ $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS)
+LTCXXCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CXXFLAGS) $(CXXFLAGS)
+AM_V_CXX = $(am__v_CXX_@AM_V@)
+am__v_CXX_ = $(am__v_CXX_@AM_DEFAULT_V@)
+am__v_CXX_0 = @echo " CXX " $@;
+am__v_CXX_1 =
+CXXLD = $(CXX)
+CXXLINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CXXLD) $(AM_CXXFLAGS) \
+ $(CXXFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CXXLD = $(am__v_CXXLD_@AM_V@)
+am__v_CXXLD_ = $(am__v_CXXLD_@AM_DEFAULT_V@)
+am__v_CXXLD_0 = @echo " CXXLD " $@;
+am__v_CXXLD_1 =
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libco1_la_SOURCES) $(libco2_la_SOURCES) \
+ $(libco3_la_SOURCES) $(libdhcpsrv_unittests_SOURCES)
+DIST_SOURCES = $(am__libco1_la_SOURCES_DIST) \
+ $(am__libco2_la_SOURCES_DIST) $(am__libco3_la_SOURCES_DIST) \
+ $(am__libdhcpsrv_unittests_SOURCES_DIST)
+RECURSIVE_TARGETS = all-recursive check-recursive cscopelist-recursive \
+ ctags-recursive dvi-recursive html-recursive info-recursive \
+ install-data-recursive install-dvi-recursive \
+ install-exec-recursive install-html-recursive \
+ install-info-recursive install-pdf-recursive \
+ install-ps-recursive install-recursive installcheck-recursive \
+ installdirs-recursive pdf-recursive ps-recursive \
+ tags-recursive uninstall-recursive
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \
+ distclean-recursive maintainer-clean-recursive
+am__recursive_targets = \
+ $(RECURSIVE_TARGETS) \
+ $(RECURSIVE_CLEAN_TARGETS) \
+ $(am__extra_recursive_targets)
+AM_RECURSIVE_TARGETS = $(am__recursive_targets:-recursive=) TAGS CTAGS \
+ distdir distdir-am
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__tty_colors_dummy = \
+ mgn= red= grn= lgn= blu= brg= std=; \
+ am__color_tests=no
+am__tty_colors = { \
+ $(am__tty_colors_dummy); \
+ if test "X$(AM_COLOR_TESTS)" = Xno; then \
+ am__color_tests=no; \
+ elif test "X$(AM_COLOR_TESTS)" = Xalways; then \
+ am__color_tests=yes; \
+ elif test "X$$TERM" != Xdumb && { test -t 1; } 2>/dev/null; then \
+ am__color_tests=yes; \
+ fi; \
+ if test $$am__color_tests = yes; then \
+ red=''; \
+ grn=''; \
+ lgn=''; \
+ blu=''; \
+ mgn=''; \
+ brg=''; \
+ std=''; \
+ fi; \
+}
+DIST_SUBDIRS = $(SUBDIRS)
+am__DIST_COMMON = $(srcdir)/Makefile.in $(srcdir)/test_libraries.h.in \
+ $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+am__relativize = \
+ dir0=`pwd`; \
+ sed_first='s,^\([^/]*\)/.*$$,\1,'; \
+ sed_rest='s,^[^/]*/*,,'; \
+ sed_last='s,^.*/\([^/]*\)$$,\1,'; \
+ sed_butlast='s,/*[^/]*$$,,'; \
+ while test -n "$$dir1"; do \
+ first=`echo "$$dir1" | sed -e "$$sed_first"`; \
+ if test "$$first" != "."; then \
+ if test "$$first" = ".."; then \
+ dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \
+ dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \
+ else \
+ first2=`echo "$$dir2" | sed -e "$$sed_first"`; \
+ if test "$$first2" = "$$first"; then \
+ dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \
+ else \
+ dir2="../$$dir2"; \
+ fi; \
+ dir0="$$dir0"/"$$first"; \
+ fi; \
+ fi; \
+ dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \
+ done; \
+ reldir="$$dir2"
+ACLOCAL = @ACLOCAL@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+AR = @AR@
+ASCIIDOC = @ASCIIDOC@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BOOST_INCLUDES = @BOOST_INCLUDES@
+BOOST_LIBS = @BOOST_LIBS@
+BOTAN_TOOL = @BOTAN_TOOL@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CFLAGS = @CFLAGS@
+CONTRIB_DIR = @CONTRIB_DIR@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPTO_CFLAGS = @CRYPTO_CFLAGS@
+CRYPTO_INCLUDES = @CRYPTO_INCLUDES@
+CRYPTO_LDFLAGS = @CRYPTO_LDFLAGS@
+CRYPTO_LIBS = @CRYPTO_LIBS@
+CRYPTO_PACKAGE = @CRYPTO_PACKAGE@
+CRYPTO_RPATH = @CRYPTO_RPATH@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DISTCHECK_BOOST_CONFIGURE_FLAG = @DISTCHECK_BOOST_CONFIGURE_FLAG@
+DISTCHECK_CONTRIB_CONFIGURE_FLAG = @DISTCHECK_CONTRIB_CONFIGURE_FLAG@
+DISTCHECK_CRYPTO_CONFIGURE_FLAG = @DISTCHECK_CRYPTO_CONFIGURE_FLAG@
+DISTCHECK_GTEST_CONFIGURE_FLAG = @DISTCHECK_GTEST_CONFIGURE_FLAG@
+DISTCHECK_KEA_SHELL_CONFIGURE_FLAG = @DISTCHECK_KEA_SHELL_CONFIGURE_FLAG@
+DISTCHECK_LOG4CPLUS_CONFIGURE_FLAG = @DISTCHECK_LOG4CPLUS_CONFIGURE_FLAG@
+DISTCHECK_PERFDHCP_CONFIGURE_FLAG = @DISTCHECK_PERFDHCP_CONFIGURE_FLAG@
+DISTCHECK_PREMIUM_CONFIGURE_FLAG = @DISTCHECK_PREMIUM_CONFIGURE_FLAG@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+GENHTML = @GENHTML@
+GREP = @GREP@
+GSSAPI_CFLAGS = @GSSAPI_CFLAGS@
+GSSAPI_LIBS = @GSSAPI_LIBS@
+GTEST_CONFIG = @GTEST_CONFIG@
+GTEST_INCLUDES = @GTEST_INCLUDES@
+GTEST_LDADD = @GTEST_LDADD@
+GTEST_LDFLAGS = @GTEST_LDFLAGS@
+GTEST_SOURCE = @GTEST_SOURCE@
+HAVE_SYSREPO = @HAVE_SYSREPO@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KEA_CXXFLAGS = @KEA_CXXFLAGS@
+KEA_SRCID = @KEA_SRCID@
+KRB5_CONFIG = @KRB5_CONFIG@
+LCOV = @LCOV@
+LD = @LD@
+LDFLAGS = @LDFLAGS@
+LEX = @LEX@
+LEXLIB = @LEXLIB@
+LEX_OUTPUT_ROOT = @LEX_OUTPUT_ROOT@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBTOOL = @LIBTOOL@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LOG4CPLUS_INCLUDES = @LOG4CPLUS_INCLUDES@
+LOG4CPLUS_LIBS = @LOG4CPLUS_LIBS@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MYSQL_CPPFLAGS = @MYSQL_CPPFLAGS@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PACKAGE_VERSION_TYPE = @PACKAGE_VERSION_TYPE@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PDFLATEX = @PDFLATEX@
+PERL = @PERL@
+PGSQL_CPPFLAGS = @PGSQL_CPPFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PKGPYTHONDIR = @PKGPYTHONDIR@
+PKG_CONFIG = @PKG_CONFIG@
+PLANTUML = @PLANTUML@
+PREMIUM_DIR = @PREMIUM_DIR@
+PYTHON = @PYTHON@
+PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@
+PYTHON_PLATFORM = @PYTHON_PLATFORM@
+PYTHON_PREFIX = @PYTHON_PREFIX@
+PYTHON_VERSION = @PYTHON_VERSION@
+RANLIB = @RANLIB@
+SED = @SED@
+SEP = @SEP@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SPHINXBUILD = @SPHINXBUILD@
+SRPD_PLUGINS_PATH = @SRPD_PLUGINS_PATH@
+SR_REPO_PATH = @SR_REPO_PATH@
+STRIP = @STRIP@
+SYSREPOCPP_VERSION = @SYSREPOCPP_VERSION@
+SYSREPO_CPPFLAGS = @SYSREPO_CPPFLAGS@
+SYSREPO_INCLUDEDIR = @SYSREPO_INCLUDEDIR@
+SYSREPO_LIBS = @SYSREPO_LIBS@
+SYSREPO_VERSION = @SYSREPO_VERSION@
+USE_LCOV = @USE_LCOV@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+WARNING_GCC_44_STRICT_ALIASING_CFLAG = @WARNING_GCC_44_STRICT_ALIASING_CFLAG@
+YACC = @YACC@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+pkgpyexecdir = @pkgpyexecdir@
+pkgpythondir = @pkgpythondir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+pyexecdir = @pyexecdir@
+pythondir = @pythondir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+srcdir = @srcdir@
+sysconfdir = @sysconfdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+SUBDIRS = .
+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib \
+ $(BOOST_INCLUDES) \
+ -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/lib/dhcpsrv/tests\" \
+ -DDHCP_DATA_DIR=\"$(abs_top_builddir)/src/lib/dhcpsrv/tests\" \
+ -DKEA_LFC_BUILD_DIR=\"$(abs_top_builddir)/src/bin/lfc\" \
+ -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
+AM_CXXFLAGS = $(KEA_CXXFLAGS)
+@USE_STATIC_LINK_TRUE@AM_LDFLAGS = -static
+CLEANFILES = *.gcno *.gcda
+DISTCLEANFILES = test_libraries.h
+TESTS_ENVIRONMENT = \
+ $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
+
+# Build shared libraries for testing. The libtool way to create a shared library
+# is to specify "-avoid-version -export-dynamic -module" in the library LDFLAGS
+# (see http://www.gnu.org/software/libtool/manual/html_node/Link-mode.html).
+# Use of these switches will guarantee that the .so files are created in the
+# .libs folder and they can be dlopened.
+# Note that the shared libraries with callouts should not be used together with
+# the --enable-static-link option. With this option, the bind10 libraries are
+# statically linked with the program and if the callout invokes the methods
+# which belong to these libraries, the library with the callout will get its
+# own copy of the static objects (e.g. logger, ServerHooks) and that will lead
+# to unexpected errors. For this reason, the --enable-static-link option is
+# ignored for unit tests built here.
+@HAVE_GTEST_TRUE@noinst_LTLIBRARIES = libco1.la libco2.la libco3.la
+
+# -rpath /nowhere is a hack to trigger libtool to not create a
+# convenience archive, resulting in shared modules
+@HAVE_GTEST_TRUE@libco1_la_SOURCES = callout_library.cc
+@HAVE_GTEST_TRUE@libco1_la_CXXFLAGS = $(AM_CXXFLAGS)
+@HAVE_GTEST_TRUE@libco1_la_CPPFLAGS = $(AM_CPPFLAGS)
+@HAVE_GTEST_TRUE@libco1_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere
+@HAVE_GTEST_TRUE@libco2_la_SOURCES = callout_library.cc
+@HAVE_GTEST_TRUE@libco2_la_CXXFLAGS = $(AM_CXXFLAGS)
+@HAVE_GTEST_TRUE@libco2_la_CPPFLAGS = $(AM_CPPFLAGS)
+@HAVE_GTEST_TRUE@libco2_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere
+@HAVE_GTEST_TRUE@libco3_la_SOURCES = callout_params_library.cc
+@HAVE_GTEST_TRUE@libco3_la_CXXFLAGS = $(AM_CXXFLAGS)
+@HAVE_GTEST_TRUE@libco3_la_CPPFLAGS = $(AM_CPPFLAGS)
+@HAVE_GTEST_TRUE@libco3_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere
+@HAVE_GTEST_TRUE@libdhcpsrv_unittests_SOURCES = run_unittests.cc \
+@HAVE_GTEST_TRUE@ alloc_engine_utils.cc alloc_engine_utils.h \
+@HAVE_GTEST_TRUE@ alloc_engine_expiration_unittest.cc \
+@HAVE_GTEST_TRUE@ alloc_engine_hooks_unittest.cc \
+@HAVE_GTEST_TRUE@ alloc_engine4_unittest.cc \
+@HAVE_GTEST_TRUE@ alloc_engine6_unittest.cc \
+@HAVE_GTEST_TRUE@ callout_handle_store_unittest.cc \
+@HAVE_GTEST_TRUE@ cb_ctl_dhcp_unittest.cc \
+@HAVE_GTEST_TRUE@ cfg_db_access_unittest.cc \
+@HAVE_GTEST_TRUE@ cfg_duid_unittest.cc \
+@HAVE_GTEST_TRUE@ cfg_expiration_unittest.cc \
+@HAVE_GTEST_TRUE@ cfg_host_operations_unittest.cc \
+@HAVE_GTEST_TRUE@ cfg_hosts_unittest.cc cfg_iface_unittest.cc \
+@HAVE_GTEST_TRUE@ cfg_mac_source_unittest.cc \
+@HAVE_GTEST_TRUE@ cfg_multi_threading_unittest.cc \
+@HAVE_GTEST_TRUE@ cfg_option_unittest.cc \
+@HAVE_GTEST_TRUE@ cfg_option_def_unittest.cc \
+@HAVE_GTEST_TRUE@ cfg_rsoo_unittest.cc \
+@HAVE_GTEST_TRUE@ cfg_shared_networks4_unittest.cc \
+@HAVE_GTEST_TRUE@ cfg_shared_networks6_unittest.cc \
+@HAVE_GTEST_TRUE@ cfg_subnets4_unittest.cc \
+@HAVE_GTEST_TRUE@ cfg_subnets6_unittest.cc cfgmgr_unittest.cc \
+@HAVE_GTEST_TRUE@ client_class_def_unittest.cc \
+@HAVE_GTEST_TRUE@ client_class_def_parser_unittest.cc \
+@HAVE_GTEST_TRUE@ csv_lease_file4_unittest.cc \
+@HAVE_GTEST_TRUE@ csv_lease_file6_unittest.cc \
+@HAVE_GTEST_TRUE@ d2_client_unittest.cc d2_udp_unittest.cc \
+@HAVE_GTEST_TRUE@ dhcp_queue_control_parser_unittest.cc \
+@HAVE_GTEST_TRUE@ dhcp4o6_ipc_unittest.cc \
+@HAVE_GTEST_TRUE@ duid_config_parser_unittest.cc \
+@HAVE_GTEST_TRUE@ expiration_config_parser_unittest.cc \
+@HAVE_GTEST_TRUE@ free_lease_queue_unittest.cc \
+@HAVE_GTEST_TRUE@ host_cache_unittest.cc \
+@HAVE_GTEST_TRUE@ host_data_source_factory_unittest.cc \
+@HAVE_GTEST_TRUE@ host_mgr_unittest.cc host_unittest.cc \
+@HAVE_GTEST_TRUE@ host_reservation_parser_unittest.cc \
+@HAVE_GTEST_TRUE@ host_reservations_list_parser_unittest.cc \
+@HAVE_GTEST_TRUE@ ifaces_config_parser_unittest.cc \
+@HAVE_GTEST_TRUE@ ip_range_unittest.cc \
+@HAVE_GTEST_TRUE@ ip_range_permutation_unittest.cc \
+@HAVE_GTEST_TRUE@ lease_file_loader_unittest.cc \
+@HAVE_GTEST_TRUE@ lease_unittest.cc \
+@HAVE_GTEST_TRUE@ lease_mgr_factory_unittest.cc \
+@HAVE_GTEST_TRUE@ lease_mgr_unittest.cc \
+@HAVE_GTEST_TRUE@ generic_lease_mgr_unittest.cc \
+@HAVE_GTEST_TRUE@ generic_lease_mgr_unittest.h \
+@HAVE_GTEST_TRUE@ memfile_lease_limits_unittest.cc \
+@HAVE_GTEST_TRUE@ memfile_lease_mgr_unittest.cc \
+@HAVE_GTEST_TRUE@ multi_threading_config_parser_unittest.cc \
+@HAVE_GTEST_TRUE@ dhcp_parsers_unittest.cc \
+@HAVE_GTEST_TRUE@ ncr_generator_unittest.cc $(am__append_2) \
+@HAVE_GTEST_TRUE@ $(am__append_3) pool_unittest.cc \
+@HAVE_GTEST_TRUE@ resource_handler_unittest.cc \
+@HAVE_GTEST_TRUE@ sanity_checks_unittest.cc \
+@HAVE_GTEST_TRUE@ shared_network_parser_unittest.cc \
+@HAVE_GTEST_TRUE@ shared_network_unittest.cc \
+@HAVE_GTEST_TRUE@ shared_networks_list_parser_unittest.cc \
+@HAVE_GTEST_TRUE@ srv_config_unittest.cc subnet_unittest.cc \
+@HAVE_GTEST_TRUE@ test_get_callout_handle.cc \
+@HAVE_GTEST_TRUE@ test_get_callout_handle.h \
+@HAVE_GTEST_TRUE@ timer_mgr_unittest.cc \
+@HAVE_GTEST_TRUE@ network_state_unittest.cc network_unittest.cc
+@HAVE_GTEST_TRUE@libdhcpsrv_unittests_CPPFLAGS = $(AM_CPPFLAGS) \
+@HAVE_GTEST_TRUE@ $(GTEST_INCLUDES) $(am__append_4) \
+@HAVE_GTEST_TRUE@ $(am__append_5)
+@HAVE_GTEST_TRUE@libdhcpsrv_unittests_CXXFLAGS = $(AM_CXXFLAGS)
+@HAVE_GTEST_TRUE@libdhcpsrv_unittests_LDFLAGS = $(AM_LDFLAGS) \
+@HAVE_GTEST_TRUE@ $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS) \
+@HAVE_GTEST_TRUE@ $(am__append_6) $(am__append_7)
+@HAVE_GTEST_TRUE@libdhcpsrv_unittests_LDADD = $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/process/libkea-process.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/eval/libkea-eval.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/stats/libkea-stats.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/config/libkea-cfgclient.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/http/libkea-http.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcp/tests/libdhcptest.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/hooks/libkea-hooks.la \
+@HAVE_GTEST_TRUE@ $(am__append_8) $(am__append_9) \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/database/testutils/libdatabasetest.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/database/libkea-database.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/testutils/libkea-testutils.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/cc/libkea-cc.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/asiolink/libkea-asiolink.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/dns/libkea-dns++.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/log/libkea-log.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/util/libkea-util.la \
+@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/exceptions/libkea-exceptions.la \
+@HAVE_GTEST_TRUE@ $(LOG4CPLUS_LIBS) $(CRYPTO_LIBS) \
+@HAVE_GTEST_TRUE@ $(BOOST_LIBS) $(GTEST_LDADD)
+all: all-recursive
+
+.SUFFIXES:
+.SUFFIXES: .cc .lo .o .obj
+$(srcdir)/Makefile.in: $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib/dhcpsrv/tests/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib/dhcpsrv/tests/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+test_libraries.h: $(top_builddir)/config.status $(srcdir)/test_libraries.h.in
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libco1.la: $(libco1_la_OBJECTS) $(libco1_la_DEPENDENCIES) $(EXTRA_libco1_la_DEPENDENCIES)
+ $(AM_V_CXXLD)$(libco1_la_LINK) $(am_libco1_la_rpath) $(libco1_la_OBJECTS) $(libco1_la_LIBADD) $(LIBS)
+
+libco2.la: $(libco2_la_OBJECTS) $(libco2_la_DEPENDENCIES) $(EXTRA_libco2_la_DEPENDENCIES)
+ $(AM_V_CXXLD)$(libco2_la_LINK) $(am_libco2_la_rpath) $(libco2_la_OBJECTS) $(libco2_la_LIBADD) $(LIBS)
+
+libco3.la: $(libco3_la_OBJECTS) $(libco3_la_DEPENDENCIES) $(EXTRA_libco3_la_DEPENDENCIES)
+ $(AM_V_CXXLD)$(libco3_la_LINK) $(am_libco3_la_rpath) $(libco3_la_OBJECTS) $(libco3_la_LIBADD) $(LIBS)
+
+libdhcpsrv_unittests$(EXEEXT): $(libdhcpsrv_unittests_OBJECTS) $(libdhcpsrv_unittests_DEPENDENCIES) $(EXTRA_libdhcpsrv_unittests_DEPENDENCIES)
+ @rm -f libdhcpsrv_unittests$(EXEEXT)
+ $(AM_V_CXXLD)$(libdhcpsrv_unittests_LINK) $(libdhcpsrv_unittests_OBJECTS) $(libdhcpsrv_unittests_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libco1_la-callout_library.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libco2_la-callout_library.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libco3_la-callout_params_library.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine4_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine6_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine_expiration_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine_hooks_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine_utils.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-callout_handle_store_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cb_ctl_dhcp_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_db_access_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_duid_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_expiration_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_host_operations_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_hosts_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_iface_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_mac_source_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_multi_threading_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_option_def_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_option_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_rsoo_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks4_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks6_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_subnets4_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfg_subnets6_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-cfgmgr_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-client_class_def_parser_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-client_class_def_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-csv_lease_file4_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-csv_lease_file6_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-d2_client_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-d2_udp_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-dhcp4o6_ipc_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-dhcp_parsers_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-duid_config_parser_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-expiration_config_parser_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-free_lease_queue_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-generic_lease_mgr_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-host_cache_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-host_data_source_factory_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-host_mgr_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-host_reservation_parser_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-host_reservations_list_parser_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-host_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-ifaces_config_parser_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-ip_range_permutation_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-ip_range_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-lease_file_loader_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-lease_mgr_factory_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-lease_mgr_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-lease_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-memfile_lease_limits_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-memfile_lease_mgr_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-multi_threading_config_parser_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-mysql_host_data_source_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-mysql_lease_mgr_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-ncr_generator_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-network_state_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-network_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-pgsql_host_data_source_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-pgsql_lease_mgr_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-pool_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-resource_handler_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-run_unittests.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-sanity_checks_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-shared_network_parser_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-shared_network_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-shared_networks_list_parser_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-srv_config_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-subnet_unittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-test_get_callout_handle.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdhcpsrv_unittests-timer_mgr_unittest.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.cc.o:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXXCOMPILE) -c -o $@ $<
+
+.cc.obj:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXXCOMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.cc.lo:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LTCXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LTCXXCOMPILE) -c -o $@ $<
+
+libco1_la-callout_library.lo: callout_library.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libco1_la_CPPFLAGS) $(CPPFLAGS) $(libco1_la_CXXFLAGS) $(CXXFLAGS) -MT libco1_la-callout_library.lo -MD -MP -MF $(DEPDIR)/libco1_la-callout_library.Tpo -c -o libco1_la-callout_library.lo `test -f 'callout_library.cc' || echo '$(srcdir)/'`callout_library.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libco1_la-callout_library.Tpo $(DEPDIR)/libco1_la-callout_library.Plo
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='callout_library.cc' object='libco1_la-callout_library.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libco1_la_CPPFLAGS) $(CPPFLAGS) $(libco1_la_CXXFLAGS) $(CXXFLAGS) -c -o libco1_la-callout_library.lo `test -f 'callout_library.cc' || echo '$(srcdir)/'`callout_library.cc
+
+libco2_la-callout_library.lo: callout_library.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libco2_la_CPPFLAGS) $(CPPFLAGS) $(libco2_la_CXXFLAGS) $(CXXFLAGS) -MT libco2_la-callout_library.lo -MD -MP -MF $(DEPDIR)/libco2_la-callout_library.Tpo -c -o libco2_la-callout_library.lo `test -f 'callout_library.cc' || echo '$(srcdir)/'`callout_library.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libco2_la-callout_library.Tpo $(DEPDIR)/libco2_la-callout_library.Plo
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='callout_library.cc' object='libco2_la-callout_library.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libco2_la_CPPFLAGS) $(CPPFLAGS) $(libco2_la_CXXFLAGS) $(CXXFLAGS) -c -o libco2_la-callout_library.lo `test -f 'callout_library.cc' || echo '$(srcdir)/'`callout_library.cc
+
+libco3_la-callout_params_library.lo: callout_params_library.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libco3_la_CPPFLAGS) $(CPPFLAGS) $(libco3_la_CXXFLAGS) $(CXXFLAGS) -MT libco3_la-callout_params_library.lo -MD -MP -MF $(DEPDIR)/libco3_la-callout_params_library.Tpo -c -o libco3_la-callout_params_library.lo `test -f 'callout_params_library.cc' || echo '$(srcdir)/'`callout_params_library.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libco3_la-callout_params_library.Tpo $(DEPDIR)/libco3_la-callout_params_library.Plo
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='callout_params_library.cc' object='libco3_la-callout_params_library.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libco3_la_CPPFLAGS) $(CPPFLAGS) $(libco3_la_CXXFLAGS) $(CXXFLAGS) -c -o libco3_la-callout_params_library.lo `test -f 'callout_params_library.cc' || echo '$(srcdir)/'`callout_params_library.cc
+
+libdhcpsrv_unittests-run_unittests.o: run_unittests.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-run_unittests.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-run_unittests.Tpo -c -o libdhcpsrv_unittests-run_unittests.o `test -f 'run_unittests.cc' || echo '$(srcdir)/'`run_unittests.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-run_unittests.Tpo $(DEPDIR)/libdhcpsrv_unittests-run_unittests.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='run_unittests.cc' object='libdhcpsrv_unittests-run_unittests.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-run_unittests.o `test -f 'run_unittests.cc' || echo '$(srcdir)/'`run_unittests.cc
+
+libdhcpsrv_unittests-run_unittests.obj: run_unittests.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-run_unittests.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-run_unittests.Tpo -c -o libdhcpsrv_unittests-run_unittests.obj `if test -f 'run_unittests.cc'; then $(CYGPATH_W) 'run_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/run_unittests.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-run_unittests.Tpo $(DEPDIR)/libdhcpsrv_unittests-run_unittests.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='run_unittests.cc' object='libdhcpsrv_unittests-run_unittests.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-run_unittests.obj `if test -f 'run_unittests.cc'; then $(CYGPATH_W) 'run_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/run_unittests.cc'; fi`
+
+libdhcpsrv_unittests-alloc_engine_utils.o: alloc_engine_utils.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-alloc_engine_utils.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_utils.Tpo -c -o libdhcpsrv_unittests-alloc_engine_utils.o `test -f 'alloc_engine_utils.cc' || echo '$(srcdir)/'`alloc_engine_utils.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_utils.Tpo $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_utils.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='alloc_engine_utils.cc' object='libdhcpsrv_unittests-alloc_engine_utils.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-alloc_engine_utils.o `test -f 'alloc_engine_utils.cc' || echo '$(srcdir)/'`alloc_engine_utils.cc
+
+libdhcpsrv_unittests-alloc_engine_utils.obj: alloc_engine_utils.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-alloc_engine_utils.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_utils.Tpo -c -o libdhcpsrv_unittests-alloc_engine_utils.obj `if test -f 'alloc_engine_utils.cc'; then $(CYGPATH_W) 'alloc_engine_utils.cc'; else $(CYGPATH_W) '$(srcdir)/alloc_engine_utils.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_utils.Tpo $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_utils.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='alloc_engine_utils.cc' object='libdhcpsrv_unittests-alloc_engine_utils.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-alloc_engine_utils.obj `if test -f 'alloc_engine_utils.cc'; then $(CYGPATH_W) 'alloc_engine_utils.cc'; else $(CYGPATH_W) '$(srcdir)/alloc_engine_utils.cc'; fi`
+
+libdhcpsrv_unittests-alloc_engine_expiration_unittest.o: alloc_engine_expiration_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-alloc_engine_expiration_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_expiration_unittest.Tpo -c -o libdhcpsrv_unittests-alloc_engine_expiration_unittest.o `test -f 'alloc_engine_expiration_unittest.cc' || echo '$(srcdir)/'`alloc_engine_expiration_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_expiration_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_expiration_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='alloc_engine_expiration_unittest.cc' object='libdhcpsrv_unittests-alloc_engine_expiration_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-alloc_engine_expiration_unittest.o `test -f 'alloc_engine_expiration_unittest.cc' || echo '$(srcdir)/'`alloc_engine_expiration_unittest.cc
+
+libdhcpsrv_unittests-alloc_engine_expiration_unittest.obj: alloc_engine_expiration_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-alloc_engine_expiration_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_expiration_unittest.Tpo -c -o libdhcpsrv_unittests-alloc_engine_expiration_unittest.obj `if test -f 'alloc_engine_expiration_unittest.cc'; then $(CYGPATH_W) 'alloc_engine_expiration_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/alloc_engine_expiration_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_expiration_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_expiration_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='alloc_engine_expiration_unittest.cc' object='libdhcpsrv_unittests-alloc_engine_expiration_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-alloc_engine_expiration_unittest.obj `if test -f 'alloc_engine_expiration_unittest.cc'; then $(CYGPATH_W) 'alloc_engine_expiration_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/alloc_engine_expiration_unittest.cc'; fi`
+
+libdhcpsrv_unittests-alloc_engine_hooks_unittest.o: alloc_engine_hooks_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-alloc_engine_hooks_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_hooks_unittest.Tpo -c -o libdhcpsrv_unittests-alloc_engine_hooks_unittest.o `test -f 'alloc_engine_hooks_unittest.cc' || echo '$(srcdir)/'`alloc_engine_hooks_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_hooks_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_hooks_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='alloc_engine_hooks_unittest.cc' object='libdhcpsrv_unittests-alloc_engine_hooks_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-alloc_engine_hooks_unittest.o `test -f 'alloc_engine_hooks_unittest.cc' || echo '$(srcdir)/'`alloc_engine_hooks_unittest.cc
+
+libdhcpsrv_unittests-alloc_engine_hooks_unittest.obj: alloc_engine_hooks_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-alloc_engine_hooks_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_hooks_unittest.Tpo -c -o libdhcpsrv_unittests-alloc_engine_hooks_unittest.obj `if test -f 'alloc_engine_hooks_unittest.cc'; then $(CYGPATH_W) 'alloc_engine_hooks_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/alloc_engine_hooks_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_hooks_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-alloc_engine_hooks_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='alloc_engine_hooks_unittest.cc' object='libdhcpsrv_unittests-alloc_engine_hooks_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-alloc_engine_hooks_unittest.obj `if test -f 'alloc_engine_hooks_unittest.cc'; then $(CYGPATH_W) 'alloc_engine_hooks_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/alloc_engine_hooks_unittest.cc'; fi`
+
+libdhcpsrv_unittests-alloc_engine4_unittest.o: alloc_engine4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-alloc_engine4_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-alloc_engine4_unittest.Tpo -c -o libdhcpsrv_unittests-alloc_engine4_unittest.o `test -f 'alloc_engine4_unittest.cc' || echo '$(srcdir)/'`alloc_engine4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-alloc_engine4_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-alloc_engine4_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='alloc_engine4_unittest.cc' object='libdhcpsrv_unittests-alloc_engine4_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-alloc_engine4_unittest.o `test -f 'alloc_engine4_unittest.cc' || echo '$(srcdir)/'`alloc_engine4_unittest.cc
+
+libdhcpsrv_unittests-alloc_engine4_unittest.obj: alloc_engine4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-alloc_engine4_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-alloc_engine4_unittest.Tpo -c -o libdhcpsrv_unittests-alloc_engine4_unittest.obj `if test -f 'alloc_engine4_unittest.cc'; then $(CYGPATH_W) 'alloc_engine4_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/alloc_engine4_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-alloc_engine4_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-alloc_engine4_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='alloc_engine4_unittest.cc' object='libdhcpsrv_unittests-alloc_engine4_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-alloc_engine4_unittest.obj `if test -f 'alloc_engine4_unittest.cc'; then $(CYGPATH_W) 'alloc_engine4_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/alloc_engine4_unittest.cc'; fi`
+
+libdhcpsrv_unittests-alloc_engine6_unittest.o: alloc_engine6_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-alloc_engine6_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-alloc_engine6_unittest.Tpo -c -o libdhcpsrv_unittests-alloc_engine6_unittest.o `test -f 'alloc_engine6_unittest.cc' || echo '$(srcdir)/'`alloc_engine6_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-alloc_engine6_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-alloc_engine6_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='alloc_engine6_unittest.cc' object='libdhcpsrv_unittests-alloc_engine6_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-alloc_engine6_unittest.o `test -f 'alloc_engine6_unittest.cc' || echo '$(srcdir)/'`alloc_engine6_unittest.cc
+
+libdhcpsrv_unittests-alloc_engine6_unittest.obj: alloc_engine6_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-alloc_engine6_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-alloc_engine6_unittest.Tpo -c -o libdhcpsrv_unittests-alloc_engine6_unittest.obj `if test -f 'alloc_engine6_unittest.cc'; then $(CYGPATH_W) 'alloc_engine6_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/alloc_engine6_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-alloc_engine6_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-alloc_engine6_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='alloc_engine6_unittest.cc' object='libdhcpsrv_unittests-alloc_engine6_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-alloc_engine6_unittest.obj `if test -f 'alloc_engine6_unittest.cc'; then $(CYGPATH_W) 'alloc_engine6_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/alloc_engine6_unittest.cc'; fi`
+
+libdhcpsrv_unittests-callout_handle_store_unittest.o: callout_handle_store_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-callout_handle_store_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-callout_handle_store_unittest.Tpo -c -o libdhcpsrv_unittests-callout_handle_store_unittest.o `test -f 'callout_handle_store_unittest.cc' || echo '$(srcdir)/'`callout_handle_store_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-callout_handle_store_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-callout_handle_store_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='callout_handle_store_unittest.cc' object='libdhcpsrv_unittests-callout_handle_store_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-callout_handle_store_unittest.o `test -f 'callout_handle_store_unittest.cc' || echo '$(srcdir)/'`callout_handle_store_unittest.cc
+
+libdhcpsrv_unittests-callout_handle_store_unittest.obj: callout_handle_store_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-callout_handle_store_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-callout_handle_store_unittest.Tpo -c -o libdhcpsrv_unittests-callout_handle_store_unittest.obj `if test -f 'callout_handle_store_unittest.cc'; then $(CYGPATH_W) 'callout_handle_store_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/callout_handle_store_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-callout_handle_store_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-callout_handle_store_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='callout_handle_store_unittest.cc' object='libdhcpsrv_unittests-callout_handle_store_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-callout_handle_store_unittest.obj `if test -f 'callout_handle_store_unittest.cc'; then $(CYGPATH_W) 'callout_handle_store_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/callout_handle_store_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cb_ctl_dhcp_unittest.o: cb_ctl_dhcp_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cb_ctl_dhcp_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cb_ctl_dhcp_unittest.Tpo -c -o libdhcpsrv_unittests-cb_ctl_dhcp_unittest.o `test -f 'cb_ctl_dhcp_unittest.cc' || echo '$(srcdir)/'`cb_ctl_dhcp_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cb_ctl_dhcp_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cb_ctl_dhcp_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cb_ctl_dhcp_unittest.cc' object='libdhcpsrv_unittests-cb_ctl_dhcp_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cb_ctl_dhcp_unittest.o `test -f 'cb_ctl_dhcp_unittest.cc' || echo '$(srcdir)/'`cb_ctl_dhcp_unittest.cc
+
+libdhcpsrv_unittests-cb_ctl_dhcp_unittest.obj: cb_ctl_dhcp_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cb_ctl_dhcp_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cb_ctl_dhcp_unittest.Tpo -c -o libdhcpsrv_unittests-cb_ctl_dhcp_unittest.obj `if test -f 'cb_ctl_dhcp_unittest.cc'; then $(CYGPATH_W) 'cb_ctl_dhcp_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cb_ctl_dhcp_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cb_ctl_dhcp_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cb_ctl_dhcp_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cb_ctl_dhcp_unittest.cc' object='libdhcpsrv_unittests-cb_ctl_dhcp_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cb_ctl_dhcp_unittest.obj `if test -f 'cb_ctl_dhcp_unittest.cc'; then $(CYGPATH_W) 'cb_ctl_dhcp_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cb_ctl_dhcp_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_db_access_unittest.o: cfg_db_access_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_db_access_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_db_access_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_db_access_unittest.o `test -f 'cfg_db_access_unittest.cc' || echo '$(srcdir)/'`cfg_db_access_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_db_access_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_db_access_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_db_access_unittest.cc' object='libdhcpsrv_unittests-cfg_db_access_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_db_access_unittest.o `test -f 'cfg_db_access_unittest.cc' || echo '$(srcdir)/'`cfg_db_access_unittest.cc
+
+libdhcpsrv_unittests-cfg_db_access_unittest.obj: cfg_db_access_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_db_access_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_db_access_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_db_access_unittest.obj `if test -f 'cfg_db_access_unittest.cc'; then $(CYGPATH_W) 'cfg_db_access_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_db_access_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_db_access_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_db_access_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_db_access_unittest.cc' object='libdhcpsrv_unittests-cfg_db_access_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_db_access_unittest.obj `if test -f 'cfg_db_access_unittest.cc'; then $(CYGPATH_W) 'cfg_db_access_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_db_access_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_duid_unittest.o: cfg_duid_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_duid_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_duid_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_duid_unittest.o `test -f 'cfg_duid_unittest.cc' || echo '$(srcdir)/'`cfg_duid_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_duid_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_duid_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_duid_unittest.cc' object='libdhcpsrv_unittests-cfg_duid_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_duid_unittest.o `test -f 'cfg_duid_unittest.cc' || echo '$(srcdir)/'`cfg_duid_unittest.cc
+
+libdhcpsrv_unittests-cfg_duid_unittest.obj: cfg_duid_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_duid_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_duid_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_duid_unittest.obj `if test -f 'cfg_duid_unittest.cc'; then $(CYGPATH_W) 'cfg_duid_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_duid_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_duid_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_duid_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_duid_unittest.cc' object='libdhcpsrv_unittests-cfg_duid_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_duid_unittest.obj `if test -f 'cfg_duid_unittest.cc'; then $(CYGPATH_W) 'cfg_duid_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_duid_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_expiration_unittest.o: cfg_expiration_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_expiration_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_expiration_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_expiration_unittest.o `test -f 'cfg_expiration_unittest.cc' || echo '$(srcdir)/'`cfg_expiration_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_expiration_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_expiration_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_expiration_unittest.cc' object='libdhcpsrv_unittests-cfg_expiration_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_expiration_unittest.o `test -f 'cfg_expiration_unittest.cc' || echo '$(srcdir)/'`cfg_expiration_unittest.cc
+
+libdhcpsrv_unittests-cfg_expiration_unittest.obj: cfg_expiration_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_expiration_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_expiration_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_expiration_unittest.obj `if test -f 'cfg_expiration_unittest.cc'; then $(CYGPATH_W) 'cfg_expiration_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_expiration_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_expiration_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_expiration_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_expiration_unittest.cc' object='libdhcpsrv_unittests-cfg_expiration_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_expiration_unittest.obj `if test -f 'cfg_expiration_unittest.cc'; then $(CYGPATH_W) 'cfg_expiration_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_expiration_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_host_operations_unittest.o: cfg_host_operations_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_host_operations_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_host_operations_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_host_operations_unittest.o `test -f 'cfg_host_operations_unittest.cc' || echo '$(srcdir)/'`cfg_host_operations_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_host_operations_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_host_operations_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_host_operations_unittest.cc' object='libdhcpsrv_unittests-cfg_host_operations_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_host_operations_unittest.o `test -f 'cfg_host_operations_unittest.cc' || echo '$(srcdir)/'`cfg_host_operations_unittest.cc
+
+libdhcpsrv_unittests-cfg_host_operations_unittest.obj: cfg_host_operations_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_host_operations_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_host_operations_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_host_operations_unittest.obj `if test -f 'cfg_host_operations_unittest.cc'; then $(CYGPATH_W) 'cfg_host_operations_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_host_operations_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_host_operations_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_host_operations_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_host_operations_unittest.cc' object='libdhcpsrv_unittests-cfg_host_operations_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_host_operations_unittest.obj `if test -f 'cfg_host_operations_unittest.cc'; then $(CYGPATH_W) 'cfg_host_operations_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_host_operations_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_hosts_unittest.o: cfg_hosts_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_hosts_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_hosts_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_hosts_unittest.o `test -f 'cfg_hosts_unittest.cc' || echo '$(srcdir)/'`cfg_hosts_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_hosts_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_hosts_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_hosts_unittest.cc' object='libdhcpsrv_unittests-cfg_hosts_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_hosts_unittest.o `test -f 'cfg_hosts_unittest.cc' || echo '$(srcdir)/'`cfg_hosts_unittest.cc
+
+libdhcpsrv_unittests-cfg_hosts_unittest.obj: cfg_hosts_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_hosts_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_hosts_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_hosts_unittest.obj `if test -f 'cfg_hosts_unittest.cc'; then $(CYGPATH_W) 'cfg_hosts_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_hosts_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_hosts_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_hosts_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_hosts_unittest.cc' object='libdhcpsrv_unittests-cfg_hosts_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_hosts_unittest.obj `if test -f 'cfg_hosts_unittest.cc'; then $(CYGPATH_W) 'cfg_hosts_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_hosts_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_iface_unittest.o: cfg_iface_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_iface_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_iface_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_iface_unittest.o `test -f 'cfg_iface_unittest.cc' || echo '$(srcdir)/'`cfg_iface_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_iface_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_iface_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_iface_unittest.cc' object='libdhcpsrv_unittests-cfg_iface_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_iface_unittest.o `test -f 'cfg_iface_unittest.cc' || echo '$(srcdir)/'`cfg_iface_unittest.cc
+
+libdhcpsrv_unittests-cfg_iface_unittest.obj: cfg_iface_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_iface_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_iface_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_iface_unittest.obj `if test -f 'cfg_iface_unittest.cc'; then $(CYGPATH_W) 'cfg_iface_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_iface_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_iface_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_iface_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_iface_unittest.cc' object='libdhcpsrv_unittests-cfg_iface_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_iface_unittest.obj `if test -f 'cfg_iface_unittest.cc'; then $(CYGPATH_W) 'cfg_iface_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_iface_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_mac_source_unittest.o: cfg_mac_source_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_mac_source_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_mac_source_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_mac_source_unittest.o `test -f 'cfg_mac_source_unittest.cc' || echo '$(srcdir)/'`cfg_mac_source_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_mac_source_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_mac_source_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_mac_source_unittest.cc' object='libdhcpsrv_unittests-cfg_mac_source_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_mac_source_unittest.o `test -f 'cfg_mac_source_unittest.cc' || echo '$(srcdir)/'`cfg_mac_source_unittest.cc
+
+libdhcpsrv_unittests-cfg_mac_source_unittest.obj: cfg_mac_source_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_mac_source_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_mac_source_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_mac_source_unittest.obj `if test -f 'cfg_mac_source_unittest.cc'; then $(CYGPATH_W) 'cfg_mac_source_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_mac_source_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_mac_source_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_mac_source_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_mac_source_unittest.cc' object='libdhcpsrv_unittests-cfg_mac_source_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_mac_source_unittest.obj `if test -f 'cfg_mac_source_unittest.cc'; then $(CYGPATH_W) 'cfg_mac_source_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_mac_source_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_multi_threading_unittest.o: cfg_multi_threading_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_multi_threading_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_multi_threading_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_multi_threading_unittest.o `test -f 'cfg_multi_threading_unittest.cc' || echo '$(srcdir)/'`cfg_multi_threading_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_multi_threading_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_multi_threading_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_multi_threading_unittest.cc' object='libdhcpsrv_unittests-cfg_multi_threading_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_multi_threading_unittest.o `test -f 'cfg_multi_threading_unittest.cc' || echo '$(srcdir)/'`cfg_multi_threading_unittest.cc
+
+libdhcpsrv_unittests-cfg_multi_threading_unittest.obj: cfg_multi_threading_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_multi_threading_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_multi_threading_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_multi_threading_unittest.obj `if test -f 'cfg_multi_threading_unittest.cc'; then $(CYGPATH_W) 'cfg_multi_threading_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_multi_threading_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_multi_threading_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_multi_threading_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_multi_threading_unittest.cc' object='libdhcpsrv_unittests-cfg_multi_threading_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_multi_threading_unittest.obj `if test -f 'cfg_multi_threading_unittest.cc'; then $(CYGPATH_W) 'cfg_multi_threading_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_multi_threading_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_option_unittest.o: cfg_option_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_option_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_option_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_option_unittest.o `test -f 'cfg_option_unittest.cc' || echo '$(srcdir)/'`cfg_option_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_option_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_option_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_option_unittest.cc' object='libdhcpsrv_unittests-cfg_option_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_option_unittest.o `test -f 'cfg_option_unittest.cc' || echo '$(srcdir)/'`cfg_option_unittest.cc
+
+libdhcpsrv_unittests-cfg_option_unittest.obj: cfg_option_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_option_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_option_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_option_unittest.obj `if test -f 'cfg_option_unittest.cc'; then $(CYGPATH_W) 'cfg_option_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_option_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_option_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_option_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_option_unittest.cc' object='libdhcpsrv_unittests-cfg_option_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_option_unittest.obj `if test -f 'cfg_option_unittest.cc'; then $(CYGPATH_W) 'cfg_option_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_option_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_option_def_unittest.o: cfg_option_def_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_option_def_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_option_def_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_option_def_unittest.o `test -f 'cfg_option_def_unittest.cc' || echo '$(srcdir)/'`cfg_option_def_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_option_def_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_option_def_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_option_def_unittest.cc' object='libdhcpsrv_unittests-cfg_option_def_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_option_def_unittest.o `test -f 'cfg_option_def_unittest.cc' || echo '$(srcdir)/'`cfg_option_def_unittest.cc
+
+libdhcpsrv_unittests-cfg_option_def_unittest.obj: cfg_option_def_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_option_def_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_option_def_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_option_def_unittest.obj `if test -f 'cfg_option_def_unittest.cc'; then $(CYGPATH_W) 'cfg_option_def_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_option_def_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_option_def_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_option_def_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_option_def_unittest.cc' object='libdhcpsrv_unittests-cfg_option_def_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_option_def_unittest.obj `if test -f 'cfg_option_def_unittest.cc'; then $(CYGPATH_W) 'cfg_option_def_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_option_def_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_rsoo_unittest.o: cfg_rsoo_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_rsoo_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_rsoo_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_rsoo_unittest.o `test -f 'cfg_rsoo_unittest.cc' || echo '$(srcdir)/'`cfg_rsoo_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_rsoo_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_rsoo_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_rsoo_unittest.cc' object='libdhcpsrv_unittests-cfg_rsoo_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_rsoo_unittest.o `test -f 'cfg_rsoo_unittest.cc' || echo '$(srcdir)/'`cfg_rsoo_unittest.cc
+
+libdhcpsrv_unittests-cfg_rsoo_unittest.obj: cfg_rsoo_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_rsoo_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_rsoo_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_rsoo_unittest.obj `if test -f 'cfg_rsoo_unittest.cc'; then $(CYGPATH_W) 'cfg_rsoo_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_rsoo_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_rsoo_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_rsoo_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_rsoo_unittest.cc' object='libdhcpsrv_unittests-cfg_rsoo_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_rsoo_unittest.obj `if test -f 'cfg_rsoo_unittest.cc'; then $(CYGPATH_W) 'cfg_rsoo_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_rsoo_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_shared_networks4_unittest.o: cfg_shared_networks4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_shared_networks4_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks4_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_shared_networks4_unittest.o `test -f 'cfg_shared_networks4_unittest.cc' || echo '$(srcdir)/'`cfg_shared_networks4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks4_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks4_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_shared_networks4_unittest.cc' object='libdhcpsrv_unittests-cfg_shared_networks4_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_shared_networks4_unittest.o `test -f 'cfg_shared_networks4_unittest.cc' || echo '$(srcdir)/'`cfg_shared_networks4_unittest.cc
+
+libdhcpsrv_unittests-cfg_shared_networks4_unittest.obj: cfg_shared_networks4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_shared_networks4_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks4_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_shared_networks4_unittest.obj `if test -f 'cfg_shared_networks4_unittest.cc'; then $(CYGPATH_W) 'cfg_shared_networks4_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_shared_networks4_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks4_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks4_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_shared_networks4_unittest.cc' object='libdhcpsrv_unittests-cfg_shared_networks4_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_shared_networks4_unittest.obj `if test -f 'cfg_shared_networks4_unittest.cc'; then $(CYGPATH_W) 'cfg_shared_networks4_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_shared_networks4_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_shared_networks6_unittest.o: cfg_shared_networks6_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_shared_networks6_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks6_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_shared_networks6_unittest.o `test -f 'cfg_shared_networks6_unittest.cc' || echo '$(srcdir)/'`cfg_shared_networks6_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks6_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks6_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_shared_networks6_unittest.cc' object='libdhcpsrv_unittests-cfg_shared_networks6_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_shared_networks6_unittest.o `test -f 'cfg_shared_networks6_unittest.cc' || echo '$(srcdir)/'`cfg_shared_networks6_unittest.cc
+
+libdhcpsrv_unittests-cfg_shared_networks6_unittest.obj: cfg_shared_networks6_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_shared_networks6_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks6_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_shared_networks6_unittest.obj `if test -f 'cfg_shared_networks6_unittest.cc'; then $(CYGPATH_W) 'cfg_shared_networks6_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_shared_networks6_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks6_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks6_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_shared_networks6_unittest.cc' object='libdhcpsrv_unittests-cfg_shared_networks6_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_shared_networks6_unittest.obj `if test -f 'cfg_shared_networks6_unittest.cc'; then $(CYGPATH_W) 'cfg_shared_networks6_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_shared_networks6_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_subnets4_unittest.o: cfg_subnets4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_subnets4_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_subnets4_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_subnets4_unittest.o `test -f 'cfg_subnets4_unittest.cc' || echo '$(srcdir)/'`cfg_subnets4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_subnets4_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_subnets4_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_subnets4_unittest.cc' object='libdhcpsrv_unittests-cfg_subnets4_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_subnets4_unittest.o `test -f 'cfg_subnets4_unittest.cc' || echo '$(srcdir)/'`cfg_subnets4_unittest.cc
+
+libdhcpsrv_unittests-cfg_subnets4_unittest.obj: cfg_subnets4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_subnets4_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_subnets4_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_subnets4_unittest.obj `if test -f 'cfg_subnets4_unittest.cc'; then $(CYGPATH_W) 'cfg_subnets4_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_subnets4_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_subnets4_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_subnets4_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_subnets4_unittest.cc' object='libdhcpsrv_unittests-cfg_subnets4_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_subnets4_unittest.obj `if test -f 'cfg_subnets4_unittest.cc'; then $(CYGPATH_W) 'cfg_subnets4_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_subnets4_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfg_subnets6_unittest.o: cfg_subnets6_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_subnets6_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_subnets6_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_subnets6_unittest.o `test -f 'cfg_subnets6_unittest.cc' || echo '$(srcdir)/'`cfg_subnets6_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_subnets6_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_subnets6_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_subnets6_unittest.cc' object='libdhcpsrv_unittests-cfg_subnets6_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_subnets6_unittest.o `test -f 'cfg_subnets6_unittest.cc' || echo '$(srcdir)/'`cfg_subnets6_unittest.cc
+
+libdhcpsrv_unittests-cfg_subnets6_unittest.obj: cfg_subnets6_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfg_subnets6_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfg_subnets6_unittest.Tpo -c -o libdhcpsrv_unittests-cfg_subnets6_unittest.obj `if test -f 'cfg_subnets6_unittest.cc'; then $(CYGPATH_W) 'cfg_subnets6_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_subnets6_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfg_subnets6_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfg_subnets6_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfg_subnets6_unittest.cc' object='libdhcpsrv_unittests-cfg_subnets6_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfg_subnets6_unittest.obj `if test -f 'cfg_subnets6_unittest.cc'; then $(CYGPATH_W) 'cfg_subnets6_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfg_subnets6_unittest.cc'; fi`
+
+libdhcpsrv_unittests-cfgmgr_unittest.o: cfgmgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfgmgr_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfgmgr_unittest.Tpo -c -o libdhcpsrv_unittests-cfgmgr_unittest.o `test -f 'cfgmgr_unittest.cc' || echo '$(srcdir)/'`cfgmgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfgmgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfgmgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfgmgr_unittest.cc' object='libdhcpsrv_unittests-cfgmgr_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfgmgr_unittest.o `test -f 'cfgmgr_unittest.cc' || echo '$(srcdir)/'`cfgmgr_unittest.cc
+
+libdhcpsrv_unittests-cfgmgr_unittest.obj: cfgmgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-cfgmgr_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-cfgmgr_unittest.Tpo -c -o libdhcpsrv_unittests-cfgmgr_unittest.obj `if test -f 'cfgmgr_unittest.cc'; then $(CYGPATH_W) 'cfgmgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfgmgr_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-cfgmgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-cfgmgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='cfgmgr_unittest.cc' object='libdhcpsrv_unittests-cfgmgr_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-cfgmgr_unittest.obj `if test -f 'cfgmgr_unittest.cc'; then $(CYGPATH_W) 'cfgmgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/cfgmgr_unittest.cc'; fi`
+
+libdhcpsrv_unittests-client_class_def_unittest.o: client_class_def_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-client_class_def_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-client_class_def_unittest.Tpo -c -o libdhcpsrv_unittests-client_class_def_unittest.o `test -f 'client_class_def_unittest.cc' || echo '$(srcdir)/'`client_class_def_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-client_class_def_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-client_class_def_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='client_class_def_unittest.cc' object='libdhcpsrv_unittests-client_class_def_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-client_class_def_unittest.o `test -f 'client_class_def_unittest.cc' || echo '$(srcdir)/'`client_class_def_unittest.cc
+
+libdhcpsrv_unittests-client_class_def_unittest.obj: client_class_def_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-client_class_def_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-client_class_def_unittest.Tpo -c -o libdhcpsrv_unittests-client_class_def_unittest.obj `if test -f 'client_class_def_unittest.cc'; then $(CYGPATH_W) 'client_class_def_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/client_class_def_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-client_class_def_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-client_class_def_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='client_class_def_unittest.cc' object='libdhcpsrv_unittests-client_class_def_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-client_class_def_unittest.obj `if test -f 'client_class_def_unittest.cc'; then $(CYGPATH_W) 'client_class_def_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/client_class_def_unittest.cc'; fi`
+
+libdhcpsrv_unittests-client_class_def_parser_unittest.o: client_class_def_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-client_class_def_parser_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-client_class_def_parser_unittest.Tpo -c -o libdhcpsrv_unittests-client_class_def_parser_unittest.o `test -f 'client_class_def_parser_unittest.cc' || echo '$(srcdir)/'`client_class_def_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-client_class_def_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-client_class_def_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='client_class_def_parser_unittest.cc' object='libdhcpsrv_unittests-client_class_def_parser_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-client_class_def_parser_unittest.o `test -f 'client_class_def_parser_unittest.cc' || echo '$(srcdir)/'`client_class_def_parser_unittest.cc
+
+libdhcpsrv_unittests-client_class_def_parser_unittest.obj: client_class_def_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-client_class_def_parser_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-client_class_def_parser_unittest.Tpo -c -o libdhcpsrv_unittests-client_class_def_parser_unittest.obj `if test -f 'client_class_def_parser_unittest.cc'; then $(CYGPATH_W) 'client_class_def_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/client_class_def_parser_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-client_class_def_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-client_class_def_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='client_class_def_parser_unittest.cc' object='libdhcpsrv_unittests-client_class_def_parser_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-client_class_def_parser_unittest.obj `if test -f 'client_class_def_parser_unittest.cc'; then $(CYGPATH_W) 'client_class_def_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/client_class_def_parser_unittest.cc'; fi`
+
+libdhcpsrv_unittests-csv_lease_file4_unittest.o: csv_lease_file4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-csv_lease_file4_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-csv_lease_file4_unittest.Tpo -c -o libdhcpsrv_unittests-csv_lease_file4_unittest.o `test -f 'csv_lease_file4_unittest.cc' || echo '$(srcdir)/'`csv_lease_file4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-csv_lease_file4_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-csv_lease_file4_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='csv_lease_file4_unittest.cc' object='libdhcpsrv_unittests-csv_lease_file4_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-csv_lease_file4_unittest.o `test -f 'csv_lease_file4_unittest.cc' || echo '$(srcdir)/'`csv_lease_file4_unittest.cc
+
+libdhcpsrv_unittests-csv_lease_file4_unittest.obj: csv_lease_file4_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-csv_lease_file4_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-csv_lease_file4_unittest.Tpo -c -o libdhcpsrv_unittests-csv_lease_file4_unittest.obj `if test -f 'csv_lease_file4_unittest.cc'; then $(CYGPATH_W) 'csv_lease_file4_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/csv_lease_file4_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-csv_lease_file4_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-csv_lease_file4_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='csv_lease_file4_unittest.cc' object='libdhcpsrv_unittests-csv_lease_file4_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-csv_lease_file4_unittest.obj `if test -f 'csv_lease_file4_unittest.cc'; then $(CYGPATH_W) 'csv_lease_file4_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/csv_lease_file4_unittest.cc'; fi`
+
+libdhcpsrv_unittests-csv_lease_file6_unittest.o: csv_lease_file6_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-csv_lease_file6_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-csv_lease_file6_unittest.Tpo -c -o libdhcpsrv_unittests-csv_lease_file6_unittest.o `test -f 'csv_lease_file6_unittest.cc' || echo '$(srcdir)/'`csv_lease_file6_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-csv_lease_file6_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-csv_lease_file6_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='csv_lease_file6_unittest.cc' object='libdhcpsrv_unittests-csv_lease_file6_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-csv_lease_file6_unittest.o `test -f 'csv_lease_file6_unittest.cc' || echo '$(srcdir)/'`csv_lease_file6_unittest.cc
+
+libdhcpsrv_unittests-csv_lease_file6_unittest.obj: csv_lease_file6_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-csv_lease_file6_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-csv_lease_file6_unittest.Tpo -c -o libdhcpsrv_unittests-csv_lease_file6_unittest.obj `if test -f 'csv_lease_file6_unittest.cc'; then $(CYGPATH_W) 'csv_lease_file6_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/csv_lease_file6_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-csv_lease_file6_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-csv_lease_file6_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='csv_lease_file6_unittest.cc' object='libdhcpsrv_unittests-csv_lease_file6_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-csv_lease_file6_unittest.obj `if test -f 'csv_lease_file6_unittest.cc'; then $(CYGPATH_W) 'csv_lease_file6_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/csv_lease_file6_unittest.cc'; fi`
+
+libdhcpsrv_unittests-d2_client_unittest.o: d2_client_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-d2_client_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-d2_client_unittest.Tpo -c -o libdhcpsrv_unittests-d2_client_unittest.o `test -f 'd2_client_unittest.cc' || echo '$(srcdir)/'`d2_client_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-d2_client_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-d2_client_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='d2_client_unittest.cc' object='libdhcpsrv_unittests-d2_client_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-d2_client_unittest.o `test -f 'd2_client_unittest.cc' || echo '$(srcdir)/'`d2_client_unittest.cc
+
+libdhcpsrv_unittests-d2_client_unittest.obj: d2_client_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-d2_client_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-d2_client_unittest.Tpo -c -o libdhcpsrv_unittests-d2_client_unittest.obj `if test -f 'd2_client_unittest.cc'; then $(CYGPATH_W) 'd2_client_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/d2_client_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-d2_client_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-d2_client_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='d2_client_unittest.cc' object='libdhcpsrv_unittests-d2_client_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-d2_client_unittest.obj `if test -f 'd2_client_unittest.cc'; then $(CYGPATH_W) 'd2_client_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/d2_client_unittest.cc'; fi`
+
+libdhcpsrv_unittests-d2_udp_unittest.o: d2_udp_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-d2_udp_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-d2_udp_unittest.Tpo -c -o libdhcpsrv_unittests-d2_udp_unittest.o `test -f 'd2_udp_unittest.cc' || echo '$(srcdir)/'`d2_udp_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-d2_udp_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-d2_udp_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='d2_udp_unittest.cc' object='libdhcpsrv_unittests-d2_udp_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-d2_udp_unittest.o `test -f 'd2_udp_unittest.cc' || echo '$(srcdir)/'`d2_udp_unittest.cc
+
+libdhcpsrv_unittests-d2_udp_unittest.obj: d2_udp_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-d2_udp_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-d2_udp_unittest.Tpo -c -o libdhcpsrv_unittests-d2_udp_unittest.obj `if test -f 'd2_udp_unittest.cc'; then $(CYGPATH_W) 'd2_udp_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/d2_udp_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-d2_udp_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-d2_udp_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='d2_udp_unittest.cc' object='libdhcpsrv_unittests-d2_udp_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-d2_udp_unittest.obj `if test -f 'd2_udp_unittest.cc'; then $(CYGPATH_W) 'd2_udp_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/d2_udp_unittest.cc'; fi`
+
+libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.o: dhcp_queue_control_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.Tpo -c -o libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.o `test -f 'dhcp_queue_control_parser_unittest.cc' || echo '$(srcdir)/'`dhcp_queue_control_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp_queue_control_parser_unittest.cc' object='libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.o `test -f 'dhcp_queue_control_parser_unittest.cc' || echo '$(srcdir)/'`dhcp_queue_control_parser_unittest.cc
+
+libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.obj: dhcp_queue_control_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.Tpo -c -o libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.obj `if test -f 'dhcp_queue_control_parser_unittest.cc'; then $(CYGPATH_W) 'dhcp_queue_control_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp_queue_control_parser_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp_queue_control_parser_unittest.cc' object='libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.obj `if test -f 'dhcp_queue_control_parser_unittest.cc'; then $(CYGPATH_W) 'dhcp_queue_control_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp_queue_control_parser_unittest.cc'; fi`
+
+libdhcpsrv_unittests-dhcp4o6_ipc_unittest.o: dhcp4o6_ipc_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-dhcp4o6_ipc_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-dhcp4o6_ipc_unittest.Tpo -c -o libdhcpsrv_unittests-dhcp4o6_ipc_unittest.o `test -f 'dhcp4o6_ipc_unittest.cc' || echo '$(srcdir)/'`dhcp4o6_ipc_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-dhcp4o6_ipc_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-dhcp4o6_ipc_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp4o6_ipc_unittest.cc' object='libdhcpsrv_unittests-dhcp4o6_ipc_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-dhcp4o6_ipc_unittest.o `test -f 'dhcp4o6_ipc_unittest.cc' || echo '$(srcdir)/'`dhcp4o6_ipc_unittest.cc
+
+libdhcpsrv_unittests-dhcp4o6_ipc_unittest.obj: dhcp4o6_ipc_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-dhcp4o6_ipc_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-dhcp4o6_ipc_unittest.Tpo -c -o libdhcpsrv_unittests-dhcp4o6_ipc_unittest.obj `if test -f 'dhcp4o6_ipc_unittest.cc'; then $(CYGPATH_W) 'dhcp4o6_ipc_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp4o6_ipc_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-dhcp4o6_ipc_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-dhcp4o6_ipc_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp4o6_ipc_unittest.cc' object='libdhcpsrv_unittests-dhcp4o6_ipc_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-dhcp4o6_ipc_unittest.obj `if test -f 'dhcp4o6_ipc_unittest.cc'; then $(CYGPATH_W) 'dhcp4o6_ipc_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp4o6_ipc_unittest.cc'; fi`
+
+libdhcpsrv_unittests-duid_config_parser_unittest.o: duid_config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-duid_config_parser_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-duid_config_parser_unittest.Tpo -c -o libdhcpsrv_unittests-duid_config_parser_unittest.o `test -f 'duid_config_parser_unittest.cc' || echo '$(srcdir)/'`duid_config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-duid_config_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-duid_config_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='duid_config_parser_unittest.cc' object='libdhcpsrv_unittests-duid_config_parser_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-duid_config_parser_unittest.o `test -f 'duid_config_parser_unittest.cc' || echo '$(srcdir)/'`duid_config_parser_unittest.cc
+
+libdhcpsrv_unittests-duid_config_parser_unittest.obj: duid_config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-duid_config_parser_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-duid_config_parser_unittest.Tpo -c -o libdhcpsrv_unittests-duid_config_parser_unittest.obj `if test -f 'duid_config_parser_unittest.cc'; then $(CYGPATH_W) 'duid_config_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/duid_config_parser_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-duid_config_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-duid_config_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='duid_config_parser_unittest.cc' object='libdhcpsrv_unittests-duid_config_parser_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-duid_config_parser_unittest.obj `if test -f 'duid_config_parser_unittest.cc'; then $(CYGPATH_W) 'duid_config_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/duid_config_parser_unittest.cc'; fi`
+
+libdhcpsrv_unittests-expiration_config_parser_unittest.o: expiration_config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-expiration_config_parser_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-expiration_config_parser_unittest.Tpo -c -o libdhcpsrv_unittests-expiration_config_parser_unittest.o `test -f 'expiration_config_parser_unittest.cc' || echo '$(srcdir)/'`expiration_config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-expiration_config_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-expiration_config_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='expiration_config_parser_unittest.cc' object='libdhcpsrv_unittests-expiration_config_parser_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-expiration_config_parser_unittest.o `test -f 'expiration_config_parser_unittest.cc' || echo '$(srcdir)/'`expiration_config_parser_unittest.cc
+
+libdhcpsrv_unittests-expiration_config_parser_unittest.obj: expiration_config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-expiration_config_parser_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-expiration_config_parser_unittest.Tpo -c -o libdhcpsrv_unittests-expiration_config_parser_unittest.obj `if test -f 'expiration_config_parser_unittest.cc'; then $(CYGPATH_W) 'expiration_config_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/expiration_config_parser_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-expiration_config_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-expiration_config_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='expiration_config_parser_unittest.cc' object='libdhcpsrv_unittests-expiration_config_parser_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-expiration_config_parser_unittest.obj `if test -f 'expiration_config_parser_unittest.cc'; then $(CYGPATH_W) 'expiration_config_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/expiration_config_parser_unittest.cc'; fi`
+
+libdhcpsrv_unittests-free_lease_queue_unittest.o: free_lease_queue_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-free_lease_queue_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-free_lease_queue_unittest.Tpo -c -o libdhcpsrv_unittests-free_lease_queue_unittest.o `test -f 'free_lease_queue_unittest.cc' || echo '$(srcdir)/'`free_lease_queue_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-free_lease_queue_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-free_lease_queue_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='free_lease_queue_unittest.cc' object='libdhcpsrv_unittests-free_lease_queue_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-free_lease_queue_unittest.o `test -f 'free_lease_queue_unittest.cc' || echo '$(srcdir)/'`free_lease_queue_unittest.cc
+
+libdhcpsrv_unittests-free_lease_queue_unittest.obj: free_lease_queue_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-free_lease_queue_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-free_lease_queue_unittest.Tpo -c -o libdhcpsrv_unittests-free_lease_queue_unittest.obj `if test -f 'free_lease_queue_unittest.cc'; then $(CYGPATH_W) 'free_lease_queue_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/free_lease_queue_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-free_lease_queue_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-free_lease_queue_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='free_lease_queue_unittest.cc' object='libdhcpsrv_unittests-free_lease_queue_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-free_lease_queue_unittest.obj `if test -f 'free_lease_queue_unittest.cc'; then $(CYGPATH_W) 'free_lease_queue_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/free_lease_queue_unittest.cc'; fi`
+
+libdhcpsrv_unittests-host_cache_unittest.o: host_cache_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-host_cache_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-host_cache_unittest.Tpo -c -o libdhcpsrv_unittests-host_cache_unittest.o `test -f 'host_cache_unittest.cc' || echo '$(srcdir)/'`host_cache_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-host_cache_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-host_cache_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_cache_unittest.cc' object='libdhcpsrv_unittests-host_cache_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-host_cache_unittest.o `test -f 'host_cache_unittest.cc' || echo '$(srcdir)/'`host_cache_unittest.cc
+
+libdhcpsrv_unittests-host_cache_unittest.obj: host_cache_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-host_cache_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-host_cache_unittest.Tpo -c -o libdhcpsrv_unittests-host_cache_unittest.obj `if test -f 'host_cache_unittest.cc'; then $(CYGPATH_W) 'host_cache_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_cache_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-host_cache_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-host_cache_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_cache_unittest.cc' object='libdhcpsrv_unittests-host_cache_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-host_cache_unittest.obj `if test -f 'host_cache_unittest.cc'; then $(CYGPATH_W) 'host_cache_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_cache_unittest.cc'; fi`
+
+libdhcpsrv_unittests-host_data_source_factory_unittest.o: host_data_source_factory_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-host_data_source_factory_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-host_data_source_factory_unittest.Tpo -c -o libdhcpsrv_unittests-host_data_source_factory_unittest.o `test -f 'host_data_source_factory_unittest.cc' || echo '$(srcdir)/'`host_data_source_factory_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-host_data_source_factory_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-host_data_source_factory_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_data_source_factory_unittest.cc' object='libdhcpsrv_unittests-host_data_source_factory_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-host_data_source_factory_unittest.o `test -f 'host_data_source_factory_unittest.cc' || echo '$(srcdir)/'`host_data_source_factory_unittest.cc
+
+libdhcpsrv_unittests-host_data_source_factory_unittest.obj: host_data_source_factory_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-host_data_source_factory_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-host_data_source_factory_unittest.Tpo -c -o libdhcpsrv_unittests-host_data_source_factory_unittest.obj `if test -f 'host_data_source_factory_unittest.cc'; then $(CYGPATH_W) 'host_data_source_factory_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_data_source_factory_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-host_data_source_factory_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-host_data_source_factory_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_data_source_factory_unittest.cc' object='libdhcpsrv_unittests-host_data_source_factory_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-host_data_source_factory_unittest.obj `if test -f 'host_data_source_factory_unittest.cc'; then $(CYGPATH_W) 'host_data_source_factory_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_data_source_factory_unittest.cc'; fi`
+
+libdhcpsrv_unittests-host_mgr_unittest.o: host_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-host_mgr_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-host_mgr_unittest.Tpo -c -o libdhcpsrv_unittests-host_mgr_unittest.o `test -f 'host_mgr_unittest.cc' || echo '$(srcdir)/'`host_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-host_mgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-host_mgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_mgr_unittest.cc' object='libdhcpsrv_unittests-host_mgr_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-host_mgr_unittest.o `test -f 'host_mgr_unittest.cc' || echo '$(srcdir)/'`host_mgr_unittest.cc
+
+libdhcpsrv_unittests-host_mgr_unittest.obj: host_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-host_mgr_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-host_mgr_unittest.Tpo -c -o libdhcpsrv_unittests-host_mgr_unittest.obj `if test -f 'host_mgr_unittest.cc'; then $(CYGPATH_W) 'host_mgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_mgr_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-host_mgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-host_mgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_mgr_unittest.cc' object='libdhcpsrv_unittests-host_mgr_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-host_mgr_unittest.obj `if test -f 'host_mgr_unittest.cc'; then $(CYGPATH_W) 'host_mgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_mgr_unittest.cc'; fi`
+
+libdhcpsrv_unittests-host_unittest.o: host_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-host_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-host_unittest.Tpo -c -o libdhcpsrv_unittests-host_unittest.o `test -f 'host_unittest.cc' || echo '$(srcdir)/'`host_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-host_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-host_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_unittest.cc' object='libdhcpsrv_unittests-host_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-host_unittest.o `test -f 'host_unittest.cc' || echo '$(srcdir)/'`host_unittest.cc
+
+libdhcpsrv_unittests-host_unittest.obj: host_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-host_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-host_unittest.Tpo -c -o libdhcpsrv_unittests-host_unittest.obj `if test -f 'host_unittest.cc'; then $(CYGPATH_W) 'host_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-host_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-host_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_unittest.cc' object='libdhcpsrv_unittests-host_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-host_unittest.obj `if test -f 'host_unittest.cc'; then $(CYGPATH_W) 'host_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_unittest.cc'; fi`
+
+libdhcpsrv_unittests-host_reservation_parser_unittest.o: host_reservation_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-host_reservation_parser_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-host_reservation_parser_unittest.Tpo -c -o libdhcpsrv_unittests-host_reservation_parser_unittest.o `test -f 'host_reservation_parser_unittest.cc' || echo '$(srcdir)/'`host_reservation_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-host_reservation_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-host_reservation_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_reservation_parser_unittest.cc' object='libdhcpsrv_unittests-host_reservation_parser_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-host_reservation_parser_unittest.o `test -f 'host_reservation_parser_unittest.cc' || echo '$(srcdir)/'`host_reservation_parser_unittest.cc
+
+libdhcpsrv_unittests-host_reservation_parser_unittest.obj: host_reservation_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-host_reservation_parser_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-host_reservation_parser_unittest.Tpo -c -o libdhcpsrv_unittests-host_reservation_parser_unittest.obj `if test -f 'host_reservation_parser_unittest.cc'; then $(CYGPATH_W) 'host_reservation_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_reservation_parser_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-host_reservation_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-host_reservation_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_reservation_parser_unittest.cc' object='libdhcpsrv_unittests-host_reservation_parser_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-host_reservation_parser_unittest.obj `if test -f 'host_reservation_parser_unittest.cc'; then $(CYGPATH_W) 'host_reservation_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_reservation_parser_unittest.cc'; fi`
+
+libdhcpsrv_unittests-host_reservations_list_parser_unittest.o: host_reservations_list_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-host_reservations_list_parser_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-host_reservations_list_parser_unittest.Tpo -c -o libdhcpsrv_unittests-host_reservations_list_parser_unittest.o `test -f 'host_reservations_list_parser_unittest.cc' || echo '$(srcdir)/'`host_reservations_list_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-host_reservations_list_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-host_reservations_list_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_reservations_list_parser_unittest.cc' object='libdhcpsrv_unittests-host_reservations_list_parser_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-host_reservations_list_parser_unittest.o `test -f 'host_reservations_list_parser_unittest.cc' || echo '$(srcdir)/'`host_reservations_list_parser_unittest.cc
+
+libdhcpsrv_unittests-host_reservations_list_parser_unittest.obj: host_reservations_list_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-host_reservations_list_parser_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-host_reservations_list_parser_unittest.Tpo -c -o libdhcpsrv_unittests-host_reservations_list_parser_unittest.obj `if test -f 'host_reservations_list_parser_unittest.cc'; then $(CYGPATH_W) 'host_reservations_list_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_reservations_list_parser_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-host_reservations_list_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-host_reservations_list_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='host_reservations_list_parser_unittest.cc' object='libdhcpsrv_unittests-host_reservations_list_parser_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-host_reservations_list_parser_unittest.obj `if test -f 'host_reservations_list_parser_unittest.cc'; then $(CYGPATH_W) 'host_reservations_list_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/host_reservations_list_parser_unittest.cc'; fi`
+
+libdhcpsrv_unittests-ifaces_config_parser_unittest.o: ifaces_config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-ifaces_config_parser_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-ifaces_config_parser_unittest.Tpo -c -o libdhcpsrv_unittests-ifaces_config_parser_unittest.o `test -f 'ifaces_config_parser_unittest.cc' || echo '$(srcdir)/'`ifaces_config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-ifaces_config_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-ifaces_config_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='ifaces_config_parser_unittest.cc' object='libdhcpsrv_unittests-ifaces_config_parser_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-ifaces_config_parser_unittest.o `test -f 'ifaces_config_parser_unittest.cc' || echo '$(srcdir)/'`ifaces_config_parser_unittest.cc
+
+libdhcpsrv_unittests-ifaces_config_parser_unittest.obj: ifaces_config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-ifaces_config_parser_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-ifaces_config_parser_unittest.Tpo -c -o libdhcpsrv_unittests-ifaces_config_parser_unittest.obj `if test -f 'ifaces_config_parser_unittest.cc'; then $(CYGPATH_W) 'ifaces_config_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/ifaces_config_parser_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-ifaces_config_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-ifaces_config_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='ifaces_config_parser_unittest.cc' object='libdhcpsrv_unittests-ifaces_config_parser_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-ifaces_config_parser_unittest.obj `if test -f 'ifaces_config_parser_unittest.cc'; then $(CYGPATH_W) 'ifaces_config_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/ifaces_config_parser_unittest.cc'; fi`
+
+libdhcpsrv_unittests-ip_range_unittest.o: ip_range_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-ip_range_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-ip_range_unittest.Tpo -c -o libdhcpsrv_unittests-ip_range_unittest.o `test -f 'ip_range_unittest.cc' || echo '$(srcdir)/'`ip_range_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-ip_range_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-ip_range_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='ip_range_unittest.cc' object='libdhcpsrv_unittests-ip_range_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-ip_range_unittest.o `test -f 'ip_range_unittest.cc' || echo '$(srcdir)/'`ip_range_unittest.cc
+
+libdhcpsrv_unittests-ip_range_unittest.obj: ip_range_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-ip_range_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-ip_range_unittest.Tpo -c -o libdhcpsrv_unittests-ip_range_unittest.obj `if test -f 'ip_range_unittest.cc'; then $(CYGPATH_W) 'ip_range_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/ip_range_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-ip_range_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-ip_range_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='ip_range_unittest.cc' object='libdhcpsrv_unittests-ip_range_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-ip_range_unittest.obj `if test -f 'ip_range_unittest.cc'; then $(CYGPATH_W) 'ip_range_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/ip_range_unittest.cc'; fi`
+
+libdhcpsrv_unittests-ip_range_permutation_unittest.o: ip_range_permutation_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-ip_range_permutation_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-ip_range_permutation_unittest.Tpo -c -o libdhcpsrv_unittests-ip_range_permutation_unittest.o `test -f 'ip_range_permutation_unittest.cc' || echo '$(srcdir)/'`ip_range_permutation_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-ip_range_permutation_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-ip_range_permutation_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='ip_range_permutation_unittest.cc' object='libdhcpsrv_unittests-ip_range_permutation_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-ip_range_permutation_unittest.o `test -f 'ip_range_permutation_unittest.cc' || echo '$(srcdir)/'`ip_range_permutation_unittest.cc
+
+libdhcpsrv_unittests-ip_range_permutation_unittest.obj: ip_range_permutation_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-ip_range_permutation_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-ip_range_permutation_unittest.Tpo -c -o libdhcpsrv_unittests-ip_range_permutation_unittest.obj `if test -f 'ip_range_permutation_unittest.cc'; then $(CYGPATH_W) 'ip_range_permutation_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/ip_range_permutation_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-ip_range_permutation_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-ip_range_permutation_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='ip_range_permutation_unittest.cc' object='libdhcpsrv_unittests-ip_range_permutation_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-ip_range_permutation_unittest.obj `if test -f 'ip_range_permutation_unittest.cc'; then $(CYGPATH_W) 'ip_range_permutation_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/ip_range_permutation_unittest.cc'; fi`
+
+libdhcpsrv_unittests-lease_file_loader_unittest.o: lease_file_loader_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-lease_file_loader_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-lease_file_loader_unittest.Tpo -c -o libdhcpsrv_unittests-lease_file_loader_unittest.o `test -f 'lease_file_loader_unittest.cc' || echo '$(srcdir)/'`lease_file_loader_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-lease_file_loader_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-lease_file_loader_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='lease_file_loader_unittest.cc' object='libdhcpsrv_unittests-lease_file_loader_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-lease_file_loader_unittest.o `test -f 'lease_file_loader_unittest.cc' || echo '$(srcdir)/'`lease_file_loader_unittest.cc
+
+libdhcpsrv_unittests-lease_file_loader_unittest.obj: lease_file_loader_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-lease_file_loader_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-lease_file_loader_unittest.Tpo -c -o libdhcpsrv_unittests-lease_file_loader_unittest.obj `if test -f 'lease_file_loader_unittest.cc'; then $(CYGPATH_W) 'lease_file_loader_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/lease_file_loader_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-lease_file_loader_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-lease_file_loader_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='lease_file_loader_unittest.cc' object='libdhcpsrv_unittests-lease_file_loader_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-lease_file_loader_unittest.obj `if test -f 'lease_file_loader_unittest.cc'; then $(CYGPATH_W) 'lease_file_loader_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/lease_file_loader_unittest.cc'; fi`
+
+libdhcpsrv_unittests-lease_unittest.o: lease_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-lease_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-lease_unittest.Tpo -c -o libdhcpsrv_unittests-lease_unittest.o `test -f 'lease_unittest.cc' || echo '$(srcdir)/'`lease_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-lease_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-lease_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='lease_unittest.cc' object='libdhcpsrv_unittests-lease_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-lease_unittest.o `test -f 'lease_unittest.cc' || echo '$(srcdir)/'`lease_unittest.cc
+
+libdhcpsrv_unittests-lease_unittest.obj: lease_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-lease_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-lease_unittest.Tpo -c -o libdhcpsrv_unittests-lease_unittest.obj `if test -f 'lease_unittest.cc'; then $(CYGPATH_W) 'lease_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/lease_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-lease_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-lease_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='lease_unittest.cc' object='libdhcpsrv_unittests-lease_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-lease_unittest.obj `if test -f 'lease_unittest.cc'; then $(CYGPATH_W) 'lease_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/lease_unittest.cc'; fi`
+
+libdhcpsrv_unittests-lease_mgr_factory_unittest.o: lease_mgr_factory_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-lease_mgr_factory_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-lease_mgr_factory_unittest.Tpo -c -o libdhcpsrv_unittests-lease_mgr_factory_unittest.o `test -f 'lease_mgr_factory_unittest.cc' || echo '$(srcdir)/'`lease_mgr_factory_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-lease_mgr_factory_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-lease_mgr_factory_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='lease_mgr_factory_unittest.cc' object='libdhcpsrv_unittests-lease_mgr_factory_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-lease_mgr_factory_unittest.o `test -f 'lease_mgr_factory_unittest.cc' || echo '$(srcdir)/'`lease_mgr_factory_unittest.cc
+
+libdhcpsrv_unittests-lease_mgr_factory_unittest.obj: lease_mgr_factory_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-lease_mgr_factory_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-lease_mgr_factory_unittest.Tpo -c -o libdhcpsrv_unittests-lease_mgr_factory_unittest.obj `if test -f 'lease_mgr_factory_unittest.cc'; then $(CYGPATH_W) 'lease_mgr_factory_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/lease_mgr_factory_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-lease_mgr_factory_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-lease_mgr_factory_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='lease_mgr_factory_unittest.cc' object='libdhcpsrv_unittests-lease_mgr_factory_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-lease_mgr_factory_unittest.obj `if test -f 'lease_mgr_factory_unittest.cc'; then $(CYGPATH_W) 'lease_mgr_factory_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/lease_mgr_factory_unittest.cc'; fi`
+
+libdhcpsrv_unittests-lease_mgr_unittest.o: lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-lease_mgr_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-lease_mgr_unittest.Tpo -c -o libdhcpsrv_unittests-lease_mgr_unittest.o `test -f 'lease_mgr_unittest.cc' || echo '$(srcdir)/'`lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-lease_mgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-lease_mgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='lease_mgr_unittest.cc' object='libdhcpsrv_unittests-lease_mgr_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-lease_mgr_unittest.o `test -f 'lease_mgr_unittest.cc' || echo '$(srcdir)/'`lease_mgr_unittest.cc
+
+libdhcpsrv_unittests-lease_mgr_unittest.obj: lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-lease_mgr_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-lease_mgr_unittest.Tpo -c -o libdhcpsrv_unittests-lease_mgr_unittest.obj `if test -f 'lease_mgr_unittest.cc'; then $(CYGPATH_W) 'lease_mgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/lease_mgr_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-lease_mgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-lease_mgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='lease_mgr_unittest.cc' object='libdhcpsrv_unittests-lease_mgr_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-lease_mgr_unittest.obj `if test -f 'lease_mgr_unittest.cc'; then $(CYGPATH_W) 'lease_mgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/lease_mgr_unittest.cc'; fi`
+
+libdhcpsrv_unittests-generic_lease_mgr_unittest.o: generic_lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-generic_lease_mgr_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-generic_lease_mgr_unittest.Tpo -c -o libdhcpsrv_unittests-generic_lease_mgr_unittest.o `test -f 'generic_lease_mgr_unittest.cc' || echo '$(srcdir)/'`generic_lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-generic_lease_mgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-generic_lease_mgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='generic_lease_mgr_unittest.cc' object='libdhcpsrv_unittests-generic_lease_mgr_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-generic_lease_mgr_unittest.o `test -f 'generic_lease_mgr_unittest.cc' || echo '$(srcdir)/'`generic_lease_mgr_unittest.cc
+
+libdhcpsrv_unittests-generic_lease_mgr_unittest.obj: generic_lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-generic_lease_mgr_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-generic_lease_mgr_unittest.Tpo -c -o libdhcpsrv_unittests-generic_lease_mgr_unittest.obj `if test -f 'generic_lease_mgr_unittest.cc'; then $(CYGPATH_W) 'generic_lease_mgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/generic_lease_mgr_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-generic_lease_mgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-generic_lease_mgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='generic_lease_mgr_unittest.cc' object='libdhcpsrv_unittests-generic_lease_mgr_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-generic_lease_mgr_unittest.obj `if test -f 'generic_lease_mgr_unittest.cc'; then $(CYGPATH_W) 'generic_lease_mgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/generic_lease_mgr_unittest.cc'; fi`
+
+libdhcpsrv_unittests-memfile_lease_limits_unittest.o: memfile_lease_limits_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-memfile_lease_limits_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-memfile_lease_limits_unittest.Tpo -c -o libdhcpsrv_unittests-memfile_lease_limits_unittest.o `test -f 'memfile_lease_limits_unittest.cc' || echo '$(srcdir)/'`memfile_lease_limits_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-memfile_lease_limits_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-memfile_lease_limits_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='memfile_lease_limits_unittest.cc' object='libdhcpsrv_unittests-memfile_lease_limits_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-memfile_lease_limits_unittest.o `test -f 'memfile_lease_limits_unittest.cc' || echo '$(srcdir)/'`memfile_lease_limits_unittest.cc
+
+libdhcpsrv_unittests-memfile_lease_limits_unittest.obj: memfile_lease_limits_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-memfile_lease_limits_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-memfile_lease_limits_unittest.Tpo -c -o libdhcpsrv_unittests-memfile_lease_limits_unittest.obj `if test -f 'memfile_lease_limits_unittest.cc'; then $(CYGPATH_W) 'memfile_lease_limits_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/memfile_lease_limits_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-memfile_lease_limits_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-memfile_lease_limits_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='memfile_lease_limits_unittest.cc' object='libdhcpsrv_unittests-memfile_lease_limits_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-memfile_lease_limits_unittest.obj `if test -f 'memfile_lease_limits_unittest.cc'; then $(CYGPATH_W) 'memfile_lease_limits_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/memfile_lease_limits_unittest.cc'; fi`
+
+libdhcpsrv_unittests-memfile_lease_mgr_unittest.o: memfile_lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-memfile_lease_mgr_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-memfile_lease_mgr_unittest.Tpo -c -o libdhcpsrv_unittests-memfile_lease_mgr_unittest.o `test -f 'memfile_lease_mgr_unittest.cc' || echo '$(srcdir)/'`memfile_lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-memfile_lease_mgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-memfile_lease_mgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='memfile_lease_mgr_unittest.cc' object='libdhcpsrv_unittests-memfile_lease_mgr_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-memfile_lease_mgr_unittest.o `test -f 'memfile_lease_mgr_unittest.cc' || echo '$(srcdir)/'`memfile_lease_mgr_unittest.cc
+
+libdhcpsrv_unittests-memfile_lease_mgr_unittest.obj: memfile_lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-memfile_lease_mgr_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-memfile_lease_mgr_unittest.Tpo -c -o libdhcpsrv_unittests-memfile_lease_mgr_unittest.obj `if test -f 'memfile_lease_mgr_unittest.cc'; then $(CYGPATH_W) 'memfile_lease_mgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/memfile_lease_mgr_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-memfile_lease_mgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-memfile_lease_mgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='memfile_lease_mgr_unittest.cc' object='libdhcpsrv_unittests-memfile_lease_mgr_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-memfile_lease_mgr_unittest.obj `if test -f 'memfile_lease_mgr_unittest.cc'; then $(CYGPATH_W) 'memfile_lease_mgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/memfile_lease_mgr_unittest.cc'; fi`
+
+libdhcpsrv_unittests-multi_threading_config_parser_unittest.o: multi_threading_config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-multi_threading_config_parser_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-multi_threading_config_parser_unittest.Tpo -c -o libdhcpsrv_unittests-multi_threading_config_parser_unittest.o `test -f 'multi_threading_config_parser_unittest.cc' || echo '$(srcdir)/'`multi_threading_config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-multi_threading_config_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-multi_threading_config_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='multi_threading_config_parser_unittest.cc' object='libdhcpsrv_unittests-multi_threading_config_parser_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-multi_threading_config_parser_unittest.o `test -f 'multi_threading_config_parser_unittest.cc' || echo '$(srcdir)/'`multi_threading_config_parser_unittest.cc
+
+libdhcpsrv_unittests-multi_threading_config_parser_unittest.obj: multi_threading_config_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-multi_threading_config_parser_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-multi_threading_config_parser_unittest.Tpo -c -o libdhcpsrv_unittests-multi_threading_config_parser_unittest.obj `if test -f 'multi_threading_config_parser_unittest.cc'; then $(CYGPATH_W) 'multi_threading_config_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/multi_threading_config_parser_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-multi_threading_config_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-multi_threading_config_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='multi_threading_config_parser_unittest.cc' object='libdhcpsrv_unittests-multi_threading_config_parser_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-multi_threading_config_parser_unittest.obj `if test -f 'multi_threading_config_parser_unittest.cc'; then $(CYGPATH_W) 'multi_threading_config_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/multi_threading_config_parser_unittest.cc'; fi`
+
+libdhcpsrv_unittests-dhcp_parsers_unittest.o: dhcp_parsers_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-dhcp_parsers_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-dhcp_parsers_unittest.Tpo -c -o libdhcpsrv_unittests-dhcp_parsers_unittest.o `test -f 'dhcp_parsers_unittest.cc' || echo '$(srcdir)/'`dhcp_parsers_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-dhcp_parsers_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-dhcp_parsers_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp_parsers_unittest.cc' object='libdhcpsrv_unittests-dhcp_parsers_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-dhcp_parsers_unittest.o `test -f 'dhcp_parsers_unittest.cc' || echo '$(srcdir)/'`dhcp_parsers_unittest.cc
+
+libdhcpsrv_unittests-dhcp_parsers_unittest.obj: dhcp_parsers_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-dhcp_parsers_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-dhcp_parsers_unittest.Tpo -c -o libdhcpsrv_unittests-dhcp_parsers_unittest.obj `if test -f 'dhcp_parsers_unittest.cc'; then $(CYGPATH_W) 'dhcp_parsers_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp_parsers_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-dhcp_parsers_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-dhcp_parsers_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='dhcp_parsers_unittest.cc' object='libdhcpsrv_unittests-dhcp_parsers_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-dhcp_parsers_unittest.obj `if test -f 'dhcp_parsers_unittest.cc'; then $(CYGPATH_W) 'dhcp_parsers_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/dhcp_parsers_unittest.cc'; fi`
+
+libdhcpsrv_unittests-ncr_generator_unittest.o: ncr_generator_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-ncr_generator_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-ncr_generator_unittest.Tpo -c -o libdhcpsrv_unittests-ncr_generator_unittest.o `test -f 'ncr_generator_unittest.cc' || echo '$(srcdir)/'`ncr_generator_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-ncr_generator_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-ncr_generator_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='ncr_generator_unittest.cc' object='libdhcpsrv_unittests-ncr_generator_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-ncr_generator_unittest.o `test -f 'ncr_generator_unittest.cc' || echo '$(srcdir)/'`ncr_generator_unittest.cc
+
+libdhcpsrv_unittests-ncr_generator_unittest.obj: ncr_generator_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-ncr_generator_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-ncr_generator_unittest.Tpo -c -o libdhcpsrv_unittests-ncr_generator_unittest.obj `if test -f 'ncr_generator_unittest.cc'; then $(CYGPATH_W) 'ncr_generator_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/ncr_generator_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-ncr_generator_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-ncr_generator_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='ncr_generator_unittest.cc' object='libdhcpsrv_unittests-ncr_generator_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-ncr_generator_unittest.obj `if test -f 'ncr_generator_unittest.cc'; then $(CYGPATH_W) 'ncr_generator_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/ncr_generator_unittest.cc'; fi`
+
+libdhcpsrv_unittests-mysql_lease_mgr_unittest.o: mysql_lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-mysql_lease_mgr_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-mysql_lease_mgr_unittest.Tpo -c -o libdhcpsrv_unittests-mysql_lease_mgr_unittest.o `test -f 'mysql_lease_mgr_unittest.cc' || echo '$(srcdir)/'`mysql_lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-mysql_lease_mgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-mysql_lease_mgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='mysql_lease_mgr_unittest.cc' object='libdhcpsrv_unittests-mysql_lease_mgr_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-mysql_lease_mgr_unittest.o `test -f 'mysql_lease_mgr_unittest.cc' || echo '$(srcdir)/'`mysql_lease_mgr_unittest.cc
+
+libdhcpsrv_unittests-mysql_lease_mgr_unittest.obj: mysql_lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-mysql_lease_mgr_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-mysql_lease_mgr_unittest.Tpo -c -o libdhcpsrv_unittests-mysql_lease_mgr_unittest.obj `if test -f 'mysql_lease_mgr_unittest.cc'; then $(CYGPATH_W) 'mysql_lease_mgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/mysql_lease_mgr_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-mysql_lease_mgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-mysql_lease_mgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='mysql_lease_mgr_unittest.cc' object='libdhcpsrv_unittests-mysql_lease_mgr_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-mysql_lease_mgr_unittest.obj `if test -f 'mysql_lease_mgr_unittest.cc'; then $(CYGPATH_W) 'mysql_lease_mgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/mysql_lease_mgr_unittest.cc'; fi`
+
+libdhcpsrv_unittests-mysql_host_data_source_unittest.o: mysql_host_data_source_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-mysql_host_data_source_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-mysql_host_data_source_unittest.Tpo -c -o libdhcpsrv_unittests-mysql_host_data_source_unittest.o `test -f 'mysql_host_data_source_unittest.cc' || echo '$(srcdir)/'`mysql_host_data_source_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-mysql_host_data_source_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-mysql_host_data_source_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='mysql_host_data_source_unittest.cc' object='libdhcpsrv_unittests-mysql_host_data_source_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-mysql_host_data_source_unittest.o `test -f 'mysql_host_data_source_unittest.cc' || echo '$(srcdir)/'`mysql_host_data_source_unittest.cc
+
+libdhcpsrv_unittests-mysql_host_data_source_unittest.obj: mysql_host_data_source_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-mysql_host_data_source_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-mysql_host_data_source_unittest.Tpo -c -o libdhcpsrv_unittests-mysql_host_data_source_unittest.obj `if test -f 'mysql_host_data_source_unittest.cc'; then $(CYGPATH_W) 'mysql_host_data_source_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/mysql_host_data_source_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-mysql_host_data_source_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-mysql_host_data_source_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='mysql_host_data_source_unittest.cc' object='libdhcpsrv_unittests-mysql_host_data_source_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-mysql_host_data_source_unittest.obj `if test -f 'mysql_host_data_source_unittest.cc'; then $(CYGPATH_W) 'mysql_host_data_source_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/mysql_host_data_source_unittest.cc'; fi`
+
+libdhcpsrv_unittests-pgsql_lease_mgr_unittest.o: pgsql_lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-pgsql_lease_mgr_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-pgsql_lease_mgr_unittest.Tpo -c -o libdhcpsrv_unittests-pgsql_lease_mgr_unittest.o `test -f 'pgsql_lease_mgr_unittest.cc' || echo '$(srcdir)/'`pgsql_lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-pgsql_lease_mgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-pgsql_lease_mgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='pgsql_lease_mgr_unittest.cc' object='libdhcpsrv_unittests-pgsql_lease_mgr_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-pgsql_lease_mgr_unittest.o `test -f 'pgsql_lease_mgr_unittest.cc' || echo '$(srcdir)/'`pgsql_lease_mgr_unittest.cc
+
+libdhcpsrv_unittests-pgsql_lease_mgr_unittest.obj: pgsql_lease_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-pgsql_lease_mgr_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-pgsql_lease_mgr_unittest.Tpo -c -o libdhcpsrv_unittests-pgsql_lease_mgr_unittest.obj `if test -f 'pgsql_lease_mgr_unittest.cc'; then $(CYGPATH_W) 'pgsql_lease_mgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/pgsql_lease_mgr_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-pgsql_lease_mgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-pgsql_lease_mgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='pgsql_lease_mgr_unittest.cc' object='libdhcpsrv_unittests-pgsql_lease_mgr_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-pgsql_lease_mgr_unittest.obj `if test -f 'pgsql_lease_mgr_unittest.cc'; then $(CYGPATH_W) 'pgsql_lease_mgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/pgsql_lease_mgr_unittest.cc'; fi`
+
+libdhcpsrv_unittests-pgsql_host_data_source_unittest.o: pgsql_host_data_source_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-pgsql_host_data_source_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-pgsql_host_data_source_unittest.Tpo -c -o libdhcpsrv_unittests-pgsql_host_data_source_unittest.o `test -f 'pgsql_host_data_source_unittest.cc' || echo '$(srcdir)/'`pgsql_host_data_source_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-pgsql_host_data_source_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-pgsql_host_data_source_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='pgsql_host_data_source_unittest.cc' object='libdhcpsrv_unittests-pgsql_host_data_source_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-pgsql_host_data_source_unittest.o `test -f 'pgsql_host_data_source_unittest.cc' || echo '$(srcdir)/'`pgsql_host_data_source_unittest.cc
+
+libdhcpsrv_unittests-pgsql_host_data_source_unittest.obj: pgsql_host_data_source_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-pgsql_host_data_source_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-pgsql_host_data_source_unittest.Tpo -c -o libdhcpsrv_unittests-pgsql_host_data_source_unittest.obj `if test -f 'pgsql_host_data_source_unittest.cc'; then $(CYGPATH_W) 'pgsql_host_data_source_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/pgsql_host_data_source_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-pgsql_host_data_source_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-pgsql_host_data_source_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='pgsql_host_data_source_unittest.cc' object='libdhcpsrv_unittests-pgsql_host_data_source_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-pgsql_host_data_source_unittest.obj `if test -f 'pgsql_host_data_source_unittest.cc'; then $(CYGPATH_W) 'pgsql_host_data_source_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/pgsql_host_data_source_unittest.cc'; fi`
+
+libdhcpsrv_unittests-pool_unittest.o: pool_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-pool_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-pool_unittest.Tpo -c -o libdhcpsrv_unittests-pool_unittest.o `test -f 'pool_unittest.cc' || echo '$(srcdir)/'`pool_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-pool_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-pool_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='pool_unittest.cc' object='libdhcpsrv_unittests-pool_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-pool_unittest.o `test -f 'pool_unittest.cc' || echo '$(srcdir)/'`pool_unittest.cc
+
+libdhcpsrv_unittests-pool_unittest.obj: pool_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-pool_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-pool_unittest.Tpo -c -o libdhcpsrv_unittests-pool_unittest.obj `if test -f 'pool_unittest.cc'; then $(CYGPATH_W) 'pool_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/pool_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-pool_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-pool_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='pool_unittest.cc' object='libdhcpsrv_unittests-pool_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-pool_unittest.obj `if test -f 'pool_unittest.cc'; then $(CYGPATH_W) 'pool_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/pool_unittest.cc'; fi`
+
+libdhcpsrv_unittests-resource_handler_unittest.o: resource_handler_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-resource_handler_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-resource_handler_unittest.Tpo -c -o libdhcpsrv_unittests-resource_handler_unittest.o `test -f 'resource_handler_unittest.cc' || echo '$(srcdir)/'`resource_handler_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-resource_handler_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-resource_handler_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='resource_handler_unittest.cc' object='libdhcpsrv_unittests-resource_handler_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-resource_handler_unittest.o `test -f 'resource_handler_unittest.cc' || echo '$(srcdir)/'`resource_handler_unittest.cc
+
+libdhcpsrv_unittests-resource_handler_unittest.obj: resource_handler_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-resource_handler_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-resource_handler_unittest.Tpo -c -o libdhcpsrv_unittests-resource_handler_unittest.obj `if test -f 'resource_handler_unittest.cc'; then $(CYGPATH_W) 'resource_handler_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/resource_handler_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-resource_handler_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-resource_handler_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='resource_handler_unittest.cc' object='libdhcpsrv_unittests-resource_handler_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-resource_handler_unittest.obj `if test -f 'resource_handler_unittest.cc'; then $(CYGPATH_W) 'resource_handler_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/resource_handler_unittest.cc'; fi`
+
+libdhcpsrv_unittests-sanity_checks_unittest.o: sanity_checks_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-sanity_checks_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-sanity_checks_unittest.Tpo -c -o libdhcpsrv_unittests-sanity_checks_unittest.o `test -f 'sanity_checks_unittest.cc' || echo '$(srcdir)/'`sanity_checks_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-sanity_checks_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-sanity_checks_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='sanity_checks_unittest.cc' object='libdhcpsrv_unittests-sanity_checks_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-sanity_checks_unittest.o `test -f 'sanity_checks_unittest.cc' || echo '$(srcdir)/'`sanity_checks_unittest.cc
+
+libdhcpsrv_unittests-sanity_checks_unittest.obj: sanity_checks_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-sanity_checks_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-sanity_checks_unittest.Tpo -c -o libdhcpsrv_unittests-sanity_checks_unittest.obj `if test -f 'sanity_checks_unittest.cc'; then $(CYGPATH_W) 'sanity_checks_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/sanity_checks_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-sanity_checks_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-sanity_checks_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='sanity_checks_unittest.cc' object='libdhcpsrv_unittests-sanity_checks_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-sanity_checks_unittest.obj `if test -f 'sanity_checks_unittest.cc'; then $(CYGPATH_W) 'sanity_checks_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/sanity_checks_unittest.cc'; fi`
+
+libdhcpsrv_unittests-shared_network_parser_unittest.o: shared_network_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-shared_network_parser_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-shared_network_parser_unittest.Tpo -c -o libdhcpsrv_unittests-shared_network_parser_unittest.o `test -f 'shared_network_parser_unittest.cc' || echo '$(srcdir)/'`shared_network_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-shared_network_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-shared_network_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='shared_network_parser_unittest.cc' object='libdhcpsrv_unittests-shared_network_parser_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-shared_network_parser_unittest.o `test -f 'shared_network_parser_unittest.cc' || echo '$(srcdir)/'`shared_network_parser_unittest.cc
+
+libdhcpsrv_unittests-shared_network_parser_unittest.obj: shared_network_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-shared_network_parser_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-shared_network_parser_unittest.Tpo -c -o libdhcpsrv_unittests-shared_network_parser_unittest.obj `if test -f 'shared_network_parser_unittest.cc'; then $(CYGPATH_W) 'shared_network_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/shared_network_parser_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-shared_network_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-shared_network_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='shared_network_parser_unittest.cc' object='libdhcpsrv_unittests-shared_network_parser_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-shared_network_parser_unittest.obj `if test -f 'shared_network_parser_unittest.cc'; then $(CYGPATH_W) 'shared_network_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/shared_network_parser_unittest.cc'; fi`
+
+libdhcpsrv_unittests-shared_network_unittest.o: shared_network_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-shared_network_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-shared_network_unittest.Tpo -c -o libdhcpsrv_unittests-shared_network_unittest.o `test -f 'shared_network_unittest.cc' || echo '$(srcdir)/'`shared_network_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-shared_network_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-shared_network_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='shared_network_unittest.cc' object='libdhcpsrv_unittests-shared_network_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-shared_network_unittest.o `test -f 'shared_network_unittest.cc' || echo '$(srcdir)/'`shared_network_unittest.cc
+
+libdhcpsrv_unittests-shared_network_unittest.obj: shared_network_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-shared_network_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-shared_network_unittest.Tpo -c -o libdhcpsrv_unittests-shared_network_unittest.obj `if test -f 'shared_network_unittest.cc'; then $(CYGPATH_W) 'shared_network_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/shared_network_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-shared_network_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-shared_network_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='shared_network_unittest.cc' object='libdhcpsrv_unittests-shared_network_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-shared_network_unittest.obj `if test -f 'shared_network_unittest.cc'; then $(CYGPATH_W) 'shared_network_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/shared_network_unittest.cc'; fi`
+
+libdhcpsrv_unittests-shared_networks_list_parser_unittest.o: shared_networks_list_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-shared_networks_list_parser_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-shared_networks_list_parser_unittest.Tpo -c -o libdhcpsrv_unittests-shared_networks_list_parser_unittest.o `test -f 'shared_networks_list_parser_unittest.cc' || echo '$(srcdir)/'`shared_networks_list_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-shared_networks_list_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-shared_networks_list_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='shared_networks_list_parser_unittest.cc' object='libdhcpsrv_unittests-shared_networks_list_parser_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-shared_networks_list_parser_unittest.o `test -f 'shared_networks_list_parser_unittest.cc' || echo '$(srcdir)/'`shared_networks_list_parser_unittest.cc
+
+libdhcpsrv_unittests-shared_networks_list_parser_unittest.obj: shared_networks_list_parser_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-shared_networks_list_parser_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-shared_networks_list_parser_unittest.Tpo -c -o libdhcpsrv_unittests-shared_networks_list_parser_unittest.obj `if test -f 'shared_networks_list_parser_unittest.cc'; then $(CYGPATH_W) 'shared_networks_list_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/shared_networks_list_parser_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-shared_networks_list_parser_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-shared_networks_list_parser_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='shared_networks_list_parser_unittest.cc' object='libdhcpsrv_unittests-shared_networks_list_parser_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-shared_networks_list_parser_unittest.obj `if test -f 'shared_networks_list_parser_unittest.cc'; then $(CYGPATH_W) 'shared_networks_list_parser_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/shared_networks_list_parser_unittest.cc'; fi`
+
+libdhcpsrv_unittests-srv_config_unittest.o: srv_config_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-srv_config_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-srv_config_unittest.Tpo -c -o libdhcpsrv_unittests-srv_config_unittest.o `test -f 'srv_config_unittest.cc' || echo '$(srcdir)/'`srv_config_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-srv_config_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-srv_config_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='srv_config_unittest.cc' object='libdhcpsrv_unittests-srv_config_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-srv_config_unittest.o `test -f 'srv_config_unittest.cc' || echo '$(srcdir)/'`srv_config_unittest.cc
+
+libdhcpsrv_unittests-srv_config_unittest.obj: srv_config_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-srv_config_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-srv_config_unittest.Tpo -c -o libdhcpsrv_unittests-srv_config_unittest.obj `if test -f 'srv_config_unittest.cc'; then $(CYGPATH_W) 'srv_config_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/srv_config_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-srv_config_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-srv_config_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='srv_config_unittest.cc' object='libdhcpsrv_unittests-srv_config_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-srv_config_unittest.obj `if test -f 'srv_config_unittest.cc'; then $(CYGPATH_W) 'srv_config_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/srv_config_unittest.cc'; fi`
+
+libdhcpsrv_unittests-subnet_unittest.o: subnet_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-subnet_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-subnet_unittest.Tpo -c -o libdhcpsrv_unittests-subnet_unittest.o `test -f 'subnet_unittest.cc' || echo '$(srcdir)/'`subnet_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-subnet_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-subnet_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='subnet_unittest.cc' object='libdhcpsrv_unittests-subnet_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-subnet_unittest.o `test -f 'subnet_unittest.cc' || echo '$(srcdir)/'`subnet_unittest.cc
+
+libdhcpsrv_unittests-subnet_unittest.obj: subnet_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-subnet_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-subnet_unittest.Tpo -c -o libdhcpsrv_unittests-subnet_unittest.obj `if test -f 'subnet_unittest.cc'; then $(CYGPATH_W) 'subnet_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/subnet_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-subnet_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-subnet_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='subnet_unittest.cc' object='libdhcpsrv_unittests-subnet_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-subnet_unittest.obj `if test -f 'subnet_unittest.cc'; then $(CYGPATH_W) 'subnet_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/subnet_unittest.cc'; fi`
+
+libdhcpsrv_unittests-test_get_callout_handle.o: test_get_callout_handle.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-test_get_callout_handle.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-test_get_callout_handle.Tpo -c -o libdhcpsrv_unittests-test_get_callout_handle.o `test -f 'test_get_callout_handle.cc' || echo '$(srcdir)/'`test_get_callout_handle.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-test_get_callout_handle.Tpo $(DEPDIR)/libdhcpsrv_unittests-test_get_callout_handle.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='test_get_callout_handle.cc' object='libdhcpsrv_unittests-test_get_callout_handle.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-test_get_callout_handle.o `test -f 'test_get_callout_handle.cc' || echo '$(srcdir)/'`test_get_callout_handle.cc
+
+libdhcpsrv_unittests-test_get_callout_handle.obj: test_get_callout_handle.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-test_get_callout_handle.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-test_get_callout_handle.Tpo -c -o libdhcpsrv_unittests-test_get_callout_handle.obj `if test -f 'test_get_callout_handle.cc'; then $(CYGPATH_W) 'test_get_callout_handle.cc'; else $(CYGPATH_W) '$(srcdir)/test_get_callout_handle.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-test_get_callout_handle.Tpo $(DEPDIR)/libdhcpsrv_unittests-test_get_callout_handle.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='test_get_callout_handle.cc' object='libdhcpsrv_unittests-test_get_callout_handle.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-test_get_callout_handle.obj `if test -f 'test_get_callout_handle.cc'; then $(CYGPATH_W) 'test_get_callout_handle.cc'; else $(CYGPATH_W) '$(srcdir)/test_get_callout_handle.cc'; fi`
+
+libdhcpsrv_unittests-timer_mgr_unittest.o: timer_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-timer_mgr_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-timer_mgr_unittest.Tpo -c -o libdhcpsrv_unittests-timer_mgr_unittest.o `test -f 'timer_mgr_unittest.cc' || echo '$(srcdir)/'`timer_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-timer_mgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-timer_mgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='timer_mgr_unittest.cc' object='libdhcpsrv_unittests-timer_mgr_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-timer_mgr_unittest.o `test -f 'timer_mgr_unittest.cc' || echo '$(srcdir)/'`timer_mgr_unittest.cc
+
+libdhcpsrv_unittests-timer_mgr_unittest.obj: timer_mgr_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-timer_mgr_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-timer_mgr_unittest.Tpo -c -o libdhcpsrv_unittests-timer_mgr_unittest.obj `if test -f 'timer_mgr_unittest.cc'; then $(CYGPATH_W) 'timer_mgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/timer_mgr_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-timer_mgr_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-timer_mgr_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='timer_mgr_unittest.cc' object='libdhcpsrv_unittests-timer_mgr_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-timer_mgr_unittest.obj `if test -f 'timer_mgr_unittest.cc'; then $(CYGPATH_W) 'timer_mgr_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/timer_mgr_unittest.cc'; fi`
+
+libdhcpsrv_unittests-network_state_unittest.o: network_state_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-network_state_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-network_state_unittest.Tpo -c -o libdhcpsrv_unittests-network_state_unittest.o `test -f 'network_state_unittest.cc' || echo '$(srcdir)/'`network_state_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-network_state_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-network_state_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='network_state_unittest.cc' object='libdhcpsrv_unittests-network_state_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-network_state_unittest.o `test -f 'network_state_unittest.cc' || echo '$(srcdir)/'`network_state_unittest.cc
+
+libdhcpsrv_unittests-network_state_unittest.obj: network_state_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-network_state_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-network_state_unittest.Tpo -c -o libdhcpsrv_unittests-network_state_unittest.obj `if test -f 'network_state_unittest.cc'; then $(CYGPATH_W) 'network_state_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/network_state_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-network_state_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-network_state_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='network_state_unittest.cc' object='libdhcpsrv_unittests-network_state_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-network_state_unittest.obj `if test -f 'network_state_unittest.cc'; then $(CYGPATH_W) 'network_state_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/network_state_unittest.cc'; fi`
+
+libdhcpsrv_unittests-network_unittest.o: network_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-network_unittest.o -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-network_unittest.Tpo -c -o libdhcpsrv_unittests-network_unittest.o `test -f 'network_unittest.cc' || echo '$(srcdir)/'`network_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-network_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-network_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='network_unittest.cc' object='libdhcpsrv_unittests-network_unittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-network_unittest.o `test -f 'network_unittest.cc' || echo '$(srcdir)/'`network_unittest.cc
+
+libdhcpsrv_unittests-network_unittest.obj: network_unittest.cc
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -MT libdhcpsrv_unittests-network_unittest.obj -MD -MP -MF $(DEPDIR)/libdhcpsrv_unittests-network_unittest.Tpo -c -o libdhcpsrv_unittests-network_unittest.obj `if test -f 'network_unittest.cc'; then $(CYGPATH_W) 'network_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/network_unittest.cc'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdhcpsrv_unittests-network_unittest.Tpo $(DEPDIR)/libdhcpsrv_unittests-network_unittest.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='network_unittest.cc' object='libdhcpsrv_unittests-network_unittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdhcpsrv_unittests_CPPFLAGS) $(CPPFLAGS) $(libdhcpsrv_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libdhcpsrv_unittests-network_unittest.obj `if test -f 'network_unittest.cc'; then $(CYGPATH_W) 'network_unittest.cc'; else $(CYGPATH_W) '$(srcdir)/network_unittest.cc'; fi`
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+
+# This directory's subdirectories are mostly independent; you can cd
+# into them and run 'make' without going through this Makefile.
+# To change the values of 'make' variables: instead of editing Makefiles,
+# (1) if the variable is set in 'config.status', edit 'config.status'
+# (which will cause the Makefiles to be regenerated when you run 'make');
+# (2) otherwise, pass the desired values on the 'make' command line.
+$(am__recursive_targets):
+ @fail=; \
+ if $(am__make_keepgoing); then \
+ failcom='fail=yes'; \
+ else \
+ failcom='exit 1'; \
+ fi; \
+ dot_seen=no; \
+ target=`echo $@ | sed s/-recursive//`; \
+ case "$@" in \
+ distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \
+ *) list='$(SUBDIRS)' ;; \
+ esac; \
+ for subdir in $$list; do \
+ echo "Making $$target in $$subdir"; \
+ if test "$$subdir" = "."; then \
+ dot_seen=yes; \
+ local_target="$$target-am"; \
+ else \
+ local_target="$$target"; \
+ fi; \
+ ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \
+ || eval $$failcom; \
+ done; \
+ if test "$$dot_seen" = "no"; then \
+ $(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \
+ fi; test -z "$$fail"
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-recursive
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \
+ include_option=--etags-include; \
+ empty_fix=.; \
+ else \
+ include_option=--include; \
+ empty_fix=; \
+ fi; \
+ list='$(SUBDIRS)'; for subdir in $$list; do \
+ if test "$$subdir" = .; then :; else \
+ test ! -f $$subdir/TAGS || \
+ set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \
+ fi; \
+ done; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-recursive
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-recursive
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+check-TESTS: $(TESTS)
+ @failed=0; all=0; xfail=0; xpass=0; skip=0; \
+ srcdir=$(srcdir); export srcdir; \
+ list=' $(TESTS) '; \
+ $(am__tty_colors); \
+ if test -n "$$list"; then \
+ for tst in $$list; do \
+ if test -f ./$$tst; then dir=./; \
+ elif test -f $$tst; then dir=; \
+ else dir="$(srcdir)/"; fi; \
+ if $(TESTS_ENVIRONMENT) $${dir}$$tst $(AM_TESTS_FD_REDIRECT); then \
+ all=`expr $$all + 1`; \
+ case " $(XFAIL_TESTS) " in \
+ *[\ \ ]$$tst[\ \ ]*) \
+ xpass=`expr $$xpass + 1`; \
+ failed=`expr $$failed + 1`; \
+ col=$$red; res=XPASS; \
+ ;; \
+ *) \
+ col=$$grn; res=PASS; \
+ ;; \
+ esac; \
+ elif test $$? -ne 77; then \
+ all=`expr $$all + 1`; \
+ case " $(XFAIL_TESTS) " in \
+ *[\ \ ]$$tst[\ \ ]*) \
+ xfail=`expr $$xfail + 1`; \
+ col=$$lgn; res=XFAIL; \
+ ;; \
+ *) \
+ failed=`expr $$failed + 1`; \
+ col=$$red; res=FAIL; \
+ ;; \
+ esac; \
+ else \
+ skip=`expr $$skip + 1`; \
+ col=$$blu; res=SKIP; \
+ fi; \
+ echo "$${col}$$res$${std}: $$tst"; \
+ done; \
+ if test "$$all" -eq 1; then \
+ tests="test"; \
+ All=""; \
+ else \
+ tests="tests"; \
+ All="All "; \
+ fi; \
+ if test "$$failed" -eq 0; then \
+ if test "$$xfail" -eq 0; then \
+ banner="$$All$$all $$tests passed"; \
+ else \
+ if test "$$xfail" -eq 1; then failures=failure; else failures=failures; fi; \
+ banner="$$All$$all $$tests behaved as expected ($$xfail expected $$failures)"; \
+ fi; \
+ else \
+ if test "$$xpass" -eq 0; then \
+ banner="$$failed of $$all $$tests failed"; \
+ else \
+ if test "$$xpass" -eq 1; then passes=pass; else passes=passes; fi; \
+ banner="$$failed of $$all $$tests did not behave as expected ($$xpass unexpected $$passes)"; \
+ fi; \
+ fi; \
+ dashes="$$banner"; \
+ skipped=""; \
+ if test "$$skip" -ne 0; then \
+ if test "$$skip" -eq 1; then \
+ skipped="($$skip test was not run)"; \
+ else \
+ skipped="($$skip tests were not run)"; \
+ fi; \
+ test `echo "$$skipped" | wc -c` -le `echo "$$banner" | wc -c` || \
+ dashes="$$skipped"; \
+ fi; \
+ report=""; \
+ if test "$$failed" -ne 0 && test -n "$(PACKAGE_BUGREPORT)"; then \
+ report="Please report to $(PACKAGE_BUGREPORT)"; \
+ test `echo "$$report" | wc -c` -le `echo "$$banner" | wc -c` || \
+ dashes="$$report"; \
+ fi; \
+ dashes=`echo "$$dashes" | sed s/./=/g`; \
+ if test "$$failed" -eq 0; then \
+ col="$$grn"; \
+ else \
+ col="$$red"; \
+ fi; \
+ echo "$${col}$$dashes$${std}"; \
+ echo "$${col}$$banner$${std}"; \
+ test -z "$$skipped" || echo "$${col}$$skipped$${std}"; \
+ test -z "$$report" || echo "$${col}$$report$${std}"; \
+ echo "$${col}$$dashes$${std}"; \
+ test "$$failed" -eq 0; \
+ else :; fi
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+ @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \
+ if test "$$subdir" = .; then :; else \
+ $(am__make_dryrun) \
+ || test -d "$(distdir)/$$subdir" \
+ || $(MKDIR_P) "$(distdir)/$$subdir" \
+ || exit 1; \
+ dir1=$$subdir; dir2="$(distdir)/$$subdir"; \
+ $(am__relativize); \
+ new_distdir=$$reldir; \
+ dir1=$$subdir; dir2="$(top_distdir)"; \
+ $(am__relativize); \
+ new_top_distdir=$$reldir; \
+ echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \
+ echo " am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \
+ ($(am__cd) $$subdir && \
+ $(MAKE) $(AM_MAKEFLAGS) \
+ top_distdir="$$new_top_distdir" \
+ distdir="$$new_distdir" \
+ am__remove_distdir=: \
+ am__skip_length_check=: \
+ am__skip_mode_fix=: \
+ distdir) \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-TESTS
+check: check-recursive
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES)
+installdirs: installdirs-recursive
+installdirs-am:
+install: install-recursive
+install-exec: install-exec-recursive
+install-data: install-data-recursive
+uninstall: uninstall-recursive
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-recursive
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+ -test -z "$(CLEANFILES)" || rm -f $(CLEANFILES)
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+ -test -z "$(DISTCLEANFILES)" || rm -f $(DISTCLEANFILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-recursive
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-recursive
+ -rm -f ./$(DEPDIR)/libco1_la-callout_library.Plo
+ -rm -f ./$(DEPDIR)/libco2_la-callout_library.Plo
+ -rm -f ./$(DEPDIR)/libco3_la-callout_params_library.Plo
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine4_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine6_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine_expiration_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine_hooks_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine_utils.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-callout_handle_store_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cb_ctl_dhcp_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_db_access_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_duid_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_expiration_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_host_operations_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_hosts_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_iface_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_mac_source_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_multi_threading_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_option_def_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_option_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_rsoo_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks4_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks6_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_subnets4_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_subnets6_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfgmgr_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-client_class_def_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-client_class_def_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-csv_lease_file4_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-csv_lease_file6_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-d2_client_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-d2_udp_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-dhcp4o6_ipc_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-dhcp_parsers_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-duid_config_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-expiration_config_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-free_lease_queue_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-generic_lease_mgr_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-host_cache_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-host_data_source_factory_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-host_mgr_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-host_reservation_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-host_reservations_list_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-host_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-ifaces_config_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-ip_range_permutation_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-ip_range_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-lease_file_loader_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-lease_mgr_factory_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-lease_mgr_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-lease_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-memfile_lease_limits_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-memfile_lease_mgr_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-multi_threading_config_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-mysql_host_data_source_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-mysql_lease_mgr_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-ncr_generator_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-network_state_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-network_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-pgsql_host_data_source_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-pgsql_lease_mgr_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-pool_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-resource_handler_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-run_unittests.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-sanity_checks_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-shared_network_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-shared_network_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-shared_networks_list_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-srv_config_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-subnet_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-test_get_callout_handle.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-timer_mgr_unittest.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-recursive
+
+dvi-am:
+
+html: html-recursive
+
+html-am:
+
+info: info-recursive
+
+info-am:
+
+install-data-am:
+
+install-dvi: install-dvi-recursive
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-recursive
+
+install-html-am:
+
+install-info: install-info-recursive
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-recursive
+
+install-pdf-am:
+
+install-ps: install-ps-recursive
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-recursive
+ -rm -f ./$(DEPDIR)/libco1_la-callout_library.Plo
+ -rm -f ./$(DEPDIR)/libco2_la-callout_library.Plo
+ -rm -f ./$(DEPDIR)/libco3_la-callout_params_library.Plo
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine4_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine6_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine_expiration_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine_hooks_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-alloc_engine_utils.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-callout_handle_store_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cb_ctl_dhcp_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_db_access_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_duid_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_expiration_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_host_operations_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_hosts_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_iface_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_mac_source_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_multi_threading_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_option_def_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_option_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_rsoo_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks4_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_shared_networks6_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_subnets4_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfg_subnets6_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-cfgmgr_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-client_class_def_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-client_class_def_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-csv_lease_file4_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-csv_lease_file6_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-d2_client_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-d2_udp_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-dhcp4o6_ipc_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-dhcp_parsers_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-dhcp_queue_control_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-duid_config_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-expiration_config_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-free_lease_queue_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-generic_lease_mgr_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-host_cache_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-host_data_source_factory_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-host_mgr_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-host_reservation_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-host_reservations_list_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-host_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-ifaces_config_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-ip_range_permutation_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-ip_range_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-lease_file_loader_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-lease_mgr_factory_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-lease_mgr_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-lease_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-memfile_lease_limits_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-memfile_lease_mgr_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-multi_threading_config_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-mysql_host_data_source_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-mysql_lease_mgr_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-ncr_generator_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-network_state_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-network_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-pgsql_host_data_source_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-pgsql_lease_mgr_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-pool_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-resource_handler_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-run_unittests.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-sanity_checks_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-shared_network_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-shared_network_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-shared_networks_list_parser_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-srv_config_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-subnet_unittest.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-test_get_callout_handle.Po
+ -rm -f ./$(DEPDIR)/libdhcpsrv_unittests-timer_mgr_unittest.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-recursive
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-recursive
+
+pdf-am:
+
+ps: ps-recursive
+
+ps-am:
+
+uninstall-am:
+
+.MAKE: $(am__recursive_targets) check-am install-am install-strip
+
+.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am \
+ am--depfiles check check-TESTS check-am clean clean-generic \
+ clean-libtool clean-noinstLTLIBRARIES clean-noinstPROGRAMS \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-ps install-ps-am install-strip \
+ installcheck installcheck-am installdirs installdirs-am \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib/dhcpsrv/tests/alloc_engine4_unittest.cc b/src/lib/dhcpsrv/tests/alloc_engine4_unittest.cc
new file mode 100644
index 0000000..2734ebb
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/alloc_engine4_unittest.cc
@@ -0,0 +1,4980 @@
+// Copyright (C) 2015-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/option_int.h>
+#include <dhcpsrv/shared_network.h>
+#include <dhcpsrv/host_mgr.h>
+#include <dhcpsrv/tests/alloc_engine_utils.h>
+#include <dhcpsrv/testutils/test_utils.h>
+#include <testutils/gtest_utils.h>
+#include <hooks/hooks_manager.h>
+#include <hooks/callout_handle.h>
+#include <stats/stats_mgr.h>
+
+#if defined HAVE_MYSQL
+#include <mysql/testutils/mysql_schema.h>
+#endif
+
+#if defined HAVE_PGSQL
+#include <pgsql/testutils/pgsql_schema.h>
+#endif
+
+using namespace std;
+using namespace isc::hooks;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::stats;
+using namespace isc::util;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+// This test checks if the v4 Allocation Engine can be instantiated, parses
+// parameters string and allocators are created.
+TEST_F(AllocEngine4Test, constructor) {
+ boost::scoped_ptr<AllocEngine> x;
+
+ // Hashed and random allocators are not supported yet
+ ASSERT_THROW(x.reset(new AllocEngine(AllocEngine::ALLOC_HASHED, 5, false)),
+ NotImplemented);
+ ASSERT_THROW(x.reset(new AllocEngine(AllocEngine::ALLOC_RANDOM, 5, false)),
+ NotImplemented);
+
+ // Create V4 (ipv6=false) Allocation Engine that will try at most
+ // 100 attempts to pick up a lease
+ ASSERT_NO_THROW(x.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100,
+ false)));
+
+ // There should be V4 allocator
+ ASSERT_TRUE(x->getAllocator(Lease::TYPE_V4));
+
+ // Check that allocators for V6 stuff are not created
+ EXPECT_THROW(x->getAllocator(Lease::TYPE_NA), BadValue);
+ EXPECT_THROW(x->getAllocator(Lease::TYPE_TA), BadValue);
+ EXPECT_THROW(x->getAllocator(Lease::TYPE_PD), BadValue);
+}
+
+// This test checks if two simple IPv4 allocations succeed and that the
+// statistics is properly updated. Prior to the second allocation it
+// resets the pointer to the last allocated address within the address
+// pool. This causes the engine to walk over the already allocated
+// address and then pick the first available address for the second
+// allocation. Because the allocation engine checks the callouts next
+// step status after each attempt to allocate an address, this test
+// also sets this status to non-default value prior to the second
+// allocation attempt, to make sure that this unexpected status will
+// not interfere with the allocation.
+TEST_F(AllocEngine4Test, simpleAlloc4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Assigned addresses should be zero.
+ EXPECT_TRUE(testStatistics("assigned-addresses", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-addresses",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-addresses");
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Do all checks on the lease
+ checkLease4(lease);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+
+ // Assigned addresses should have incremented.
+ EXPECT_TRUE(testStatistics("assigned-addresses", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses", glbl_cumulative));
+
+ // Second allocation starts here.
+ uint8_t hwaddr2_data[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe };
+ HWAddrPtr hwaddr2(new HWAddr(hwaddr2_data, sizeof(hwaddr2_data), HTYPE_ETHER));
+ AllocEngine::ClientContext4 ctx2(subnet_, ClientIdPtr(), hwaddr2, IOAddress("0.0.0.0"),
+ false, true, "anotherhost.example.com.",
+ false);
+ ctx2.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Set the next step to non-default value to verify that it doesn't
+ // affect the allocation.
+ ctx2.callout_handle_ = HooksManager::createCalloutHandle();
+ ctx2.callout_handle_->setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ // Set the last allocated to the beginning of the pool. The allocation
+ // engine should detect that the first address is already allocated and
+ // assign the first available one.
+ pool_->resetLastAllocated();
+
+ lease = engine->allocateLease4(ctx2);
+
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx2.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Check that the lease is indeed in LeaseMgr
+ from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+
+ // Assigned addresses should have incremented.
+ EXPECT_TRUE(testStatistics("assigned-addresses", 2, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses", glbl_cumulative));
+}
+
+// This test checks that simple allocation uses the default valid lifetime.
+TEST_F(AllocEngine4Test, defaultAlloc4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Do all checks on the lease
+ checkLease4(lease);
+
+ // Check the valid lifetime has the default.
+ EXPECT_EQ(subnet_->getValid(), lease->valid_lft_);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+}
+
+// This test checks that simple allocation uses the specified valid lifetime.
+TEST_F(AllocEngine4Test, hintAlloc4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Specify the valid lifetime we want.
+ OptionUint32Ptr opt(new OptionUint32(Option::V4, DHO_DHCP_LEASE_TIME, 4));
+ ctx.query_->addOption(opt);
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Do all checks on the lease
+ checkLease4(lease);
+
+ // Check the valid lifetime has the wanted value.
+ EXPECT_EQ(opt->getValue(), lease->valid_lft_);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+}
+
+// This test checks that simple allocation uses the min valid lifetime.
+TEST_F(AllocEngine4Test, minAlloc4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(2, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Specify the valid lifetime we want, as it is lower than the min value
+ // we'll get this min value instead.
+ OptionUint32Ptr opt(new OptionUint32(Option::V4, DHO_DHCP_LEASE_TIME, 1));
+ ctx.query_->addOption(opt);
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Do all checks on the lease
+ checkLease4(lease);
+
+ // Check the valid lifetime has the wanted value.
+ EXPECT_EQ(subnet_->getValid().getMin(), lease->valid_lft_);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+}
+
+// This test checks that simple allocation uses the max valid lifetime.
+TEST_F(AllocEngine4Test, maxAlloc4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Specify the valid lifetime we want, as it is greater than the max value
+ // we'll get this max value instead.
+ OptionUint32Ptr opt(new OptionUint32(Option::V4, DHO_DHCP_LEASE_TIME, 6));
+ ctx.query_->addOption(opt);
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Do all checks on the lease
+ checkLease4(lease);
+
+ // Check the valid lifetime has the wanted value.
+ EXPECT_EQ(subnet_->getValid().getMax(), lease->valid_lft_);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+}
+
+// This test checks that simple allocation handles BOOTP queries.
+TEST_F(AllocEngine4Test, bootpAlloc4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Make the query a BOOTP one.
+ ctx.query_->addClass("BOOTP");
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Check that is belongs to the right subnet and client.
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+ EXPECT_TRUE(subnet_->inRange(lease->addr_));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, lease->addr_));
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_TRUE(*lease->client_id_ == *clientid_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(*lease->hwaddr_ == *hwaddr_);
+
+ // Check the valid lifetime is infinite.
+ uint32_t infinity_lft = Lease::INFINITY_LFT;
+ EXPECT_EQ(infinity_lft, lease->valid_lft_);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+}
+
+// This test checks if the fake allocation (for DHCPDISCOVER) can succeed
+TEST_F(AllocEngine4Test, fakeAlloc4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Assigned addresses should be zero.
+ EXPECT_TRUE(testStatistics("assigned-addresses", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-addresses",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-addresses");
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, true,
+ "host.example.com.", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Do all checks on the lease
+ checkLease4(lease);
+
+ // Check that the lease is NOT in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_FALSE(from_mgr);
+
+ // Assigned addresses should still be zero.
+ EXPECT_TRUE(testStatistics("assigned-addresses", 0, subnet_->getID()));
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-addresses", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-addresses"));
+}
+
+// This test checks if the allocation with a hint that is valid (in range,
+// in pool and free) can succeed
+TEST_F(AllocEngine4Test, allocWithValidHint4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.105"), true, true,
+ "host.example.com.", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // We have allocated the new lease, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // We should get what we asked for
+ EXPECT_EQ(lease->addr_.toText(), "192.0.2.105");
+
+ // Do all checks on the lease
+ checkLease4(lease);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+}
+
+// This test checks if the allocation with a hint that is in range,
+// in pool, but is currently used can succeed
+TEST_F(AllocEngine4Test, allocWithUsedHint4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Let's create a lease and put it in the LeaseMgr
+ uint8_t hwaddr2_data[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe };
+ HWAddrPtr hwaddr2(new HWAddr(hwaddr2_data, sizeof(hwaddr2_data), HTYPE_ETHER));
+ uint8_t clientid2[] = { 8, 7, 6, 5, 4, 3, 2, 1 };
+ time_t now = time(NULL);
+ Lease4Ptr used(new Lease4(IOAddress("192.0.2.106"), hwaddr2,
+ clientid2, sizeof(clientid2), 1, now, subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Another client comes in and request an address that is in pool, but
+ // unfortunately it is used already. The same address must not be allocated
+ // twice.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.106"), false, false,
+ "", true);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // New lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Allocated address must be different
+ EXPECT_NE(used->addr_, lease->addr_);
+
+ // We should NOT get what we asked for, because it is used already
+ EXPECT_NE("192.0.2.106", lease->addr_.toText());
+
+ // Do all checks on the lease
+ checkLease4(lease);
+
+ // The lease should not be in the LeaseMgr because it was a failed allocation.
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_FALSE(from_mgr);
+}
+
+// This test checks if an allocation with a hint that is out of the blue
+// can succeed. The invalid hint should be ignored completely.
+TEST_F(AllocEngine4Test, allocBogusHint4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Client would like to get a 10.1.1.1 lease, which does not belong to any
+ // supported lease. Allocation engine should ignore it and carry on
+ // with the normal allocation
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("10.1.1.1"), false, false,
+ "", true);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // We have allocated a new lease, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // We should NOT get what we asked for, because it is used already
+ EXPECT_NE("10.1.1.1", lease->addr_.toText());
+
+ // Do all checks on the lease
+ checkLease4(lease);
+
+ // Check that the lease is not in the LeaseMgr as it is a fake allocation.
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ EXPECT_FALSE(from_mgr);
+}
+
+// This test checks that NULL values are handled properly
+TEST_F(AllocEngine4Test, allocateLease4Nulls) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Allocations without subnet are not allowed
+ AllocEngine::ClientContext4 ctx1(Subnet4Ptr(), clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx1.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ Lease4Ptr lease = engine->allocateLease4(ctx1);
+
+ ASSERT_FALSE(lease);
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes", 1));
+
+ // Allocations without HW address are not allowed
+ AllocEngine::ClientContext4 ctx2(subnet_, clientid_, HWAddrPtr(),
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx2.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ lease = engine->allocateLease4(ctx2);
+ ASSERT_FALSE(lease);
+ ASSERT_FALSE(ctx2.old_lease_);
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes", 1));
+
+ // Allocations without client-id are allowed
+ clientid_.reset();
+ AllocEngine::ClientContext4 ctx3(subnet_, ClientIdPtr(), hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx3.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ lease = engine->allocateLease4(ctx3);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+ // New lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx3.old_lease_);
+
+ // Do all checks on the lease
+ checkLease4(lease);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+}
+
+// This test checks if a returning client can renew an
+// an existing lease and assigned-leases increments accordingly
+TEST_F(AllocEngine4Test, simpleRenew4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ EXPECT_TRUE(testStatistics("assigned-addresses", 0, subnet_->getID()));
+ int64_t cumulative = getStatistics("cumulative-assigned-addresses",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-addresses");
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // Check that we got a lease and it's sane
+ ASSERT_TRUE(lease);
+ checkLease4(lease);
+
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // We should have incremented assigned-addresses
+ EXPECT_TRUE(testStatistics("assigned-addresses", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses", glbl_cumulative));
+
+ // Do it again, this should amount to the renew of an existing lease
+ Lease4Ptr lease2 = engine->allocateLease4(ctx);
+
+ // Check that we got a lease and it's sane
+ ASSERT_TRUE(lease2);
+ checkLease4(lease2);
+
+ // Lease already existed, so old_lease should be set.
+ EXPECT_TRUE(ctx.old_lease_);
+
+ // Should NOT have bumped assigned-addresses
+ EXPECT_TRUE(testStatistics("assigned-addresses", 1, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses",
+ cumulative, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses", glbl_cumulative));
+}
+
+// This test checks simple renewal uses the default valid lifetime.
+TEST_F(AllocEngine4Test, defaultRenew4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // Check that we got a lease and it's sane
+ ASSERT_TRUE(lease);
+ checkLease4(lease);
+
+ // Check the valid lifetime has the default.
+ EXPECT_EQ(subnet_->getValid(), lease->valid_lft_);
+
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Do it again, this should amount to the renew of an existing lease
+ Lease4Ptr lease2 = engine->allocateLease4(ctx);
+
+ // Check that we got a lease and it's sane
+ ASSERT_TRUE(lease2);
+ checkLease4(lease2);
+
+ // Lease already existed, so old_lease should be set.
+ EXPECT_TRUE(ctx.old_lease_);
+
+ // Check the renewed valid lifetime has the default.
+ EXPECT_EQ(subnet_->getValid(), lease2->valid_lft_);
+}
+
+// This test checks simple renewal uses the specified valid lifetime.
+TEST_F(AllocEngine4Test, hintRenew4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Specify the valid lifetime we want.
+ OptionUint32Ptr opt(new OptionUint32(Option::V4, DHO_DHCP_LEASE_TIME, 4));
+ ctx.query_->addOption(opt);
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // Check that we got a lease and it's sane
+ ASSERT_TRUE(lease);
+ checkLease4(lease);
+
+ // Check the valid lifetime has the wanted value.
+ EXPECT_EQ(opt->getValue(), lease->valid_lft_);
+
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Do it again, this should amount to the renew of an existing lease
+ Lease4Ptr lease2 = engine->allocateLease4(ctx);
+
+ // Check that we got a lease and it's sane
+ ASSERT_TRUE(lease2);
+ checkLease4(lease2);
+
+ // Lease already existed, so old_lease should be set.
+ EXPECT_TRUE(ctx.old_lease_);
+
+ // Check the renewed valid lifetime has the wanted value.
+ EXPECT_EQ(opt->getValue(), lease2->valid_lft_);
+}
+
+// This test checks simple renewal uses the min valid lifetime.
+TEST_F(AllocEngine4Test, minRenew4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(2, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Specify the valid lifetime we want, as it is lower than the min value
+ // we'll get this min value instead.
+ OptionUint32Ptr opt(new OptionUint32(Option::V4, DHO_DHCP_LEASE_TIME, 1));
+ ctx.query_->addOption(opt);
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // Check that we got a lease and it's sane
+ ASSERT_TRUE(lease);
+ checkLease4(lease);
+
+ // Check the valid lifetime has the min value.
+ EXPECT_EQ(subnet_->getValid().getMin(), lease->valid_lft_);
+
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Do it again, this should amount to the renew of an existing lease
+ Lease4Ptr lease2 = engine->allocateLease4(ctx);
+
+ // Check that we got a lease and it's sane
+ ASSERT_TRUE(lease2);
+ checkLease4(lease2);
+
+ // Lease already existed, so old_lease should be set.
+ EXPECT_TRUE(ctx.old_lease_);
+
+ // Check the renewed valid lifetime has the min value.
+ EXPECT_EQ(subnet_->getValid().getMin(), lease2->valid_lft_);
+}
+
+// This test checks simple renewal uses the max valid lifetime.
+TEST_F(AllocEngine4Test, maxRenew4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Specify the valid lifetime we want, as it is greater than the max value
+ // we'll get this max value instead.
+ OptionUint32Ptr opt(new OptionUint32(Option::V4, DHO_DHCP_LEASE_TIME, 6));
+ ctx.query_->addOption(opt);
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // Check that we got a lease and it's sane
+ ASSERT_TRUE(lease);
+ checkLease4(lease);
+
+ // Check the valid lifetime has the max value.
+ EXPECT_EQ(subnet_->getValid().getMax(), lease->valid_lft_);
+
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Do it again, this should amount to the renew of an existing lease
+ Lease4Ptr lease2 = engine->allocateLease4(ctx);
+
+ // Check that we got a lease and it's sane
+ ASSERT_TRUE(lease2);
+ checkLease4(lease2);
+
+ // Lease already existed, so old_lease should be set.
+ EXPECT_TRUE(ctx.old_lease_);
+
+ // Check the renewed valid lifetime has the max value.
+ EXPECT_EQ(subnet_->getValid().getMax(), lease2->valid_lft_);
+}
+
+// This test checks simple renewal handles BOOTP queries.
+TEST_F(AllocEngine4Test, bootpRenew4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Make the query a BOOTP one.
+ ctx.query_->addClass("BOOTP");
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // Check that we got a lease.
+ ASSERT_TRUE(lease);
+
+ // Check that is belongs to the right subnet and client.
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+ EXPECT_TRUE(subnet_->inRange(lease->addr_));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, lease->addr_));
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_TRUE(*lease->client_id_ == *clientid_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(*lease->hwaddr_ == *hwaddr_);
+
+ // Check the valid lifetime is infinite.
+ uint32_t infinity_lft = Lease::INFINITY_LFT;
+ EXPECT_EQ(infinity_lft, lease->valid_lft_);
+
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Do it again, this should amount to the renew of an existing lease
+ Lease4Ptr lease2 = engine->allocateLease4(ctx);
+
+ // Check that we got a lease.
+ ASSERT_TRUE(lease2);
+
+ // Check that is belongs to the right subnet and client.
+ EXPECT_EQ(lease2->subnet_id_, subnet_->getID());
+ EXPECT_TRUE(subnet_->inRange(lease2->addr_));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, lease2->addr_));
+ ASSERT_TRUE(lease2->client_id_);
+ EXPECT_TRUE(*lease2->client_id_ == *clientid_);
+ ASSERT_TRUE(lease2->hwaddr_);
+ EXPECT_TRUE(*lease2->hwaddr_ == *hwaddr_);
+
+ // Lease already existed, so old_lease should be set.
+ EXPECT_TRUE(ctx.old_lease_);
+
+ // Check the renewed valid lifetime has the max value.
+ EXPECT_EQ(infinity_lft, lease2->valid_lft_);
+}
+
+// This test verifies that the allocator picks addresses that belong to the
+// pool
+TEST_F(AllocEngine4Test, IterativeAllocator) {
+ boost::scoped_ptr<NakedAllocEngine::Allocator>
+ alloc(new NakedAllocEngine::IterativeAllocator(Lease::TYPE_V4));
+
+ for (int i = 0; i < 1000; ++i) {
+ IOAddress candidate = alloc->pickAddress(subnet_, cc_, clientid_,
+ IOAddress("0.0.0.0"));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, candidate));
+ }
+}
+
+// This test verifies that the allocator picks addresses that belong to the
+// pool using classification
+TEST_F(AllocEngine4Test, IterativeAllocator_class) {
+ boost::scoped_ptr<NakedAllocEngine::Allocator>
+ alloc(new NakedAllocEngine::IterativeAllocator(Lease::TYPE_V4));
+
+ // Restrict pool_ to the foo class. Add a second pool with bar class.
+ pool_->allowClientClass("foo");
+ Pool4Ptr pool(new Pool4(IOAddress("192.0.2.200"),
+ IOAddress("192.0.2.209")));
+ pool->allowClientClass("bar");
+ subnet_->addPool(pool);
+
+ // Clients are in bar
+ cc_.insert("bar");
+
+ for (int i = 0; i < 1000; ++i) {
+ IOAddress candidate = alloc->pickAddress(subnet_, cc_, clientid_,
+ IOAddress("0.0.0.0"));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, candidate));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, candidate, cc_));
+ }
+}
+
+// This test verifies that the iterative allocator really walks over all addresses
+// in all pools in specified subnet. It also must not pick the same address twice
+// unless it runs out of pool space and must start over.
+TEST_F(AllocEngine4Test, IterativeAllocator_manyPools4) {
+ NakedAllocEngine::IterativeAllocator alloc(Lease::TYPE_V4);
+
+ // Let's start from 2, as there is 2001:db8:1::10 - 2001:db8:1::20 pool already.
+ for (int i = 2; i < 10; ++i) {
+ stringstream min, max;
+
+ min << "192.0.2." << i * 10 + 1;
+ max << "192.0.2." << i * 10 + 9;
+
+ Pool4Ptr pool(new Pool4(IOAddress(min.str()),
+ IOAddress(max.str())));
+ // cout << "Adding pool: " << min.str() << "-" << max.str() << endl;
+ subnet_->addPool(pool);
+ }
+
+ int total = 10 + 8 * 9; // first pool (.100 - .109) has 10 addresses in it,
+ // there are 8 extra pools with 9 addresses in each.
+
+ // Let's keep picked addresses here and check their uniqueness.
+ std::set<IOAddress> generated_addrs;
+ int cnt = 0;
+ while (++cnt) {
+ IOAddress candidate = alloc.pickAddress(subnet_, cc_, clientid_, IOAddress("0.0.0.0"));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, candidate));
+
+ // One way to easily verify that the iterative allocator really works is
+ // to uncomment the following line and observe its output that it
+ // covers all defined subnets.
+ // cout << candidate.toText() << endl;
+
+ if (generated_addrs.find(candidate) == generated_addrs.end()) {
+ // We haven't had this
+ generated_addrs.insert(candidate);
+ } else {
+ // We have seen this address before. That should mean that we
+ // iterated over all addresses.
+ if (generated_addrs.size() == total) {
+ // We have exactly the number of address in all pools
+ break;
+ }
+ ADD_FAILURE() << "Too many or not enough unique addresses generated.";
+ break;
+ }
+
+ if ( cnt>total ) {
+ ADD_FAILURE() << "Too many unique addresses generated.";
+ break;
+ }
+ }
+}
+
+// This test checks if really small pools are working
+TEST_F(AllocEngine4Test, smallPool4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ IOAddress addr("192.0.2.17");
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+
+ // Get rid of the default subnet configuration.
+ cfg_mgr.clear();
+
+ // Create configuration similar to other tests, but with a single address pool
+ subnet_ = Subnet4Ptr(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3));
+ pool_ = Pool4Ptr(new Pool4(addr, addr)); // just a single address
+ subnet_->addPool(pool_);
+ cfg_mgr.getStagingCfg()->getCfgSubnets4()->add(subnet_);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "host.example.com.", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // Check that we got that single lease
+ ASSERT_TRUE(lease);
+
+ // We have allocated new lease, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ EXPECT_EQ("192.0.2.17", lease->addr_.toText());
+
+ // Do all checks on the lease
+ checkLease4(lease);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+}
+
+// This test checks if all addresses in a pool are currently used, the attempt
+// to find out a new lease fails.
+TEST_F(AllocEngine4Test, outOfAddresses4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ IOAddress addr("192.0.2.17");
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ // Get rid of the default test configuration.
+ cfg_mgr.clear();
+
+ // Create configuration similar to other tests, but with a single address pool
+ subnet_ = Subnet4Ptr(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3));
+ pool_ = Pool4Ptr(new Pool4(addr, addr)); // just a single address
+ subnet_->addPool(pool_);
+ cfg_mgr.getStagingCfg()->getCfgSubnets4()->add(subnet_);
+
+ // Just a different hw/client-id for the second client
+ uint8_t hwaddr2_data[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe };
+ HWAddrPtr hwaddr2(new HWAddr(hwaddr2_data, sizeof(hwaddr2_data), HTYPE_ETHER));
+ uint8_t clientid2[] = { 8, 7, 6, 5, 4, 3, 2, 1 };
+ time_t now = time(NULL);
+ Lease4Ptr lease(new Lease4(addr, hwaddr2, clientid2,
+ sizeof(clientid2), 501, now,
+ subnet_->getID()));
+ lease->cltt_ = time(NULL) - 10; // Allocated 10 seconds ago
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // There is just a single address in the pool and allocated it to someone
+ // else, so the allocation should fail
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "host.example.com.", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ Lease4Ptr lease2 = engine->allocateLease4(ctx);
+ ASSERT_FALSE(lease2);
+ ASSERT_FALSE(ctx.old_lease_);
+
+ EXPECT_EQ(1, getStatistics("v4-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network"));
+ EXPECT_EQ(1, getStatistics("v4-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes"));
+
+ EXPECT_EQ(1, getStatistics("v4-allocation-fail", 2));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network", 2));
+ EXPECT_EQ(1, getStatistics("v4-allocation-fail-subnet", 2));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools", 2));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes", 2));
+}
+
+/// @brief This test class is dedicated to testing shared networks
+///
+/// It uses one common configuration:
+/// 1 shared network with 2 subnets:
+/// - 192.0.2.0/24 subnet with a small pool of single address: 192.0.2.17
+/// - 10.1.2.0/24 subnet with pool with 96 addresses.
+class SharedNetworkAlloc4Test : public AllocEngine4Test {
+public:
+
+ /// @brief Initializes configuration (2 subnets, 1 shared network)
+ SharedNetworkAlloc4Test()
+ :engine_(AllocEngine::ALLOC_ITERATIVE, 0, false) {
+ // Create two subnets, each with a single address pool. The first subnet
+ // has only one address in its address pool to make it easier to simulate
+ // address exhaustion.
+ subnet1_.reset(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3, SubnetID(1)));
+ subnet2_.reset(new Subnet4(IOAddress("10.1.2.0"), 24, 1, 2, 3, SubnetID(2)));
+ pool1_.reset(new Pool4(IOAddress("192.0.2.17"), IOAddress("192.0.2.17")));
+ pool2_.reset(new Pool4(IOAddress("10.1.2.5"), IOAddress("10.1.2.100")));
+
+ subnet1_->addPool(pool1_);
+ subnet2_->addPool(pool2_);
+
+ // Both subnets belong to the same network so they can be used
+ // interchangeably.
+ network_.reset(new SharedNetwork4("test_network"));
+ network_->add(subnet1_);
+ network_->add(subnet2_);
+
+ std::vector<uint8_t> hwaddr_vec = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe };
+ hwaddr2_.reset(new HWAddr(hwaddr_vec, HTYPE_ETHER));
+ }
+
+ /// @brief Inserts a new lease for specified address
+ ///
+ /// Creates a new lease for specified address and subnet-id and inserts
+ /// it into database. This is not particularly fancy method, it is used
+ /// just to mark existing addresses as used. It uses hwaddr2_ to allocate
+ /// the lease.
+ ///
+ /// @param addr text representation of the address
+ /// @param subnet_id ID of the subnet
+ /// @param return pointer to the lease
+ Lease4Ptr
+ insertLease(std::string addr, SubnetID subnet_id) {
+ Lease4Ptr lease(new Lease4(IOAddress(addr), hwaddr2_, ClientIdPtr(),
+ 501, time(NULL), subnet_id));
+ lease->cltt_ = time(NULL) - 10; // Allocated 10 seconds ago
+ if (!LeaseMgrFactory::instance().addLease(lease)) {
+ ADD_FAILURE() << "Attempt to add a lease for IP " << addr
+ << " in subnet " << subnet_id << " failed";
+ }
+
+ return (lease);
+ }
+
+ /// Convenience pointers to configuration elements. These are initialized
+ /// in the constructor and are used throughout the tests.
+ AllocEngine engine_;
+ Subnet4Ptr subnet1_;
+ Subnet4Ptr subnet2_;
+ Pool4Ptr pool1_;
+ Pool4Ptr pool2_;
+ SharedNetwork4Ptr network_;
+
+ HWAddrPtr hwaddr2_; // Note there's hwaddr_ already defined in base class.
+};
+
+// This test verifies that the server can offer an address from a
+// subnet and the introduction of shared network doesn't break anything here.
+TEST_F(SharedNetworkAlloc4Test, discoverSharedNetworkSimple) {
+
+ // Create context which will be used to try to allocate leases from the
+ // shared network. The context points to subnet1, which address space
+ // is exhausted. We expect the allocation engine to find another subnet
+ // within the same shared network and offer an address from there.
+ AllocEngine::ClientContext4
+ ctx(subnet1_, ClientIdPtr(), hwaddr_, IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "host.example.com.", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ Lease4Ptr lease = engine_.allocateLease4(ctx);
+
+ // The allocation engine should have assigned an address from the first
+ // subnet.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.2.17", lease->addr_.toText());
+
+ // Make sure the lease is not in the lease mgr (this is only
+ // discover).
+ ASSERT_FALSE(LeaseMgrFactory::instance().getLease4(lease->addr_));
+}
+
+// This test verifies that the server will pick a second subnet out of two
+// shared subnets if there is a hint for the second subnet.
+TEST_F(SharedNetworkAlloc4Test, discoverSharedNetworkHint) {
+
+ // Create context which will be used to try to allocate leases from the
+ // shared network. The context points to subnet1, which address space
+ // is exhausted. We expect the allocation engine to find another subnet
+ // within the same shared network and offer an address from there.
+
+ AllocEngine::ClientContext4
+ ctx(subnet1_, ClientIdPtr(), hwaddr_, IOAddress("10.1.2.25"),
+ false, false, "host.example.com.", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ Lease4Ptr lease = engine_.allocateLease4(ctx);
+
+ // The allocation engine should have assigned an address from the second
+ // subnet, because that's what the hint requested.
+ ASSERT_TRUE(lease);
+ EXPECT_TRUE(subnet2_->inPool(Lease::TYPE_V4, lease->addr_));
+}
+
+// This test verifies that the server can offer an address from a
+// different subnet than orginally selected, when the address pool in
+// the first subnet is exhausted.
+TEST_F(SharedNetworkAlloc4Test, discoverSharedNetwork) {
+ // Create a lease for a single address in the first address pool. The
+ // pool is now exhausted.
+ Lease4Ptr lease = insertLease("192.0.2.17", subnet1_->getID());
+
+ // Create context which will be used to try to allocate leases from the
+ // shared network. The context points to subnet1, which address space
+ // is exhausted. We expect the allocation engine to find another subnet
+ // within the same shared network and offer an address from there.
+ AllocEngine::ClientContext4
+ ctx(subnet1_, ClientIdPtr(), hwaddr_, IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "host.example.com.", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ Lease4Ptr lease2 = engine_.allocateLease4(ctx);
+ // The allocation engine should have assigned an address from the second
+ // subnet. We could guess that this is 10.1.2.5, being the first address
+ // in the address pool, but to make the test more generic, we merely
+ // verify that the address is in the given address pool.
+ ASSERT_TRUE(lease2);
+ EXPECT_TRUE(subnet2_->inPool(Lease::TYPE_V4, lease2->addr_));
+
+ // The client should also be offered a lease when it specifies a hint
+ // that doesn't match the subnet from which the lease is offered. The
+ // engine should check alternative subnets to match the hint to
+ // a subnet. The requested lease is available, so it should be offered.
+ ctx.subnet_ = subnet1_;
+ ctx.requested_address_ = IOAddress("10.1.2.25");
+ lease2 = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease2);
+ EXPECT_EQ("10.1.2.25", lease2->addr_.toText());
+
+ // The returning client (the one that has a lease) should also be able
+ // to renew its lease regardless of a subnet it begins with. So, it has
+ // an address assigned from subnet1, but we use subnet2 as a selected
+ // subnet.
+ AllocEngine::ClientContext4 ctx2(subnet2_, ClientIdPtr(), hwaddr2_,
+ IOAddress("0.0.0.0"), false, false,
+ "host.example.com.", true);
+ ctx2.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ lease2 = engine_.allocateLease4(ctx2);
+ // The existing lease should be returned.
+ ASSERT_TRUE(lease2);
+ EXPECT_EQ("192.0.2.17", lease2->addr_.toText());
+}
+
+// This test verifies that the server can offer an address from a
+// different subnet than orginally selected, when the address pool in
+// the first subnet is exhausted.
+TEST_F(SharedNetworkAlloc4Test, discoverSharedNetworkClassification) {
+
+ // Try to offer address from subnet1. There is one address available
+ // so it should be offered.
+ AllocEngine::ClientContext4
+ ctx(subnet1_, ClientIdPtr(), hwaddr_, IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "host.example.com.", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ Lease4Ptr lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_TRUE(subnet1_->inPool(Lease::TYPE_V4, lease->addr_));
+
+ // Apply restrictions on the subnet1. This should be only assigned
+ // to clients belonging to cable-modem class.
+ subnet1_->allowClientClass("cable-modem");
+
+ // The allocation engine should determine that the subnet1 is not
+ // available for the client not belonging to the cable-modem class.
+ // Instead, it should offer an address from subnet2 that belongs
+ // to the same shared network.
+ ctx.subnet_ = subnet1_;
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_TRUE(subnet2_->inPool(Lease::TYPE_V4, lease->addr_));
+
+ // Create reservation for the client in subnet1. Because this subnet is
+ // not allowed for the client the client should still be offered a
+ // lease from subnet2.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet1_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.17")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+ AllocEngine::findReservation(ctx);
+
+ ctx.subnet_ = subnet1_;
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_TRUE(subnet2_->inPool(Lease::TYPE_V4, lease->addr_));
+
+ // Assign cable-modem class and try again. This time, we should
+ // offer an address from the subnet1.
+ ctx.query_->addClass(ClientClass("cable-modem"));
+
+ ctx.subnet_ = subnet1_;
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.2.17", lease->addr_.toText());
+}
+
+// This test verifies that the server can offer an address from a
+// different subnet than orginally selected, when the address pool in
+// the first subnet requires another class.
+TEST_F(SharedNetworkAlloc4Test, discoverSharedNetworkPoolClassification) {
+
+ // Try to offer address from subnet1. There is one address available
+ // so it should be offered.
+ AllocEngine::ClientContext4
+ ctx(subnet1_, ClientIdPtr(), hwaddr_, IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "host.example.com.", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ Lease4Ptr lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_TRUE(subnet1_->inPool(Lease::TYPE_V4, lease->addr_));
+
+ // Apply restrictions on the pool1. This should be only assigned
+ // to clients belonging to cable-modem class.
+ pool1_->allowClientClass("cable-modem");
+
+ // The allocation engine should determine that the pool1 is not
+ // available for the client not belonging to the cable-modem class.
+ // Instead, it should offer an address from subnet2 that belongs
+ // to the same shared network.
+ ctx.subnet_ = subnet1_;
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_TRUE(subnet2_->inPool(Lease::TYPE_V4, lease->addr_));
+
+ // Assign cable-modem class and try again. This time, we should
+ // offer an address from the pool1.
+ ctx.query_->addClass(ClientClass("cable-modem"));
+
+ // Restrict access to pool2 for this client, to make sure that the
+ // server doesn't accidentally get an address from this pool.
+ pool2_->allowClientClass("telephone");
+
+ ctx.subnet_ = subnet1_;
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.2.17", lease->addr_.toText());
+}
+
+// Test that reservations within shared network take precedence over the
+// existing leases regardless in which subnet belonging to a shared network
+// reservations belong.
+TEST_F(SharedNetworkAlloc4Test, discoverSharedNetworkReservations) {
+
+ EXPECT_FALSE(HostMgr::instance().getDisableSingleQuery());
+
+ // Create reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet2_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("10.2.3.23")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Start allocation from subnet1. The engine should determine that the
+ // client has reservations in subnet2 and should rather assign reserved
+ // addresses.
+ AllocEngine::ClientContext4
+ ctx(subnet1_, ClientIdPtr(), hwaddr_, IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "host.example.com.", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ AllocEngine::findReservation(ctx);
+ Lease4Ptr lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("10.2.3.23", lease->addr_.toText());
+
+ // Let's create a lease for the client to make sure the lease is not
+ // renewed but a reserved lease is offered.
+ Lease4Ptr lease2(new Lease4(IOAddress("192.0.2.17"), hwaddr_, ClientIdPtr(),
+ 501, time(NULL), subnet1_->getID()));
+ lease->cltt_ = time(NULL) - 10; // Allocated 10 seconds ago
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease2));
+ ctx.subnet_ = subnet1_;
+ ctx.hosts_.clear();
+ AllocEngine::findReservation(ctx);
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("10.2.3.23", lease->addr_.toText());
+}
+
+// Test that reservations within shared network take precedence over the
+// existing leases regardless in which subnet belonging to a shared network
+// reservations belong. Host lookups returning a collection are disabled.
+// As it is only an optimization the behavior (so the test) must stay
+// unchanged.
+TEST_F(SharedNetworkAlloc4Test, discoverSharedNetworkReservationsNoColl) {
+
+ // Disable host lookups returning a collection.
+ ASSERT_FALSE(HostMgr::instance().getDisableSingleQuery());
+ HostMgr::instance().setDisableSingleQuery(true);
+
+ // Create reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet2_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("10.2.3.23")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Start allocation from subnet1. The engine should determine that the
+ // client has reservations in subnet2 and should rather assign reserved
+ // addresses.
+ AllocEngine::ClientContext4
+ ctx(subnet1_, ClientIdPtr(), hwaddr_, IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "host.example.com.", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ AllocEngine::findReservation(ctx);
+ Lease4Ptr lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("10.2.3.23", lease->addr_.toText());
+
+ // Let's create a lease for the client to make sure the lease is not
+ // renewed but a reserved lease is offered.
+ Lease4Ptr lease2(new Lease4(IOAddress("192.0.2.17"), hwaddr_, ClientIdPtr(),
+ 501, time(NULL), subnet1_->getID()));
+ lease->cltt_ = time(NULL) - 10; // Allocated 10 seconds ago
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease2));
+ ctx.subnet_ = subnet1_;
+ ctx.hosts_.clear();
+ AllocEngine::findReservation(ctx);
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("10.2.3.23", lease->addr_.toText());
+}
+
+// This test verifies that the server can offer an address from a shared
+// subnet if there's at least 1 address left there, but will not offer
+// anything if both subnets are completely full.
+TEST_F(SharedNetworkAlloc4Test, runningOut) {
+
+ // Allocate everything in subnet1
+ insertLease("192.0.2.17", subnet1_->getID());
+
+ // Allocate everything, except one address in subnet2.
+ for (int i = 5; i < 100; i++) {
+ stringstream tmp;
+ tmp << "10.1.2." << i;
+ insertLease(tmp.str(), subnet2_->getID());
+ }
+
+ // Create context which will be used to try to allocate leases from the
+ // shared network. The context points to subnet1, which address space
+ // is exhausted. We expect the allocation engine to find another subnet
+ // within the same shared network and offer an address from there.
+ AllocEngine::ClientContext4
+ ctx(subnet1_, ClientIdPtr(), hwaddr_, IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "host.example.com.", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ Lease4Ptr lease = engine_.allocateLease4(ctx);
+ EXPECT_TRUE(lease);
+
+ // Now allocate the last address. Now both subnets are exhausted.
+ insertLease("10.1.2.100", subnet2_->getID());
+
+ // Ok, we're out. We should not get anything now.
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_FALSE(lease);
+
+ EXPECT_EQ(1, getStatistics("v4-allocation-fail"));
+ EXPECT_EQ(1, getStatistics("v4-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes"));
+
+ EXPECT_EQ(1, getStatistics("v4-allocation-fail", 1));
+ EXPECT_EQ(1, getStatistics("v4-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes", 1));
+}
+
+// This test verifies that the server can offer an address from a
+// subnet and the introduction of shared network doesn't break anything here.
+TEST_F(SharedNetworkAlloc4Test, requestSharedNetworkSimple) {
+
+ // Create context which will be used to try to allocate leases from the
+ // shared network. The context points to subnet1, which address space
+ // is exhausted. We expect the allocation engine to find another subnet
+ // within the same shared network and offer an address from there.
+ AllocEngine::ClientContext4
+ ctx(subnet1_, ClientIdPtr(), hwaddr_, IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "host.example.com.", false);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ Lease4Ptr lease = engine_.allocateLease4(ctx);
+
+ // The allocation engine should have assigned an address from the first
+ // subnet.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.2.17", lease->addr_.toText());
+
+ // Make sure the lease is in the lease mgr.
+ ASSERT_TRUE(LeaseMgrFactory::instance().getLease4(lease->addr_));
+}
+
+// This test verifies that the server can allocate an address from a
+// different subnet than orginally selected, when the address pool in
+// the first subnet is exhausted.
+TEST_F(SharedNetworkAlloc4Test, requestSharedNetwork) {
+
+ // Create a lease for a single address in the first address pool. The
+ // pool is now exhausted.
+ Lease4Ptr lease = insertLease("192.0.2.17", subnet1_->getID());
+
+ // Create context which will be used to try to allocate leases from the
+ // shared network. The context points to subnet1, which address space
+ // is exhausted. We expect the allocation engine to find another subnet
+ // within the same shared network and offer an address from there.
+ AllocEngine::ClientContext4
+ ctx(subnet1_, ClientIdPtr(), hwaddr_, IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "host.example.com.", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ Lease4Ptr lease2 = engine_.allocateLease4(ctx);
+ // The allocation engine should have assigned an address from the second
+ // subnet. We could guess that this is 10.1.2.5, being the first address
+ // in the address pool, but to make the test more generic, we merely
+ // verify that the address is in the given address pool.
+ ASSERT_TRUE(lease2);
+ EXPECT_TRUE(subnet2_->inPool(Lease::TYPE_V4, lease2->addr_));
+
+ ASSERT_TRUE(LeaseMgrFactory::instance().deleteLease(lease2));
+
+ // The client should also be assigned a lease when it specifies a hint
+ // that doesn't match the subnet from which the lease is offered. The
+ // engine should check alternative subnets to match the hint to
+ // a subnet. The requested lease is available, so it should be offered.
+ ctx.subnet_ = subnet1_;
+ ctx.requested_address_ = IOAddress("10.1.2.25");
+ lease2 = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease2);
+ EXPECT_EQ("10.1.2.25", lease2->addr_.toText());
+
+ // The returning client (the one that has a lease) should also be able
+ // to renew its lease regardless of a subnet it begins with. So, it has
+ // an address assigned from subnet1, but we use subnet2 as a selected
+ // subnet.
+ AllocEngine::ClientContext4 ctx2(subnet2_, ClientIdPtr(), hwaddr2_,
+ IOAddress("0.0.0.0"), false, false,
+ "host.example.com.", false);
+ ctx2.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ lease2 = engine_.allocateLease4(ctx2);
+ // The existing lease should be returned.
+ ASSERT_TRUE(lease2);
+ EXPECT_EQ("192.0.2.17", lease2->addr_.toText());
+}
+
+// This test verifies that the server can assign an address from a
+// different subnet than orginally selected, when the address pool in
+// the first subnet is exhausted.
+TEST_F(SharedNetworkAlloc4Test, requestSharedNetworkClassification) {
+ // Try to offer address from subnet1. There is one address available
+ // so it should be offered.
+ AllocEngine::ClientContext4
+ ctx(subnet1_, ClientIdPtr(), hwaddr_, IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "host.example.com.", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ Lease4Ptr lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_TRUE(subnet1_->inPool(Lease::TYPE_V4, lease->addr_));
+
+ // Remove the lease so as we can start over.
+ ASSERT_TRUE(LeaseMgrFactory::instance().deleteLease(lease));
+
+ // Apply restrictions on the subnet1. This should be only assigned
+ // to clients belonging to cable-modem class.
+ subnet1_->allowClientClass("cable-modem");
+
+ // The allocation engine should determine that the subnet1 is not
+ // available for the client not belonging to the cable-modem class.
+ // Instead, it should assign an address from subnet2 that belongs
+ // to the same shared network.
+ ctx.subnet_ = subnet1_;
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_TRUE(subnet2_->inPool(Lease::TYPE_V4, lease->addr_));
+
+ // Remove the lease so as we can start over.
+ ASSERT_TRUE(LeaseMgrFactory::instance().deleteLease(lease));
+
+ // Assign cable-modem class and try again. This time, we should
+ // offer an address from the subnet1.
+ ctx.query_->addClass(ClientClass("cable-modem"));
+
+ ctx.subnet_ = subnet1_;
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_TRUE(subnet1_->inPool(Lease::TYPE_V4, lease->addr_));
+
+ // Let's now remove the client from the cable-modem class and try
+ // to renew the address. The engine should determine that the
+ // client doesn't have access to the subnet1 pools anymore and
+ // assign an address from unrestricted subnet.
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ ctx.subnet_ = subnet1_;
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_TRUE(subnet2_->inPool(Lease::TYPE_V4, lease->addr_));
+}
+
+// This test verifies that the server can assign an address from a
+// different subnet than orginally selected, when the address pool in
+// the first subnet requires another class.
+TEST_F(SharedNetworkAlloc4Test, requestSharedNetworkPoolClassification) {
+ // Try to offer address from subnet1. There is one address available
+ // so it should be offered.
+ AllocEngine::ClientContext4
+ ctx(subnet1_, ClientIdPtr(), hwaddr_, IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "host.example.com.", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ Lease4Ptr lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_TRUE(subnet1_->inPool(Lease::TYPE_V4, lease->addr_));
+
+ // Remove the lease so as we can start over.
+ ASSERT_TRUE(LeaseMgrFactory::instance().deleteLease(lease));
+
+ // Apply restrictions on the pool1. This should be only assigned
+ // to clients belonging to cable-modem class.
+ pool1_->allowClientClass("cable-modem");
+
+ // The allocation engine should determine that the pool1 is not
+ // available for the client not belonging to the cable-modem class.
+ // Instead, it should assign an address from subnet2 that belongs
+ // to the same shared network.
+ ctx.subnet_ = subnet1_;
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_TRUE(subnet2_->inPool(Lease::TYPE_V4, lease->addr_));
+
+ // Remove the lease so as we can start over.
+ ASSERT_TRUE(LeaseMgrFactory::instance().deleteLease(lease));
+
+ // Assign cable-modem class and try again. This time, we should
+ // offer an address from the pool1.
+ ctx.query_->addClass(ClientClass("cable-modem"));
+
+ // Restrict access to pool2 for this client, to make sure that the
+ // server doesn't accidentally get an address from this pool.
+ pool2_->allowClientClass("telephone");
+
+ ctx.subnet_ = subnet1_;
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_TRUE(subnet1_->inPool(Lease::TYPE_V4, lease->addr_));
+
+ // Let's now remove the client from the cable-modem class and try
+ // to renew the address. The engine should determine that the
+ // client doesn't have access to the pool1 anymore and
+ // assign an address from another pool.
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ ctx.query_->addClass(ClientClass("telephone"));
+ ctx.subnet_ = subnet1_;
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_TRUE(subnet2_->inPool(Lease::TYPE_V4, lease->addr_));
+}
+
+// Test that reservations within shared network take precedence over the
+// existing leases regardless in which subnet belonging to a shared network
+// reservations belong (DHCPREQUEST case).
+TEST_F(SharedNetworkAlloc4Test, requestSharedNetworkReservations) {
+
+ EXPECT_FALSE(HostMgr::instance().getDisableSingleQuery());
+
+ // Create reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet2_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("10.2.3.23")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Start allocation from subnet1. The engine should determine that the
+ // client has reservations in subnet2 and should rather assign reserved
+ // addresses.
+ AllocEngine::ClientContext4
+ ctx(subnet1_, ClientIdPtr(), hwaddr_, IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "host.example.com.", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx);
+ Lease4Ptr lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("10.2.3.23", lease->addr_.toText());
+
+ // Remove the lease for another test below.
+ ASSERT_TRUE(LeaseMgrFactory::instance().deleteLease(lease));
+
+ // Let's create a lease for the client to make sure the lease is not
+ // renewed but a reserved lease is allocated again.
+ Lease4Ptr lease2(new Lease4(IOAddress("192.0.2.17"), hwaddr_, ClientIdPtr(),
+ 501, time(NULL), subnet1_->getID()));
+ lease->cltt_ = time(NULL) - 10; // Allocated 10 seconds ago
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease2));
+ ctx.subnet_ = subnet1_;
+ ctx.hosts_.clear();
+ AllocEngine::findReservation(ctx);
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("10.2.3.23", lease->addr_.toText());
+}
+
+// Test that reservations within shared network take precedence over the
+// existing leases regardless in which subnet belonging to a shared network
+// reservations belong (DHCPREQUEST case). Host lookups returning a collection
+// are disabled. As it is only an optimization the behavior (so the test)
+// must stay unchanged.
+TEST_F(SharedNetworkAlloc4Test, requestSharedNetworkReservationsNoColl) {
+
+ // Disable host lookups returning a collection.
+ ASSERT_FALSE(HostMgr::instance().getDisableSingleQuery());
+ HostMgr::instance().setDisableSingleQuery(true);
+
+ // Create reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet2_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("10.2.3.23")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Start allocation from subnet1. The engine should determine that the
+ // client has reservations in subnet2 and should rather assign reserved
+ // addresses.
+ AllocEngine::ClientContext4
+ ctx(subnet1_, ClientIdPtr(), hwaddr_, IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "host.example.com.", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx);
+ Lease4Ptr lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("10.2.3.23", lease->addr_.toText());
+
+ // Remove the lease for another test below.
+ ASSERT_TRUE(LeaseMgrFactory::instance().deleteLease(lease));
+
+ // Let's create a lease for the client to make sure the lease is not
+ // renewed but a reserved lease is allocated again.
+ Lease4Ptr lease2(new Lease4(IOAddress("192.0.2.17"), hwaddr_, ClientIdPtr(),
+ 501, time(NULL), subnet1_->getID()));
+ lease->cltt_ = time(NULL) - 10; // Allocated 10 seconds ago
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease2));
+ ctx.subnet_ = subnet1_;
+ ctx.hosts_.clear();
+ AllocEngine::findReservation(ctx);
+ lease = engine_.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("10.2.3.23", lease->addr_.toText());
+}
+
+// This test checks if an expired lease can be reused in DHCPDISCOVER (fake
+// allocation)
+TEST_F(AllocEngine4Test, discoverReuseExpiredLease4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ IOAddress addr("192.0.2.15");
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ // Get rid of the default test configuration.
+ cfg_mgr.clear();
+
+ // Create configuration similar to other tests, but with a single address pool
+ subnet_ = Subnet4Ptr(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3));
+ pool_ = Pool4Ptr(new Pool4(addr, addr)); // just a single address
+ subnet_->addPool(pool_);
+ cfg_mgr.getStagingCfg()->getCfgSubnets4()->add(subnet_);
+
+ // Just a different hw/client-id for the second client
+ uint8_t hwaddr2_data[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe };
+ HWAddrPtr hwaddr2(new HWAddr(hwaddr2_data, sizeof(hwaddr2_data), HTYPE_ETHER));
+ uint8_t clientid2[] = { 8, 7, 6, 5, 4, 3, 2, 1 };
+ time_t now = time(NULL) - 500; // Allocated 500 seconds ago
+ Lease4Ptr lease(new Lease4(addr, hwaddr2, clientid2, sizeof(clientid2),
+ 495, now, subnet_->getID()));
+ // Copy the lease, so as it can be compared with the old lease returned
+ // by the allocation engine.
+ Lease4 original_lease(*lease);
+
+ // Lease was assigned 500 seconds ago, but its valid lifetime is 495, so it
+ // is expired already
+ ASSERT_TRUE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // CASE 1: Asking for any address
+ AllocEngine::ClientContext4 ctx1(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", true);
+ ctx1.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ lease = engine->allocateLease4(ctx1);
+ // Check that we got that single lease
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // We are reusing expired lease, the old (expired) instance should be
+ // returned. The returned instance should be the same as the original
+ // lease.
+ ASSERT_TRUE(ctx1.old_lease_);
+ EXPECT_TRUE(original_lease == *ctx1.old_lease_);
+
+ // Do all checks on the lease (if subnet-id, preferred/valid times are ok etc.)
+ checkLease4(lease);
+
+ // CASE 2: Asking specifically for this address
+ AllocEngine::ClientContext4 ctx2(subnet_, clientid_, hwaddr_,
+ IOAddress(addr), false, false,
+ "", true);
+ ctx2.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ lease = engine->allocateLease4(ctx2);
+ // Check that we got that single lease
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // We are updating expired lease. The copy of the old lease should be
+ // returned and it should be equal to the original lease.
+ ASSERT_TRUE(ctx2.old_lease_);
+ EXPECT_TRUE(*ctx2.old_lease_ == original_lease);
+}
+
+// This test checks if an expired lease can be reused in REQUEST (actual allocation)
+TEST_F(AllocEngine4Test, requestReuseExpiredLease4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ IOAddress addr("192.0.2.105");
+
+ EXPECT_TRUE(testStatistics("assigned-addresses", 0, subnet_->getID()));
+ int64_t cumulative = getStatistics("cumulative-assigned-addresses",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-addresses");
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 0));
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 0, subnet_->getID()));
+
+ // Just a different hw/client-id for the second client
+ uint8_t hwaddr2_data[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe };
+ HWAddrPtr hwaddr2(new HWAddr(hwaddr2_data, sizeof(hwaddr2_data), HTYPE_ETHER));
+ uint8_t clientid2[] = { 8, 7, 6, 5, 4, 3, 2, 1 };
+ time_t now = time(NULL) - 500; // Allocated 500 seconds ago
+
+ Lease4Ptr lease(new Lease4(addr, hwaddr2, clientid2, sizeof(clientid2),
+ 495, now, subnet_->getID()));
+ // Make a copy of the lease, so as we can compare that with the old lease
+ // instance returned by the allocation engine.
+ Lease4 original_lease(*lease);
+
+ // Lease was assigned 500 seconds ago, but its valid lifetime is 495, so it
+ // is expired already
+ ASSERT_TRUE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // A client comes along, asking specifically for this address
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress(addr), false, false,
+ "host.example.com.", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ lease = engine->allocateLease4(ctx);
+
+ // Check that he got that single lease
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // Check that the lease is indeed updated in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+
+ // The allocation engine should return a copy of the old lease. This
+ // lease should be equal to the original lease.
+ ASSERT_TRUE(ctx.old_lease_);
+ EXPECT_TRUE(*ctx.old_lease_ == original_lease);
+
+ // Check that the stats declined stats were modified correctly. Note, because
+ // added the lease directly, assigned-leases never bumped to one, so when we
+ // reclaim it gets decremented to -1, then on assignment back to 0.
+ EXPECT_TRUE(testStatistics("assigned-addresses", 0, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses", glbl_cumulative));
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 1));
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 1, subnet_->getID()));
+}
+
+// This test checks if an expired declined lease can be reused when responding
+// to DHCPDISCOVER (fake allocation)
+TEST_F(AllocEngine4Test, discoverReuseDeclinedLease4) {
+
+ AllocEnginePtr engine(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false));
+ ASSERT_TRUE(engine);
+
+ // Now prepare a configuration with single address pool.
+ IOAddress addr("192.0.2.15");
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ cfg_mgr.clear();
+ subnet_ = Subnet4Ptr(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3));
+ pool_ = Pool4Ptr(new Pool4(addr, addr)); // just a single address
+ subnet_->addPool(pool_);
+ cfg_mgr.getStagingCfg()->getCfgSubnets4()->add(subnet_);
+
+ // Now create a declined lease, decline it and rewind its cltt, so it
+ // is expired.
+ Lease4Ptr declined = generateDeclinedLease("192.0.2.15", 100, -10);
+
+ // CASE 1: Ask for any address
+ Lease4Ptr assigned;
+ testReuseLease4(engine, declined, "0.0.0.0", true, SHOULD_PASS, assigned);
+
+ // Check that we got that single lease
+ ASSERT_TRUE(assigned);
+ EXPECT_EQ(addr, assigned->addr_);
+
+ // CASE 2: Asking specifically for this address
+ testReuseLease4(engine, declined, "192.0.2.15", true, SHOULD_PASS, assigned);
+
+ // Check that we get it again
+ ASSERT_TRUE(assigned);
+ EXPECT_EQ(addr, assigned->addr_);
+}
+
+// This test checks if statistics are not updated when expired declined lease
+// is reused when responding to DHCPDISCOVER (fake allocation)
+TEST_F(AllocEngine4Test, discoverReuseDeclinedLease4Stats) {
+
+ // Now prepare for DISCOVER processing
+ AllocEnginePtr engine(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false));
+ ASSERT_TRUE(engine);
+
+ // Now prepare a configuration with single address pool.
+ IOAddress addr("192.0.2.15");
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ cfg_mgr.clear();
+ subnet_ = Subnet4Ptr(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3));
+ pool_ = Pool4Ptr(new Pool4(addr, addr)); // just a single address
+ subnet_->addPool(pool_);
+ cfg_mgr.getStagingCfg()->getCfgSubnets4()->add(subnet_);
+ cfg_mgr.commit(); // so we will recalc stats
+ int64_t cumulative = getStatistics("cumulative-assigned-addresses",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-addresses");
+
+ // Now create a declined lease, decline it and rewind its cltt, so it
+ // is expired.
+ Lease4Ptr declined = generateDeclinedLease("192.0.2.15", 100, -10);
+
+ // Ask for any address. There's only one address in the pool, so it doesn't
+ // matter much.
+ Lease4Ptr assigned;
+ testReuseLease4(engine, declined, "0.0.0.0", true, SHOULD_PASS, assigned);
+
+ // Check that the stats declined stats were not modified
+ EXPECT_TRUE(testStatistics("assigned-addresses", 0, subnet_->getID()));
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-addresses", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-addresses"));
+ EXPECT_TRUE(testStatistics("declined-addresses", 0));
+ EXPECT_TRUE(testStatistics("reclaimed-declined-addresses", 0));
+ EXPECT_TRUE(testStatistics("declined-addresses", 0, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("reclaimed-declined-addresses", 0, subnet_->getID()));
+}
+
+// This test checks if an expired declined lease can be reused when responding
+// to REQUEST (actual allocation)
+TEST_F(AllocEngine4Test, requestReuseDeclinedLease4) {
+
+ AllocEnginePtr engine(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false));
+ ASSERT_TRUE(engine);
+
+ // Now prepare a configuration with single address pool.
+ IOAddress addr("192.0.2.15");
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ cfg_mgr.clear();
+ subnet_ = Subnet4Ptr(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3));
+ pool_ = Pool4Ptr(new Pool4(addr, addr)); // just a single address
+ subnet_->addPool(pool_);
+ cfg_mgr.getStagingCfg()->getCfgSubnets4()->add(subnet_);
+
+ // Now create a declined lease, decline it and rewind its cltt, so it
+ // is expired.
+ Lease4Ptr declined = generateDeclinedLease("192.0.2.15", 100, -10);
+
+ // Asking specifically for this address
+ Lease4Ptr assigned;
+ testReuseLease4(engine, declined, "192.0.2.15", false, SHOULD_PASS, assigned);
+ // Check that we got it.
+ ASSERT_TRUE(assigned);
+ EXPECT_EQ(addr, assigned->addr_);
+
+ // Check that the lease is indeed updated in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(assigned, from_mgr);
+}
+
+// This test checks if statistics are not updated when expired declined lease
+// is reused when responding to DHCPREQUEST (actual allocation)
+TEST_F(AllocEngine4Test, requestReuseDeclinedLease4Stats) {
+
+ AllocEnginePtr engine(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false));
+ ASSERT_TRUE(engine);
+
+ // Now prepare a configuration with single address pool.
+ IOAddress addr("192.0.2.15");
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ cfg_mgr.clear();
+ subnet_ = Subnet4Ptr(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3));
+ pool_ = Pool4Ptr(new Pool4(addr, addr)); // just a single address
+ subnet_->addPool(pool_);
+ cfg_mgr.getStagingCfg()->getCfgSubnets4()->add(subnet_);
+ cfg_mgr.commit();
+ int64_t cumulative = getStatistics("cumulative-assigned-addresses",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-addresses");
+
+ // Now create a declined lease, decline it and rewind its cltt, so it
+ // is expired.
+ Lease4Ptr declined = generateDeclinedLease("192.0.2.15", 100, -10);
+
+ // Asking specifically for this address
+ Lease4Ptr assigned;
+ testReuseLease4(engine, declined, "192.0.2.15", false, SHOULD_PASS, assigned);
+ // Check that we got it.
+ ASSERT_TRUE(assigned);
+
+ // Check that the stats are correct. Note that assigned-addresses does
+ // not get decremented when a lease is declined, ergo not incremented
+ // when it is reused. Declined address stats will be -1 since
+ // lease was created as declined which does not increment the stat.
+ EXPECT_TRUE(testStatistics("assigned-addresses", 0, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses", glbl_cumulative));
+ EXPECT_TRUE(testStatistics("declined-addresses", -1));
+ EXPECT_TRUE(testStatistics("reclaimed-declined-addresses", 1));
+ EXPECT_TRUE(testStatistics("declined-addresses", -1, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("reclaimed-declined-addresses", 1, subnet_->getID()));
+}
+
+// This test checks that the Allocation Engine correctly identifies the
+// existing client's lease in the lease database, using the client
+// identifier and HW address.
+TEST_F(AllocEngine4Test, identifyClientLease) {
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.2.101"), hwaddr_, clientid_,
+ 100, time(NULL), subnet_->getID()));
+ LeaseMgrFactory::instance().addLease(lease);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "", true);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ Lease4Ptr identified_lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(identified_lease);
+ EXPECT_EQ("192.0.2.101", identified_lease->addr_.toText());
+
+ ctx.hwaddr_ = hwaddr2_;
+ ctx.clientid_ = clientid_;
+ identified_lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(identified_lease);
+ EXPECT_EQ("192.0.2.101", identified_lease->addr_.toText());
+
+ ctx.hwaddr_ = hwaddr_;
+ ctx.clientid_ = clientid2_;
+ identified_lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(identified_lease);
+ EXPECT_NE(identified_lease->addr_.toText(), "192.0.2.101");
+
+ ctx.hwaddr_ = hwaddr_;
+ ctx.clientid_.reset();
+ identified_lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(identified_lease);
+ EXPECT_EQ("192.0.2.101", identified_lease->addr_.toText());
+
+ ctx.hwaddr_ = hwaddr2_;
+ ctx.clientid_.reset();
+ identified_lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(identified_lease);
+ EXPECT_NE(identified_lease->addr_.toText(), "192.0.2.101");
+
+ lease->client_id_.reset();
+ ASSERT_NO_THROW(LeaseMgrFactory::instance().updateLease4(lease));
+
+ ctx.hwaddr_ = hwaddr_;
+ ctx.clientid_ = clientid_;
+ identified_lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(identified_lease);
+ EXPECT_EQ("192.0.2.101", identified_lease->addr_.toText());
+
+ ctx.hwaddr_ = hwaddr_;
+ ctx.clientid_.reset();
+ identified_lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(identified_lease);
+ EXPECT_EQ("192.0.2.101", identified_lease->addr_.toText());
+
+ ctx.hwaddr_ = hwaddr2_;
+ ctx.clientid_ = clientid_;
+ identified_lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(identified_lease);
+ EXPECT_NE(identified_lease->addr_.toText(), "192.0.2.101");
+}
+
+// This test checks that when the client requests the address which belongs
+// to another client, the allocation engine returns NULL (for the
+// DHCPREQUEST case) or a lease for the address which belongs to this
+// client (DHCPDISCOVER case).
+TEST_F(AllocEngine4Test, requestOtherClientLease) {
+ // Create the first lease.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.2.101"), hwaddr_,
+ &clientid_->getClientId()[0],
+ clientid_->getClientId().size(),
+ 100, time(NULL), subnet_->getID(),
+ false, false, ""));
+ // Create the second lease. Note that we use the same client id here and
+ // we expect that the allocation engine will figure out that the hardware
+ // address is different.
+ Lease4Ptr lease2(new Lease4(IOAddress("192.0.2.102"), hwaddr2_,
+ &clientid_->getClientId()[0],
+ clientid_->getClientId().size(),
+ 100, time(NULL), subnet_->getID(),
+ false, false, ""));
+ // Add leases for both clients to the Lease Manager.
+ LeaseMgrFactory::instance().addLease(lease);
+ LeaseMgrFactory::instance().addLease(lease2);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // First client requests the lease which belongs to the second client.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("192.0.2.102"),
+ false, false, "", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ Lease4Ptr new_lease = engine.allocateLease4(ctx);
+ // Allocation engine should return NULL.
+ ASSERT_FALSE(new_lease);
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes", 1));
+
+ // Now simulate the DHCPDISCOVER case when the provided address is
+ // treated as a hint. The engine should return a lease for a
+ // different address than requested.
+ ctx.fake_allocation_ = true;
+ new_lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(new_lease);
+ EXPECT_EQ("192.0.2.101", new_lease->addr_.toText());
+}
+
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has a reservation.
+// - Client sends DHCPREQUEST without requested IP Address, nor ciaddr.
+// - Client is allocated a reserved address.
+//
+// Note that client must normally include a requested IP address or ciaddr
+// in its message. But, we still want to provision clients that don't do that.
+// The server simply picks reserved address or any other available one if there
+// is no reservation.
+TEST_F(AllocEngine4Test, reservedAddressNoHint) {
+ // Create reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Try to allocate a lease without specifying a hint. This is actually
+ // incorrect behavior of the client to not send an address it wants to
+ // obtain but the server should handle this gracefully.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx);
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.2.123", lease->addr_.toText());
+
+ // Make sure that the lease has been committed to the lease database.
+ // And that the committed lease is equal to the one returned.
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+ detailCompareLease(lease, from_mgr);
+
+ // Initially, there was no lease for this client, so the returned old
+ // lease should be NULL.
+ ASSERT_FALSE(ctx.old_lease_);
+}
+
+// This test checks behavior of the allocation engine in the following scenario:
+// - Client has no lease in the database.
+// - Client has a reservation.
+// - Client sends DHCPDISCOVER without requested IP Address.
+// - Server returns DHCPOFFER with the reserved address.
+TEST_F(AllocEngine4Test,reservedAddressNoHintFakeAllocation) {
+ // Create reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Query allocation engine for the lease to be assigned to this
+ // client without specifying the address to be assigned.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ AllocEngine::findReservation(ctx);
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+
+ ASSERT_TRUE(lease);
+ // The allocation engine should return a reserved address.
+ EXPECT_EQ("192.0.2.123", lease->addr_.toText());
+
+ // This is a "fake" allocation so the returned lease should not be committed
+ // to the lease database.
+ EXPECT_FALSE(LeaseMgrFactory::instance().getLease4(lease->addr_));
+
+ // Client had no lease in the database, so the old lease returned should
+ // be NULL.
+ ASSERT_FALSE(ctx.old_lease_);
+}
+
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has a reservation.
+// - Client sends DHCPREQUEST with a requested IP address
+// - Server returns DHCPNAK when requested IP address is different than
+// the reserved address. Note that the allocation engine returns NULL
+// to indicate to the server that it should send DHCPNAK.
+// - Server allocates a reserved address to the client when the client requests
+// this address using requested IP address option.
+TEST_F(AllocEngine4Test, reservedAddressHint) {
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ AllocEngine::ClientContext4 ctx1(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.234"), false, false,
+ "", false);
+ ctx1.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx1);
+ Lease4Ptr lease = engine.allocateLease4(ctx1);
+
+ // The client requested a different address than reserved, so
+ // the allocation engine should return NULL lease. When the server
+ // receives a NULL lease for the client, it will send a DHCPNAK.
+ ASSERT_FALSE(lease);
+ ASSERT_FALSE(ctx1.old_lease_);
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes", 1));
+
+ // Now, request a correct address. The client should obtain it.
+ AllocEngine::ClientContext4 ctx2(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.123"), false, false,
+ "", false);
+ ctx2.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx2);
+ lease = engine.allocateLease4(ctx2);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.2.123", lease->addr_.toText());
+
+ // Make sure that the lease has been committed to the lease database.
+ // And that the committed lease is equal to the one returned.
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+ detailCompareLease(lease, from_mgr);
+
+ ASSERT_FALSE(ctx2.old_lease_);
+}
+
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has a reservation.
+// - Client sends DHCPDISCOVER with a requested IP address as a hint.
+// - Server offers a reserved address, even though it is different than the
+// requested address.
+TEST_F(AllocEngine4Test, reservedAddressHintFakeAllocation) {
+ // Create a reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Query the allocation engine for the lease to be assigned to the client
+ // and specify a hint being a different address than the reserved one.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.234"), false, false,
+ "", true);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx);
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+
+ ASSERT_TRUE(lease);
+ // Allocation engine should return reserved address.
+ EXPECT_EQ("192.0.2.123", lease->addr_.toText());
+
+ // This is a "fake" allocation so the returned lease should not be committed
+ // to the lease database.
+ EXPECT_FALSE(LeaseMgrFactory::instance().getLease4(lease->addr_));
+
+ ASSERT_FALSE(ctx.old_lease_);
+}
+
+// This test checks that the behavior of the allocation engine in the following
+// scenario:
+// - Client has a lease for the address from the dynamic pool in the database.
+// - Client has a reservation for a different address than the one for which
+// the client has a lease.
+// - Client sends DHCPREQUEST, asking for the reserved address (as it has been
+// offered to it when it sent DHCPDISCOVER).
+// - Server allocates a reserved address and removes the lease for the address
+// previously allocated to the client.
+TEST_F(AllocEngine4Test, reservedAddressExistingLease) {
+ // Create the reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Create a lease for the client with a different address than the reserved
+ // one.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.2.101"), hwaddr_,
+ &clientid_->getClientId()[0],
+ clientid_->getClientId().size(),
+ 100, time(NULL), subnet_->getID(),
+ false, false, ""));
+ LeaseMgrFactory::instance().addLease(lease);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Request allocation of the reserved address.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.123"), false, false,
+ "", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx);
+ Lease4Ptr allocated_lease = engine.allocateLease4(ctx);
+
+ ASSERT_TRUE(allocated_lease);
+ // The engine should have allocated the reserved address.
+ EXPECT_EQ("192.0.2.123", allocated_lease->addr_.toText());
+
+ // Make sure that the lease has been committed to the lease database.
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(allocated_lease->addr_);
+ ASSERT_TRUE(from_mgr);
+ detailCompareLease(allocated_lease, from_mgr);
+
+ // The previous lease should have been replaced by a new one. The previous
+ // lease should be returned by the allocation engine to the caller.
+ ASSERT_TRUE(ctx.old_lease_);
+ EXPECT_EQ("192.0.2.101", ctx.old_lease_->addr_.toText());
+ detailCompareLease(ctx.old_lease_, lease);
+}
+
+// This test checks that the behavior of the allocation engine in the following
+// scenario:
+// - Client A has a lease in the database.
+// - Client B has a reservation for the address in use by client A.
+// - Client B sends a DHCPREQUEST requesting the allocation of the reserved
+// lease (in use by client A).
+// - Server determines that the reserved address is in use by a different client
+// and returns DHCPNAK to client B.
+TEST_F(AllocEngine4Test, reservedAddressHijacked) {
+ // Create host reservation for the client B.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Allocate a lease for the client A for the same address as reserved
+ // for the client B.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.2.123"), hwaddr2_, 0, 0,
+ 100, time(NULL), subnet_->getID(),
+ false, false, ""));
+ LeaseMgrFactory::instance().addLease(lease);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Try to allocate the reserved lease to client B.
+ AllocEngine::ClientContext4 ctx1(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.123"), false, false,
+ "", false);
+ ctx1.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx1);
+ Lease4Ptr allocated_lease = engine.allocateLease4(ctx1);
+ // The lease is allocated to someone else, so the allocation should not
+ // succeed.
+ ASSERT_FALSE(allocated_lease);
+ ASSERT_FALSE(ctx1.old_lease_);
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes", 1));
+
+ // Make sure that the allocation engine didn't modify the lease of the
+ // client A.
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+ detailCompareLease(lease, from_mgr);
+
+ // Try doing the same thing, but this time do not request any specific
+ // address. It should have the same effect.
+ AllocEngine::ClientContext4 ctx2(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx2.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx2);
+ allocated_lease = engine.allocateLease4(ctx2);
+ ASSERT_FALSE(allocated_lease);
+ ASSERT_FALSE(ctx2.old_lease_);
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes", 1));
+
+ from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+ detailCompareLease(lease, from_mgr);
+}
+
+// This test checks that the behavior of the allocation engine in the following
+// scenario:
+// - Client A has a lease in the database.
+// - Client B has a reservation for the address in use by client A.
+// - Client B sends a DHCPDISCOVER.
+// - Server determines that the reserved address is in use by a different client
+// so it offers an address from the dynamic pool.
+TEST_F(AllocEngine4Test, reservedAddressHijackedFakeAllocation) {
+ // Create a reservation for the client B.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Create a lease for the client A.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.2.123"), hwaddr2_, 0, 0,
+ 100, time(NULL), subnet_->getID(),
+ false, false, ""));
+ LeaseMgrFactory::instance().addLease(lease);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Query allocation engine for the lease to be allocated to the client B.
+ // The allocation engine is not able to allocate the lease to the client
+ // B, because the address is in use by client A.
+ AllocEngine::ClientContext4 ctx1(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.123"), false, false,
+ "", true);
+ ctx1.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx1);
+ Lease4Ptr allocated_lease = engine.allocateLease4(ctx1);
+
+ // The allocation engine should return a lease but for a different address
+ // than requested because this address is in use.
+ ASSERT_TRUE(allocated_lease);
+ ASSERT_FALSE(ctx1.old_lease_);
+ EXPECT_NE(allocated_lease->addr_.toText(), "192.0.2.123");
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, allocated_lease->addr_));
+
+ // Do the same test. But, this time do not specify any address to be
+ // allocated.
+ AllocEngine::ClientContext4 ctx2(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", true);
+ ctx2.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx2);
+ allocated_lease = engine.allocateLease4(ctx2);
+
+ ASSERT_TRUE(allocated_lease);
+ EXPECT_NE(allocated_lease->addr_.toText(), "192.0.2.123");
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, allocated_lease->addr_));
+ ASSERT_FALSE(ctx2.old_lease_);
+}
+
+// This test checks that the behavior of the allocation engine in the following
+// scenario:
+// - Client has a reservation.
+// - Client has a lease in the database for a different address than reserved.
+// - Client sends a DHCPREQUEST and asks for a different address than reserved,
+// and different than it has in a database.
+// - Server doesn't allocate the reserved address to the client because the
+// client asked for the different address.
+//
+// Note that in this case the client should get the DHCPNAK and should fall back
+// to the DHCPDISCOVER.
+TEST_F(AllocEngine4Test, reservedAddressExistingLeaseInvalidHint) {
+ // Create a reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Create a lease for the client for a different address than reserved.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.2.101"), hwaddr_, ClientIdPtr(),
+ 100, time(NULL), subnet_->getID(),
+ false, false, ""));
+ LeaseMgrFactory::instance().addLease(lease);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Try to allocate a lease and specify a different address than reserved
+ // and different from the one that client is currently using.
+ AllocEngine::ClientContext4 ctx1(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.102"), false, false,
+ "", false);
+ ctx1.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx1);
+ Lease4Ptr allocated_lease = engine.allocateLease4(ctx1);
+ ASSERT_FALSE(allocated_lease);
+ ASSERT_FALSE(ctx1.old_lease_);
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes", 1));
+
+ // Repeat the test, but this time ask for the address that the client
+ // has allocated.
+ AllocEngine::ClientContext4 ctx2(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.101"), false, false,
+ "", false);
+ ctx2.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx2);
+ allocated_lease = engine.allocateLease4(ctx2);
+ // The client has reservation so the server wants to allocate a
+ // reserved address and doesn't want to renew the address that the
+ // client is currently using. This is equivalent of the case when
+ // the client tries to renew the lease but there is a new reservation
+ // for this client. The server doesn't allow for the renewal and
+ // responds with DHCPNAK to force the client to return to the
+ // DHCP server discovery.
+ ASSERT_FALSE(allocated_lease);
+ ASSERT_FALSE(ctx2.old_lease_);
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes", 1));
+}
+
+// This test checks that the behavior of the allocation engine in the following
+// scenario:
+// - Client has a lease in the database.
+// - Client has a reservation for a different address than the one for which it
+// has a lease.
+// - Client sends a DHCPDISCOVER and asks for a different address than reserved
+// and different from which it has a lease for.
+// - Server ignores the client's hint and offers a reserved address.
+TEST_F(AllocEngine4Test, reservedAddressExistingLeaseFakeAllocation) {
+ // Create a reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Create a lease for a different address than reserved.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.2.101"), hwaddr_,
+ &clientid_->getClientId()[0],
+ clientid_->getClientId().size(),
+ 100, time(NULL), subnet_->getID(),
+ false, false, ""));
+ LeaseMgrFactory::instance().addLease(lease);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Try to allocate a lease and use a completely different address
+ // as a hint.
+ AllocEngine::ClientContext4 ctx1(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.102"), false, false,
+ "", true);
+ ctx1.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ AllocEngine::findReservation(ctx1);
+ Lease4Ptr allocated_lease = engine.allocateLease4(ctx1);
+
+ // Server should offer a lease for a reserved address.
+ ASSERT_TRUE(allocated_lease);
+ EXPECT_EQ("192.0.2.123", allocated_lease->addr_.toText());
+
+ // The lease should not be allocated until the client sends a DHCPREQUEST.
+ EXPECT_FALSE(LeaseMgrFactory::instance().getLease4(allocated_lease->addr_));
+
+ // Old lease should contain the currently used lease.
+ ASSERT_TRUE(ctx1.old_lease_);
+ EXPECT_EQ("192.0.2.101", ctx1.old_lease_->addr_.toText());
+
+ // Repeat the test but this time ask for the address for which the
+ // client has a lease.
+ AllocEngine::ClientContext4 ctx2(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.101"), false, false,
+ "", true);
+ ctx2.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ AllocEngine::findReservation(ctx2);
+ allocated_lease = engine.allocateLease4(ctx2);
+
+ // The server should offer the lease, but not for the address that
+ // the client requested. The server should offer a reserved address.
+ ASSERT_TRUE(allocated_lease);
+ EXPECT_EQ("192.0.2.123", allocated_lease->addr_.toText());
+
+ // Old lease should contain the currently used lease.
+ ASSERT_TRUE(ctx2.old_lease_);
+ EXPECT_EQ("192.0.2.101", ctx2.old_lease_->addr_.toText());
+}
+
+// This test checks that the behavior of the allocation engine in the following
+// scenario:
+// - Client has a reservation.
+// - Client has a lease for a different address than reserved.
+// - Client sends a DHCPREQUEST to allocate a lease.
+// - The server determines that the client has a reservation for the
+// different address than it is currently using and should assign
+// a reserved address and remove the previous lease.
+TEST_F(AllocEngine4Test, reservedAddressExistingLeaseNoHint) {
+ // Create a reservation.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Create a lease for a different address than reserved.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.2.101"), hwaddr_,
+ &clientid_->getClientId()[0],
+ clientid_->getClientId().size(),
+ 100, time(NULL), subnet_->getID(),
+ false, false, ""));
+ LeaseMgrFactory::instance().addLease(lease);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Try to allocate a lease with providing no hint.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx);
+ Lease4Ptr allocated_lease = engine.allocateLease4(ctx);
+
+ // The reserved address should be allocated.
+ ASSERT_TRUE(allocated_lease);
+ EXPECT_EQ("192.0.2.123", allocated_lease->addr_.toText());
+
+ // The previous lease should be removed.
+ EXPECT_FALSE(LeaseMgrFactory::instance().getLease4(lease->addr_));
+
+ // Make sure that the allocated lease is committed in the lease database.
+ Lease4Ptr from_mgr =
+ LeaseMgrFactory::instance().getLease4(allocated_lease->addr_);
+ ASSERT_TRUE(from_mgr);
+ detailCompareLease(allocated_lease, from_mgr);
+
+ // Old lease should be returned.
+ ASSERT_TRUE(ctx.old_lease_);
+ detailCompareLease(lease, ctx.old_lease_);
+}
+
+// This test checks that the behavior of the allocation engine in the following
+// scenario:
+// - Client has a reservation.
+// - Client has a lease for a different address than reserved.
+// - Client sends a DHCPDISCOVER with no hint.
+// - Server determines that there is a reservation for the client and that
+// the reserved address should be offered when the client sends a
+// DHCPDISCOVER.
+TEST_F(AllocEngine4Test, reservedAddressExistingLeaseNoHintFakeAllocation) {
+ // Create a reservation.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Create a lease for a different address than reserved.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.2.101"), hwaddr_,
+ &clientid_->getClientId()[0],
+ clientid_->getClientId().size(),
+ 100, time(NULL), subnet_->getID(),
+ false, false, ""));
+ LeaseMgrFactory::instance().addLease(lease);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Query the allocation engine for the lease to be allocated for the
+ // client.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ AllocEngine::findReservation(ctx);
+ Lease4Ptr allocated_lease = engine.allocateLease4(ctx);
+
+ // The server should offer the reserved address.
+ ASSERT_TRUE(allocated_lease);
+ EXPECT_EQ("192.0.2.123", allocated_lease->addr_.toText());
+
+ // The lease should not be committed to the lease database until the
+ // client sends a DHCPREQUEST.
+ EXPECT_FALSE(LeaseMgrFactory::instance().getLease4(allocated_lease->addr_));
+
+ // The old lease should reflect what is in the database.
+ ASSERT_TRUE(ctx.old_lease_);
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+ detailCompareLease(lease, from_mgr);
+}
+
+// This test checks that the behavior of the allocation engine in the following
+// scenario:
+// - Client A has a lease for the address.
+// - Client B has a reservation for the same address that the Client A is using.
+// - Client B requests allocation of the reserved address.
+// - Server returns DHCPNAK to the client to indicate that the requested address
+// can't be allocated.
+// - Client A renews the lease.
+// - Server determines that the lease that the Client A is trying to renew
+// is for the address reserved for Client B. Therefore, the server returns
+// DHCPNAK to force the client to return to the server discovery.
+// - The Client A sends DHCPDISCOVER.
+// - The server offers an address to the Client A, which is different than
+// the address reserved for Client B.
+// - The Client A requests allocation of the offered address.
+// - The server allocates the new address to Client A.
+// - The Client B sends DHCPDISCOVER to the server.
+// - The server offers a reserved address to the Client B.
+// - The Client B requests the offered address.
+// - The server allocates the reserved address to the Client B.
+TEST_F(AllocEngine4Test, reservedAddressConflictResolution) {
+ // Create a reservation for client B.
+ HostPtr host(new Host(&hwaddr2_->hwaddr_[0], hwaddr2_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.101")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Create a lease for Client A.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.2.101"), hwaddr_,
+ &clientid_->getClientId()[0],
+ clientid_->getClientId().size(),
+ 100, time(NULL), subnet_->getID(),
+ false, false, ""));
+ LeaseMgrFactory::instance().addLease(lease);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Client B sends a DHCPREQUEST to allocate a reserved lease. The
+ // allocation engine can't allocate a reserved lease for this client
+ // because this specific address is in use by the Client A.
+ AllocEngine::ClientContext4 ctx1(subnet_, ClientIdPtr(), hwaddr2_,
+ IOAddress("192.0.2.101"), false, false,
+ "", false);
+ ctx1.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx1);
+ Lease4Ptr offered_lease = engine.allocateLease4(ctx1);
+ ASSERT_FALSE(offered_lease);
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes", 1));
+
+ // Client A tries to renew the lease. The renewal should fail because
+ // server detects that Client A doesn't have reservation for this
+ // address.
+ AllocEngine::ClientContext4 ctx2(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.101"), false, false,
+ "", false);
+ ctx2.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx2);
+ ASSERT_FALSE(engine.allocateLease4(ctx2));
+ ASSERT_FALSE(ctx2.old_lease_);
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes", 1));
+
+ // Client A returns to DHCPDISCOVER and should be offered a lease.
+ // The offered lease address must be different than the one the
+ // Client B has reservation for.
+ AllocEngine::ClientContext4 ctx3(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.101"), false, false,
+ "", true);
+ ctx3.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx3);
+ offered_lease = engine.allocateLease4(ctx3);
+ ASSERT_TRUE(offered_lease);
+ EXPECT_NE(offered_lease->addr_.toText(), "192.0.2.101");
+
+ // Client A tries to acquire the lease. It should succeed. At this point
+ // the previous lease should be released and become available for the
+ // Client B.
+ AllocEngine::ClientContext4 ctx4(subnet_, clientid_, hwaddr_,
+ offered_lease->addr_, false, false,
+ "", false);
+ ctx4.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx4);
+ Lease4Ptr allocated_lease = engine.allocateLease4(ctx4);
+
+ ASSERT_TRUE(allocated_lease);
+ EXPECT_NE(allocated_lease->addr_.toText(), "192.0.2.101");
+
+ // Client B tries to get the lease again. It should be offered
+ // a reserved lease.
+ AllocEngine::ClientContext4 ctx5(subnet_, ClientIdPtr(), hwaddr2_,
+ IOAddress("0.0.0.0"), false, false,
+ "", true);
+ ctx5.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx5);
+ offered_lease = engine.allocateLease4(ctx5);
+
+ ASSERT_TRUE(offered_lease);
+ EXPECT_EQ("192.0.2.101", offered_lease->addr_.toText());
+
+ // Client B requests allocation of the lease and it should succeed.
+ AllocEngine::ClientContext4 ctx6(subnet_, ClientIdPtr(), hwaddr2_,
+ offered_lease->addr_, false, false,
+ "", false);
+ ctx6.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ allocated_lease = engine.allocateLease4(ctx6);
+
+ ASSERT_TRUE(allocated_lease);
+ EXPECT_EQ("192.0.2.101", allocated_lease->addr_.toText());
+}
+
+// This test checks that the address is not assigned from the dynamic
+// pool if it has been reserved for another client.
+TEST_F(AllocEngine4Test, reservedAddressVsDynamicPool) {
+ // Create a reservation for the client.
+ HostPtr host(new Host(&hwaddr2_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.100")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Different client tries to allocate a lease. Note, that we're using
+ // an iterative allocator which would pick the first address from the
+ // dynamic pool, i.e. 192.0.2.100. This address is reserved so we expect
+ // that a different address will be allocated.
+ AllocEngine::ClientContext4 ctx(subnet_, ClientIdPtr(), hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx);
+ Lease4Ptr allocated_lease = engine.allocateLease4(ctx);
+
+ ASSERT_TRUE(allocated_lease);
+ EXPECT_NE(allocated_lease->addr_.toText(), "192.0.2.100");
+
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(allocated_lease->addr_);
+ ASSERT_TRUE(from_mgr);
+ detailCompareLease(allocated_lease, from_mgr);
+}
+
+// This test checks that the client requesting an address which is
+// reserved for another client will get no lease or a different
+// address will be assigned if the client is sending a DHCPDISCOVER.
+TEST_F(AllocEngine4Test, reservedAddressHintUsedByOtherClient) {
+ // Create a reservation for the client.
+ HostPtr host(new Host(&hwaddr2_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.100")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Different client is requesting this address.
+ AllocEngine::ClientContext4 ctx1(subnet_, ClientIdPtr(), hwaddr_,
+ IOAddress("192.0.2.100"), false, false,
+ "", false);
+ ctx1.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx1);
+ Lease4Ptr allocated_lease = engine.allocateLease4(ctx1);
+
+ // The client should get no lease (DHCPNAK).
+ ASSERT_FALSE(allocated_lease);
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes", 1));
+
+ // The same client should get a different lease than requested if
+ // if is sending a DHCPDISCOVER (fake allocation is true).
+ AllocEngine::ClientContext4 ctx2(subnet_, ClientIdPtr(), hwaddr_,
+ IOAddress("192.0.2.100"), false, false,
+ "", true);
+ ctx2.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx2);
+ allocated_lease = engine.allocateLease4(ctx2);
+
+ ASSERT_TRUE(allocated_lease);
+ // Make sure the lease obtained is for a different address.
+ EXPECT_NE(allocated_lease->addr_.toText(), "192.0.2.100");
+}
+
+// This test checks that the allocation engine refuses to allocate an
+// address when the pool is exhausted, and the only available
+// address is reserved for a different client.
+TEST_F(AllocEngine4Test, reservedAddressShortPool) {
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Create short pool with only one address.
+ initSubnet(IOAddress("192.0.2.100"), IOAddress("192.0.2.100"));
+ // Reserve the address for a different client.
+ HostPtr host(new Host(&hwaddr2_->hwaddr_[0], hwaddr2_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.100")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Allocation engine should determine that the available address is
+ // reserved for someone else and not allocate it.
+ AllocEngine::ClientContext4 ctx1(subnet_, ClientIdPtr(), hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx1.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx1);
+ Lease4Ptr allocated_lease = engine.allocateLease4(ctx1);
+
+ ASSERT_FALSE(allocated_lease);
+
+ EXPECT_EQ(1, getStatistics("v4-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network"));
+ EXPECT_EQ(1, getStatistics("v4-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes"));
+
+ EXPECT_EQ(1, getStatistics("v4-allocation-fail", 2));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-shared-network", 2));
+ EXPECT_EQ(1, getStatistics("v4-allocation-fail-subnet", 2));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-no-pools", 2));
+ EXPECT_EQ(0, getStatistics("v4-allocation-fail-classes", 2));
+
+ // Now, let's remove the reservation.
+ initSubnet(IOAddress("192.0.2.100"), IOAddress("192.0.2.100"));
+ CfgMgr::instance().commit();
+
+ // Address should be successfully allocated.
+ AllocEngine::ClientContext4 ctx2(subnet_, ClientIdPtr(), hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx2.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx2);
+ allocated_lease = engine.allocateLease4(ctx2);
+
+ ASSERT_TRUE(allocated_lease);
+ EXPECT_EQ("192.0.2.100", allocated_lease->addr_.toText());
+}
+
+// This test checks that the AllocEngine allocates an address from the
+// dynamic pool if the client's reservation is made for a hostname but
+// not for an address.
+TEST_F(AllocEngine4Test, reservedHostname) {
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Create a reservation for a hostname. Address is set to 0 which
+ // indicates that there is no reservation.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress::IPV4_ZERO_ADDRESS(),
+ "foo.example.org"));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Try to allocate a lease.
+ AllocEngine::ClientContext4 ctx(subnet_, ClientIdPtr(), hwaddr_,
+ IOAddress("192.0.2.109"), false, false,
+ "foo.example.org", true);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx);
+ Lease4Ptr allocated_lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(allocated_lease);
+ ASSERT_FALSE(allocated_lease->addr_.isV4Zero());
+ ASSERT_EQ("192.0.2.109", allocated_lease->addr_.toText());
+
+ ctx.requested_address_ = allocated_lease->addr_;
+ ctx.fake_allocation_ = false;
+ allocated_lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(allocated_lease);
+ EXPECT_EQ("192.0.2.109", allocated_lease->addr_.toText());
+}
+
+// This test checks that the AllocEngine::findReservation method finds
+// and returns host reservation for the DHCPv4 client using the data from
+// the client context. If the host reservation can't be found, it sets
+// the value of NULL in the host_ field of the client context.
+TEST_F(AllocEngine4Test, findReservation) {
+ // Create the instance of the allocation engine.
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Context is required to call the AllocEngine::findReservation.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ ctx.addHostIdentifier(Host::IDENT_HWADDR, hwaddr_->hwaddr_);
+ ctx.addHostIdentifier(Host::IDENT_DUID, clientid_->getDuid());
+
+ // There is no reservation in the database so no host should be returned.
+ ASSERT_NO_THROW(engine.findReservation(ctx));
+ EXPECT_FALSE(ctx.currentHost());
+
+ // Create a reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.100")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // This time the reservation should be returned.
+ ctx.hosts_.clear();
+ ASSERT_NO_THROW(engine.findReservation(ctx));
+ EXPECT_TRUE(ctx.currentHost());
+ EXPECT_EQ(ctx.currentHost()->getIPv4Reservation(), host->getIPv4Reservation());
+
+ // It shouldn't be returned when reservations-in-subnet is disabled.
+ subnet_->setReservationsInSubnet(false);
+ ctx.hosts_.clear();
+ ASSERT_NO_THROW(engine.findReservation(ctx));
+ EXPECT_FALSE(ctx.currentHost());
+
+ // Check the reservations-in-subnet and reservations-out-of-pool flags.
+ subnet_->setReservationsInSubnet(true);
+ subnet_->setReservationsOutOfPool(true);
+ ctx.hosts_.clear();
+ ASSERT_NO_THROW(engine.findReservation(ctx));
+ EXPECT_TRUE(ctx.currentHost());
+ EXPECT_EQ(ctx.currentHost()->getIPv4Reservation(), host->getIPv4Reservation());
+
+ // This time use the client identifier to search for the host.
+ host.reset(new Host(&clientid_->getClientId()[0],
+ clientid_->getClientId().size(),
+ Host::IDENT_DUID, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.101")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ ctx.hosts_.clear();
+ ASSERT_NO_THROW(engine.findReservation(ctx));
+ EXPECT_TRUE(ctx.currentHost());
+ EXPECT_EQ(ctx.currentHost()->getIPv4Reservation(), host->getIPv4Reservation());
+
+ // Remove the subnet. Subnet id is required to find host reservations, so
+ // if it is set to NULL, no reservation should be returned
+ ctx.subnet_.reset();
+ ctx.hosts_.clear();
+ ASSERT_NO_THROW(engine.findReservation(ctx));
+ EXPECT_FALSE(ctx.currentHost());
+
+ // The same if there is a mismatch of the subnet id between the reservation
+ // and the context.
+ ctx.subnet_ = subnet_;
+ host.reset(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID() + 1,
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.100")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ ctx.hosts_.clear();
+ ASSERT_NO_THROW(engine.findReservation(ctx));
+ EXPECT_FALSE(ctx.currentHost());
+}
+
+// This test checks if the simple IPv4 allocation can succeed and that
+// statistic for allocated addresses is increased appropriately.
+TEST_F(AllocEngine4Test, simpleAlloc4Stats) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Let's pretend 100 addresses were allocated already
+ string name = StatsMgr::generateName("subnet", subnet_->getID(),
+ "assigned-addresses");
+ StatsMgr::instance().addValue(name, static_cast<int64_t>(100));
+ int64_t cumulative = getStatistics("cumulative-assigned-addresses",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-addresses");
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // The statistic should be there and it should be increased by 1 (to 101).
+ ObservationPtr stat = StatsMgr::instance().getObservation(name);
+ ASSERT_TRUE(stat);
+ EXPECT_EQ(101, stat->getInteger().first);
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses", glbl_cumulative));
+}
+
+// This test checks if the fake allocation (for DHCPDISCOVER) can succeed
+// and that it doesn't increase allocated-addresses statistic.
+TEST_F(AllocEngine4Test, fakeAlloc4Stat) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, true,
+ "host.example.com.", true);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Let's pretend 100 addresses were allocated already
+ string name = StatsMgr::generateName("subnet", subnet_->getID(),
+ "assigned-addresses");
+ StatsMgr::instance().addValue(name, static_cast<int64_t>(100));
+ int64_t cumulative = getStatistics("cumulative-assigned-addresses",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-addresses");
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // The statistic should be there and it should not be increased
+ // (should be still equal to 100).
+ ObservationPtr stat = StatsMgr::instance().getObservation(name);
+ ASSERT_TRUE(stat);
+ EXPECT_EQ(100, stat->getInteger().first);
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-addresses", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-addresses"));
+}
+
+// This test checks that the allocated-addresses statistic is decreased when
+// the client has a lease and a reservation for a different address is
+// available.
+TEST_F(AllocEngine4Test, reservedAddressExistingLeaseStat) {
+ // Create the reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ // Create a lease for the client with a different address than the reserved
+ // one.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.2.101"), hwaddr_,
+ &clientid_->getClientId()[0],
+ clientid_->getClientId().size(),
+ 100, time(NULL), subnet_->getID(),
+ false, false, ""));
+ LeaseMgrFactory::instance().addLease(lease);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Let's pretend 100 addresses were allocated already
+ string name = StatsMgr::generateName("subnet", subnet_->getID(),
+ "assigned-addresses");
+ StatsMgr::instance().addValue(name, static_cast<int64_t>(100));
+ int64_t cumulative = getStatistics("cumulative-assigned-addresses",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-addresses");
+
+ // Request allocation of the reserved address.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("192.0.2.123"), false, false,
+ "", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ AllocEngine::findReservation(ctx);
+
+ Lease4Ptr allocated_lease = engine.allocateLease4(ctx);
+
+ ASSERT_TRUE(allocated_lease);
+
+ // The statistic should be still at 100. Note that it was decreased
+ // (because old lease was removed), but also increased (because the
+ // new lease was immediately allocated).
+ ObservationPtr stat = StatsMgr::instance().getObservation(name);
+ ASSERT_TRUE(stat);
+ EXPECT_EQ(100, stat->getInteger().first);
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-addresses", glbl_cumulative));
+
+ // Lets' double check that the actual allocation took place.
+ EXPECT_FALSE(ctx.fake_allocation_);
+}
+
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has a global reservation.
+// - Client sends DISCOVER
+// - Client is allocated the reserved address.
+// - Lease is not added to the lease database
+TEST_F(AllocEngine4Test, globalReservationReservedAddressDiscover) {
+ // Create reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, SUBNET_ID_GLOBAL,
+ SUBNET_ID_UNUSED, IOAddress("192.0.77.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ subnet_->setReservationsGlobal(true);
+
+ // Query allocation engine for the lease to be assigned to this
+ // client without specifying the address to be assigned.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Look up the host.
+ AllocEngine::findReservation(ctx);
+
+ // We should have the correct current host
+ EXPECT_TRUE(ctx.currentHost());
+ EXPECT_EQ(ctx.currentHost()->getHostname(), host->getHostname());
+ EXPECT_EQ(ctx.currentHost()->getIPv4Reservation(), host->getIPv4Reservation());
+
+ // We should allocate the reserved address.
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.77.123", lease->addr_.toText());
+
+ // This is a "fake" allocation so the returned lease should not be committed
+ // to the lease database.
+ EXPECT_FALSE(LeaseMgrFactory::instance().getLease4(lease->addr_));
+
+ // Client had no lease in the database, so the old lease returned should
+ // be NULL.
+ ASSERT_FALSE(ctx.old_lease_);
+}
+
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has a global reservation.
+// - Client sends REQUEST
+// - Client is allocated the reserved address.
+// - Lease is added to the lease database
+TEST_F(AllocEngine4Test, globalReservationReservedAddressRequest) {
+ // Create reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, SUBNET_ID_GLOBAL,
+ SUBNET_ID_UNUSED, IOAddress("192.0.77.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ subnet_->setReservationsGlobal(true);
+
+ // Query allocation engine for the lease to be assigned to this
+ // client without specifying the address to be assigned.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Look up the host.
+ AllocEngine::findReservation(ctx);
+
+ // We should have the correct current host
+ EXPECT_TRUE(ctx.currentHost());
+ EXPECT_EQ(ctx.currentHost()->getHostname(), host->getHostname());
+ EXPECT_EQ(ctx.currentHost()->getIPv4Reservation(), host->getIPv4Reservation());
+
+ // We should allocate the reserved address.
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.77.123", lease->addr_.toText());
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+
+ // Client had no lease in the database, so the old lease returned should
+ // be NULL.
+ ASSERT_FALSE(ctx.old_lease_);
+}
+
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has a global reservation.
+// - Client sends DISCOVER
+// - Client is allocated a dynamic address from matched subnet
+// - Lease is not added to the lease database
+TEST_F(AllocEngine4Test, globalReservationDynamicDiscover) {
+ // Create reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, SUBNET_ID_GLOBAL,
+ SUBNET_ID_UNUSED, IOAddress::IPV4_ZERO_ADDRESS(),
+ "foo.example.org"));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ subnet_->setReservationsGlobal(true);
+
+ // Query allocation engine for the lease to be assigned to this
+ // client without specifying the address to be assigned.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Look up the host.
+ AllocEngine::findReservation(ctx);
+
+ // We should have the correct current host
+ EXPECT_TRUE(ctx.currentHost());
+ EXPECT_EQ(ctx.currentHost()->getHostname(), host->getHostname());
+ EXPECT_EQ(ctx.currentHost()->getIPv4Reservation(), host->getIPv4Reservation());
+
+ // We should allocate a dynamic address.
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.2.100", lease->addr_.toText());
+
+ // This is a "fake" allocation so the returned lease should not be committed
+ // to the lease database.
+ EXPECT_FALSE(LeaseMgrFactory::instance().getLease4(lease->addr_));
+
+ // Client had no lease in the database, so the old lease returned should
+ // be NULL.
+ ASSERT_FALSE(ctx.old_lease_);
+}
+
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has a global reservation.
+// - Client sends REQUEST
+// - Client is allocated a dynamic address from matched subnet
+// - Lease is added to the lease database
+TEST_F(AllocEngine4Test, globalReservationDynamicRequest) {
+ // Create reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, SUBNET_ID_GLOBAL,
+ SUBNET_ID_UNUSED, IOAddress::IPV4_ZERO_ADDRESS(),
+ "foo.example.org"));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ subnet_->setReservationsGlobal(true);
+
+ // Query allocation engine for the lease to be assigned to this
+ // client without specifying the address to be assigned.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Look up the host.
+ AllocEngine::findReservation(ctx);
+
+ // We should have the correct current host
+ EXPECT_TRUE(ctx.currentHost());
+ EXPECT_EQ(ctx.currentHost()->getHostname(), host->getHostname());
+ EXPECT_EQ(ctx.currentHost()->getIPv4Reservation(), host->getIPv4Reservation());
+
+ // We should allocate a dynamic address.
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.2.100", lease->addr_.toText());
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+
+ // Client had no lease in the database, so the old lease returned should
+ // be NULL.
+ ASSERT_FALSE(ctx.old_lease_);
+}
+
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has a subnet reservation.
+// - Client sends DISCOVER
+// - Client is allocated the reserved address.
+// - Lease is not added to the lease database
+TEST_F(AllocEngine4Test, mixedReservationReservedAddressDiscover) {
+ // Create reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123"),
+ "foo.example.org"));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ subnet_->setReservationsGlobal(true);
+ subnet_->setReservationsInSubnet(true);
+
+ // Query allocation engine for the lease to be assigned to this
+ // client without specifying the address to be assigned.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Look up the host.
+ AllocEngine::findReservation(ctx);
+
+ // We should have the correct current host
+ EXPECT_TRUE(ctx.currentHost());
+ EXPECT_EQ(ctx.currentHost()->getHostname(), host->getHostname());
+ EXPECT_EQ(ctx.currentHost()->getIPv4Reservation(), host->getIPv4Reservation());
+
+ // We should allocate the reserved address.
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.2.123", lease->addr_.toText());
+
+ // This is a "fake" allocation so the returned lease should not be committed
+ // to the lease database.
+ EXPECT_FALSE(LeaseMgrFactory::instance().getLease4(lease->addr_));
+
+ // Client had no lease in the database, so the old lease returned should
+ // be NULL.
+ ASSERT_FALSE(ctx.old_lease_);
+}
+
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has a subnet reservation.
+// - Client sends REQUEST
+// - Client is allocated the reserved address.
+// - Lease is added to the lease database
+TEST_F(AllocEngine4Test, mixedReservationReservedAddressRequest) {
+ // Create reservation for the client.
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123"),
+ "foo.example.org"));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ subnet_->setReservationsGlobal(true);
+ subnet_->setReservationsInSubnet(true);
+
+ // Query allocation engine for the lease to be assigned to this
+ // client without specifying the address to be assigned.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Look up the host.
+ AllocEngine::findReservation(ctx);
+
+ // We should have the correct current host
+ EXPECT_TRUE(ctx.currentHost());
+ EXPECT_EQ(ctx.currentHost()->getHostname(), host->getHostname());
+ EXPECT_EQ(ctx.currentHost()->getIPv4Reservation(), host->getIPv4Reservation());
+
+ // We should allocate the reserved address.
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.2.123", lease->addr_.toText());
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+
+ // Client had no lease in the database, so the old lease returned should
+ // be NULL.
+ ASSERT_FALSE(ctx.old_lease_);
+}
+
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has a global and a subnet reservation.
+// - Client sends DISCOVER
+// - Client is allocated the reserved address.
+// - Lease is not added to the lease database
+TEST_F(AllocEngine4Test, bothReservationReservedAddressDiscover) {
+ // Create reservations for the client.
+ HostPtr ghost(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, SUBNET_ID_GLOBAL,
+ SUBNET_ID_UNUSED, IOAddress("192.0.77.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(ghost);
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123"),
+ "foo.example.org"));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ subnet_->setReservationsGlobal(true);
+ subnet_->setReservationsInSubnet(true);
+
+ // Query allocation engine for the lease to be assigned to this
+ // client without specifying the address to be assigned.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", true);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Look up the host.
+ AllocEngine::findReservation(ctx);
+
+ // We should have the correct current host
+ EXPECT_TRUE(ctx.currentHost());
+ EXPECT_EQ(ctx.currentHost()->getHostname(), host->getHostname());
+ EXPECT_EQ(ctx.currentHost()->getIPv4Reservation(), host->getIPv4Reservation());
+
+ // We should allocate the reserved address.
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.2.123", lease->addr_.toText());
+
+ // This is a "fake" allocation so the returned lease should not be committed
+ // to the lease database.
+ EXPECT_FALSE(LeaseMgrFactory::instance().getLease4(lease->addr_));
+
+ // Client had no lease in the database, so the old lease returned should
+ // be NULL.
+ ASSERT_FALSE(ctx.old_lease_);
+}
+
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has a global and a subnet reservation.
+// - Client sends REQUEST
+// - Client is allocated the reserved address.
+// - Lease is added to the lease database
+TEST_F(AllocEngine4Test, bothReservationReservedAddressRequest) {
+ // Create reservations for the client.
+ HostPtr ghost(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, SUBNET_ID_GLOBAL,
+ SUBNET_ID_UNUSED, IOAddress("192.0.77.123")));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(ghost);
+ HostPtr host(new Host(&hwaddr_->hwaddr_[0], hwaddr_->hwaddr_.size(),
+ Host::IDENT_HWADDR, subnet_->getID(),
+ SUBNET_ID_UNUSED, IOAddress("192.0.2.123"),
+ "foo.example.org"));
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ subnet_->setReservationsGlobal(true);
+ subnet_->setReservationsInSubnet(true);
+
+ // Query allocation engine for the lease to be assigned to this
+ // client without specifying the address to be assigned.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Look up the host.
+ AllocEngine::findReservation(ctx);
+
+ // We should have the correct current host
+ EXPECT_TRUE(ctx.currentHost());
+ EXPECT_EQ(ctx.currentHost()->getHostname(), host->getHostname());
+ EXPECT_EQ(ctx.currentHost()->getIPv4Reservation(), host->getIPv4Reservation());
+
+ // We should allocate the reserved address.
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.2.123", lease->addr_.toText());
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+
+ // Client had no lease in the database, so the old lease returned should
+ // be NULL.
+ ASSERT_FALSE(ctx.old_lease_);
+}
+
+// Exercises AllocEnginer4Test::updateExtendedInfo4() through various
+// permutations of client packet content.
+TEST_F(AllocEngine4Test, updateExtendedInfo4) {
+
+ // Structure that defines a test scenario.
+ struct Scenario {
+ std::string description_; // test description
+ std::string orig_context_json_; // user context the lease begins with
+ std::string rai_data_; // RAI option the client packet contains
+ std::string exp_context_json_; // expected user context on the lease
+ bool exp_ret; // expected returned value
+ };
+
+ // Test scenarios.
+ std::vector<Scenario> scenarios {
+ {
+ "no context, no rai",
+ "",
+ "",
+ "",
+ false
+ },
+ {
+ "some original context, no rai",
+ "{\"foo\": 123}",
+ "",
+ "{\"foo\": 123}",
+ false
+ },
+ {
+ "no original context, rai",
+ "",
+ "0x52050104aabbccdd",
+ "{ \"ISC\": { \"relay-agent-info\": \"0x52050104AABBCCDD\" } }",
+ true
+ },
+ {
+ "some original context, rai",
+ "{\"foo\": 123}",
+ "0x52050104aabbccdd",
+ "{ \"ISC\": { \"relay-agent-info\": \"0x52050104AABBCCDD\" }, \"foo\": 123 }",
+ true
+ },
+ {
+ "original rai context, no rai",
+ "{ \"ISC\": { \"relay-agent-info\": \"0x52050104AABBCCDD\" } }",
+ "",
+ "{ \"ISC\": { \"relay-agent-info\": \"0x52050104AABBCCDD\" } }",
+ false
+ },
+ {
+ "original rai context, different rai",
+ "{ \"ISC\": { \"relay-agent-info\": \"0x52050104AABBCCDD\" } }",
+ "0x52050104ddeeffaa",
+ "{ \"ISC\": { \"relay-agent-info\": \"0x52050104DDEEFFAA\" } }",
+ true
+ }};
+
+ // Create the allocation engine, context and lease.
+ NakedAllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "", true);
+
+ // All scenarios require storage to be enabled.
+ ctx.subnet_->setStoreExtendedInfo(true);
+
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("192.0.2.100", lease->addr_.toText());
+
+ // Verify that the lease begins with no user context.
+ ConstElementPtr user_context = lease->getContext();
+ ASSERT_FALSE(user_context);
+
+ // Iterate over the test scenarios.
+ ElementPtr orig_context;
+ ElementPtr exp_context;
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+
+ // Create the original user context from JSON.
+ if (scenario.orig_context_json_.empty()) {
+ orig_context.reset();
+ } else {
+ ASSERT_NO_THROW(orig_context = Element::fromJSON(scenario.orig_context_json_))
+ << "invalid orig_context_json_, test is broken";
+ }
+
+ // Create the expected user context from JSON.
+ if (scenario.exp_context_json_.empty()) {
+ exp_context.reset();
+ } else {
+ ASSERT_NO_THROW(exp_context = Element::fromJSON(scenario.exp_context_json_))
+ << "invalid exp_context_json_, test is broken";
+ }
+
+ // Initialize lease's user context.
+ lease->setContext(orig_context);
+ if (!orig_context) {
+ ASSERT_FALSE(lease->getContext());
+ } else {
+ ASSERT_TRUE(lease->getContext());
+ ASSERT_TRUE(orig_context->equals(*(lease->getContext())));
+ }
+
+ // Create the client packet and the add RAI option (if one).
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ if (!scenario.rai_data_.empty()) {
+ std::vector<uint8_t> opt_data;
+ ASSERT_NO_THROW(util::str::decodeFormattedHexString(scenario.rai_data_, opt_data))
+ << "scenario.rai_data_ is invalid, test is broken";
+ OptionPtr rai;
+ ASSERT_NO_THROW(rai.reset(new Option(Option::V4, 0x52, opt_data)))
+ << "could not create rai option, test is broken";
+
+ ctx.query_->addOption(rai);
+ }
+
+ // Call AllocEngine::updateLease4ExtendeInfo().
+ bool ret = false;
+ ASSERT_NO_THROW_LOG(ret = engine.callUpdateLease4ExtendedInfo(lease, ctx));
+ ASSERT_EQ(scenario.exp_ret, ret);
+
+ // Verify the lease has the expected user context content.
+ if (!exp_context) {
+ ASSERT_FALSE(lease->getContext());
+ } else {
+ ASSERT_TRUE(lease->getContext());
+ ASSERT_TRUE(exp_context->equals(*(lease->getContext())))
+ << "expected: " << *(exp_context) << std::endl
+ << " actual: " << *(lease->getContext()) << std::endl;
+ }
+ }
+}
+
+// Verifies that the extended data (e.g. RAI option for now) is
+// added to a V4 lease when leases are created and/or renewed,
+// when store-extended-info is true.
+TEST_F(AllocEngine4Test, storeExtendedInfoEnabled4) {
+
+ // Structure that defines a test scenario.
+ struct Scenario {
+ std::string description_; // test description
+ std::vector<uint8_t> mac_; // MAC address
+ std::string rai_data_; // RAI option the client packet contains
+ std::string exp_context_json_; // expected user context on the lease
+ std::string exp_address_; // expected lease address
+ };
+
+ std::vector<uint8_t> mac1 = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0x01 };
+ std::string mac1_addr = "192.0.2.100";
+
+ std::vector<uint8_t> mac2 = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0x02 };
+ std::string mac2_addr = "192.0.2.101";
+
+ // Test scenarios.
+ std::vector<Scenario> scenarios {
+ {
+ "create client one without rai",
+ mac1,
+ "",
+ "",
+ mac1_addr
+ },
+ {
+ "renew client one without rai",
+ {},
+ "",
+ "",
+ mac1_addr
+ },
+ {
+ "create client two with rai",
+ mac2,
+ "0x52050104a1b1c1d1",
+ "{ \"ISC\": { \"relay-agent-info\": \"0x52050104A1B1C1D1\" } }",
+ mac2_addr
+ },
+ {
+ "renew client two without rai",
+ {},
+ "",
+ "{ \"ISC\": { \"relay-agent-info\": \"0x52050104A1B1C1D1\" } }",
+ mac2_addr
+ }};
+
+ // Create the allocation engine, context and lease.
+ NakedAllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // All of the scenarios require storage to be enabled.
+ subnet_->setStoreExtendedInfo(true);
+
+ AllocEngine::ClientContext4 ctx(subnet_, ClientIdPtr(), hwaddr_,
+ IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "", false);
+
+ // Iterate over the test scenarios.
+ Lease4Ptr lease;
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+
+ ElementPtr exp_context;
+ // Create the expected user context from JSON.
+ if (!scenario.exp_context_json_.empty()) {
+ ASSERT_NO_THROW(exp_context = Element::fromJSON(scenario.exp_context_json_))
+ << "invalid exp_context_json_, test is broken";
+ }
+
+ // If we have a MAC address this scenario is for a new client.
+ if (!scenario.mac_.empty()) {
+ std::cout << "setting mac address" << std::endl;
+ ASSERT_NO_THROW(ctx.hwaddr_.reset(new HWAddr(scenario.mac_, HTYPE_ETHER)))
+ << "invalid MAC address, test is broken";
+ }
+
+ // Create the client packet and the add RAI option (if one).
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ if (!scenario.rai_data_.empty()) {
+ std::vector<uint8_t> opt_data;
+ ASSERT_NO_THROW(util::str::decodeFormattedHexString(scenario.rai_data_, opt_data))
+ << "scenario.rai_data_ is invalid, test is broken";
+ OptionPtr rai;
+ ASSERT_NO_THROW(rai.reset(new Option(Option::V4, 0x52, opt_data)))
+ << "could not create rai option, test is broken";
+
+ ctx.query_->addOption(rai);
+ }
+
+ // Create or renew the lease.
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(scenario.exp_address_, lease->addr_.toText());
+
+ // Verify the lease has the expected user context content.
+ if (!exp_context) {
+ ASSERT_FALSE(lease->getContext());
+ } else {
+ ASSERT_TRUE(lease->getContext());
+ ASSERT_TRUE(exp_context->equals(*(lease->getContext())))
+ << "expected: " << *(exp_context) << std::endl
+ << " actual: " << *(lease->getContext()) << std::endl;
+ }
+ }
+}
+
+// Verifies that the extended data (e.g. RAI option for now) is
+// not added to a V4 lease when leases are created and/or renewed,
+// when store-extended-info is false.
+TEST_F(AllocEngine4Test, storeExtendedInfoDisabled4) {
+
+ // Structure that defines a test scenario.
+ struct Scenario {
+ std::string description_; // test description
+ std::vector<uint8_t> mac_; // MAC address
+ std::string rai_data_; // RAI option the client packet contains
+ std::string exp_address_; // expected lease address
+ };
+
+ std::vector<uint8_t> mac1 = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0x01 };
+ std::string mac1_addr = "192.0.2.100";
+
+ std::vector<uint8_t> mac2 = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0x02 };
+ std::string mac2_addr = "192.0.2.101";
+
+ // Test scenarios.
+ std::vector<Scenario> scenarios {
+ {
+ "create client one without rai",
+ mac1,
+ "",
+ mac1_addr
+ },
+ {
+ "renew client one without rai",
+ {},
+ "",
+ mac1_addr
+ },
+ {
+ "create client two with rai",
+ mac2,
+ "0x52050104a1b1c1d1",
+ mac2_addr
+ },
+ {
+ "renew client two with rai",
+ {},
+ "0x52050104a1b1c1d1",
+ mac2_addr
+ }};
+
+ // Create the allocation engine, context and lease.
+ NakedAllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // All of the scenarios require storage to be disabled.
+ subnet_->setStoreExtendedInfo(false);
+
+ AllocEngine::ClientContext4 ctx(subnet_, ClientIdPtr(), hwaddr_,
+ IOAddress::IPV4_ZERO_ADDRESS(),
+ false, false, "", false);
+
+ Lease4Ptr lease;
+
+ // Iterate over the test scenarios.
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+
+ // If we have a MAC address this scenario is for a new client.
+ if (!scenario.mac_.empty()) {
+ std::cout << "setting mac address" << std::endl;
+ ASSERT_NO_THROW(ctx.hwaddr_.reset(new HWAddr(scenario.mac_, HTYPE_ETHER)))
+ << "invalid MAC address, test is broken";
+ }
+
+ // Create the client packet and the add RAI option (if one).
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ if (!scenario.rai_data_.empty()) {
+ std::vector<uint8_t> opt_data;
+ ASSERT_NO_THROW(util::str::decodeFormattedHexString(scenario.rai_data_, opt_data))
+ << "scenario.rai_data_ is invalid, test is broken";
+ OptionPtr rai;
+ ASSERT_NO_THROW(rai.reset(new Option(Option::V4, 0x52, opt_data)))
+ << "could not create rai option, test is broken";
+
+ ctx.query_->addOption(rai);
+ }
+
+ // Create or renew the lease.
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(scenario.exp_address_, lease->addr_.toText());
+
+ // Verify the lease does not have user context content.
+ ASSERT_FALSE(lease->getContext());
+ }
+}
+
+// This test checks if a lease can be reused in DHCPDISCOVER (fake allocation)
+// using cache threshold.
+TEST_F(AllocEngine4Test, discoverCacheThreshold4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Set valid lifetime to 500.
+ uint32_t valid = 500;
+ subnet_->setValid(valid);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ IOAddress addr("192.0.2.105");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease4Ptr lease(new Lease4(addr, hwaddr_, clientid_,
+ valid, now, subnet_->getID()));
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for fake allocation.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, addr,
+ false, false, "", true);
+
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ lease = engine->allocateLease4(ctx);
+ // Check that we got that single lease.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // The lease was reused.
+ time_t age = lease->cltt_ - now;
+ EXPECT_GE(age, 100);
+ EXPECT_LE(age, 110);
+ EXPECT_EQ(valid - age, lease->reuseable_valid_lft_);
+
+ // Check other lease parameters.
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_TRUE(*lease->client_id_ == *clientid_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(*lease->hwaddr_ == *hwaddr_);
+}
+
+// This test checks if a lease can be reused in DHCPREQUEST (real allocation)
+// using cache threshold.
+TEST_F(AllocEngine4Test, requestCacheThreshold4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Set valid lifetime to 500.
+ uint32_t valid = 500;
+ subnet_->setValid(valid);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ IOAddress addr("192.0.2.105");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease4Ptr lease(new Lease4(addr, hwaddr_, clientid_,
+ valid, now, subnet_->getID()));
+ ASSERT_FALSE(lease->expired());
+ // Copy the lease, so as it can be compared with.
+ Lease4Ptr original_lease(new Lease4(*lease));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for real allocation.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, addr,
+ false, false, "", false);
+
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ lease = engine->allocateLease4(ctx);
+ // Check that we got that single lease.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // The lease was reused.
+ time_t age = lease->cltt_ - now;
+ EXPECT_GE(age, 100);
+ EXPECT_LE(age, 110);
+ EXPECT_EQ(valid - age, lease->reuseable_valid_lft_);
+
+ // Check other lease parameters.
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_TRUE(*lease->client_id_ == *clientid_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(*lease->hwaddr_ == *hwaddr_);
+
+ // Check the lease was not updated in the database.
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(original_lease, from_mgr);
+}
+
+/// We proved that there is no different from the "cache" feature between
+/// discovers and request at the exception of the lease database update.
+
+// This test checks if a lease can be reused in DHCPDISCOVER (fake allocation)
+// using cache max age.
+TEST_F(AllocEngine4Test, discoverCacheMaxAge4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Set valid lifetime to 500.
+ uint32_t valid = 500;
+ subnet_->setValid(valid);
+
+ // Set the max age to 200.
+ subnet_->setCacheMaxAge(200);
+
+ IOAddress addr("192.0.2.105");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease4Ptr lease(new Lease4(addr, hwaddr_, clientid_,
+ valid, now, subnet_->getID()));
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for fake allocation.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, addr,
+ false, false, "", true);
+
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ lease = engine->allocateLease4(ctx);
+ // Check that we got that single lease.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // The lease was reused.
+ time_t age = lease->cltt_ - now;
+ EXPECT_GE(age, 100);
+ EXPECT_LE(age, 110);
+ EXPECT_EQ(valid - age, lease->reuseable_valid_lft_);
+
+ // Check other lease parameters.
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_TRUE(*lease->client_id_ == *clientid_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(*lease->hwaddr_ == *hwaddr_);
+}
+
+// This test checks if a lease can be reused in DHCPREQUEST (real allocation)
+// using both cache threshold and max age.
+TEST_F(AllocEngine4Test, requestCacheBoth4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Set valid lifetime to 500.
+ uint32_t valid = 500;
+ subnet_->setValid(valid);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ // Set the max age to 200.
+ subnet_->setCacheMaxAge(200);
+
+ IOAddress addr("192.0.2.105");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease4Ptr lease(new Lease4(addr, hwaddr_, clientid_,
+ valid, now, subnet_->getID()));
+ ASSERT_FALSE(lease->expired());
+ // Copy the lease, so as it can be compared with.
+ Lease4Ptr original_lease(new Lease4(*lease));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for real allocation.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, addr,
+ false, false, "", false);
+
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ lease = engine->allocateLease4(ctx);
+ // Check that we got that single lease.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // The lease was reused.
+ time_t age = lease->cltt_ - now;
+ EXPECT_GE(age, 100);
+ EXPECT_LE(age, 110);
+ EXPECT_EQ(valid - age, lease->reuseable_valid_lft_);
+
+ // Check other lease parameters.
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_TRUE(*lease->client_id_ == *clientid_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(*lease->hwaddr_ == *hwaddr_);
+
+ // Check the lease was not updated in the database.
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(original_lease, from_mgr);
+}
+
+// This test checks if a lease can't be reused in DHCPDISCOVER (fake allocation)
+// using too small cache threshold.
+TEST_F(AllocEngine4Test, discoverCacheBadThreshold4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Set valid lifetime to 500.
+ uint32_t valid = 500;
+ subnet_->setValid(valid);
+
+ // Set the threshold to 10%.
+ subnet_->setCacheThreshold(.10);
+
+ IOAddress addr("192.0.2.105");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease4Ptr lease(new Lease4(addr, hwaddr_, clientid_,
+ valid, now, subnet_->getID()));
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for fake allocation.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, addr,
+ false, false, "", true);
+
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ lease = engine->allocateLease4(ctx);
+ // Check that we got that single lease.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+}
+
+// This test checks if a lease can't be reused in DHCPREQUEST (real allocation)
+// using too small cache max age.
+TEST_F(AllocEngine4Test, requestCacheBadMaxAge4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Set valid lifetime to 500.
+ uint32_t valid = 500;
+ subnet_->setValid(valid);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ // Set the max age to 50.
+ subnet_->setCacheMaxAge(50);
+
+ IOAddress addr("192.0.2.105");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease4Ptr lease(new Lease4(addr, hwaddr_, clientid_,
+ valid, now, subnet_->getID()));
+ ASSERT_FALSE(lease->expired());
+
+ // Create a context for real allocation.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, addr,
+ false, false, "", false);
+
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ lease = engine->allocateLease4(ctx);
+ // Check that we got that single lease.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+
+ // Check the lease was updated in the database.
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// This test checks if a lease can't be reused in DHCPDISCOVER (fake allocation)
+// when the valid lifetime was reduced.
+TEST_F(AllocEngine4Test, discoverCacheReducedValid4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Set valid lifetime to 200.
+ subnet_->setValid(200);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ IOAddress addr("192.0.2.105");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ uint32_t valid = 500; // Used a value greater than subnet_->getValid().
+ Lease4Ptr lease(new Lease4(addr, hwaddr_, clientid_,
+ valid, now, subnet_->getID()));
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for fake allocation.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, addr,
+ false, false, "", true);
+
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ lease = engine->allocateLease4(ctx);
+ // Check that we got that single lease.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+}
+
+// This test checks if a lease can't be reused in DHCPREQUEST (real allocation)
+// when DDNS parameter changed.
+TEST_F(AllocEngine4Test, requestCacheFwdDDNS4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Set valid lifetime to 500.
+ uint32_t valid = 500;
+ subnet_->setValid(valid);
+
+ // Set the max age to 200.
+ subnet_->setCacheMaxAge(200);
+
+ IOAddress addr("192.0.2.105");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease4Ptr lease(new Lease4(addr, hwaddr_, clientid_,
+ valid, now, subnet_->getID()));
+ ASSERT_FALSE(lease->expired());
+
+ // Create a context for real allocation with fwd_dns_update changed.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, addr,
+ true, false, "", false);
+
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ lease = engine->allocateLease4(ctx);
+ // Check that we got that single lease.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+
+ // Check the lease was updated in the database.
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// This test checks if a lease can't be reused in DHCPDISCOVER (fake allocation)
+// when DDNS parameter changed.
+TEST_F(AllocEngine4Test, discoverCacheRevDDNS4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Set valid lifetime to 500.
+ uint32_t valid = 500;
+ subnet_->setValid(valid);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ // Set the max age to 200.
+ subnet_->setCacheMaxAge(200);
+
+ IOAddress addr("192.0.2.105");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease4Ptr lease(new Lease4(addr, hwaddr_, clientid_,
+ valid, now, subnet_->getID()));
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for fake allocation with rev_dns_update changed.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, addr,
+ false, true, "", true);
+
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ lease = engine->allocateLease4(ctx);
+ // Check that we got that single lease.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+}
+
+// This test checks if a lease can't be reused in DHCPREQUEST (real allocation)
+// when hostname changed.
+TEST_F(AllocEngine4Test, requestCacheHostname4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ // Set valid lifetime to 500.
+ uint32_t valid = 500;
+ subnet_->setValid(valid);
+
+ // Set the max age to 200.
+ subnet_->setCacheMaxAge(200);
+
+ IOAddress addr("192.0.2.105");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease4Ptr lease(new Lease4(addr, hwaddr_, clientid_,
+ valid, now, subnet_->getID(),
+ false, false, "foo"));
+ ASSERT_FALSE(lease->expired());
+
+ // Create a context for real allocation with fwd_dns_update changed.
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, addr,
+ false, false, "bar", false);
+
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ lease = engine->allocateLease4(ctx);
+ // Check that we got that single lease.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+ EXPECT_EQ("bar", lease->hostname_);
+
+ // Check the lease was updated in the database.
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// Verifies that AllocEngine::getValidLft(ctx4) returns the appropriate
+// lifetime value based on the context content.
+TEST_F(AllocEngine4Test, getValidLft4) {
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0, false);
+
+ // Let's make three classes, two with valid-lifetime and one without,
+ // and add them to the dictionary.
+ ClientClassDictionaryPtr dictionary = CfgMgr::instance().getStagingCfg()->getClientClassDictionary();
+
+ ClientClassDefPtr class_def(new ClientClassDef("valid_one", ExpressionPtr()));
+ Triplet<uint32_t> valid_one(50, 100, 150);
+ class_def->setValid(valid_one);
+ dictionary->addClass(class_def);
+
+ class_def.reset(new ClientClassDef("valid_two", ExpressionPtr()));
+ Triplet<uint32_t>valid_two(200, 250, 300);
+ class_def->setValid(valid_two);
+ dictionary->addClass(class_def);
+
+ class_def.reset(new ClientClassDef("valid_unspec", ExpressionPtr()));
+ dictionary->addClass(class_def);
+
+ // Commit our class changes.
+ CfgMgr::instance().commit();
+
+ // Update the subnet's triplet to something more useful.
+ subnet_->setValid(Triplet<uint32_t>(500, 1000, 1500));
+
+ // Describes a test scenario.
+ struct Scenario {
+ std::string desc_; // descriptive text for logging
+ std::vector<std::string> classes_; // class list of assigned classes
+ uint32_t requested_lft_; // use as option 51 is > 0
+ uint32_t exp_valid_; // expected lifetime
+ };
+
+ // Scenarios to test.
+ std::vector<Scenario> scenarios = {
+ {
+ "BOOTP",
+ { "BOOTP" },
+ 0,
+ Lease::INFINITY_LFT
+ },
+ {
+ "no classes, no option",
+ {},
+ 0,
+ subnet_->getValid()
+ },
+ {
+ "no classes, option",
+ {},
+ subnet_->getValid().getMin() + 50,
+ subnet_->getValid().getMin() + 50
+ },
+ {
+ "no classes, option too small",
+ {},
+ subnet_->getValid().getMin() - 50,
+ subnet_->getValid().getMin()
+ },
+ {
+ "no classes, option too big",
+ {},
+ subnet_->getValid().getMax() + 50,
+ subnet_->getValid().getMax()
+ },
+ {
+ "class unspecified, no option",
+ { "valid_unspec" },
+ 0,
+ subnet_->getValid()
+ },
+ {
+ "from last class, no option",
+ { "valid_unspec", "valid_one" },
+ 0,
+ valid_one.get()
+ },
+ {
+ "from first class, no option",
+ { "valid_two", "valid_one" },
+ 0,
+ valid_two.get()
+ },
+ {
+ "class plus option",
+ { "valid_one" },
+ valid_one.getMin() + 25,
+ valid_one.getMin() + 25
+ },
+ {
+ "class plus option too small",
+ { "valid_one" },
+ valid_one.getMin() - 25,
+ valid_one.getMin()
+ },
+ {
+ "class plus option too big",
+ { "valid_one" },
+ valid_one.getMax() + 25,
+ valid_one.getMax()
+ }
+ };
+
+ // Iterate over the scenarios and verify the correct outcome.
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.desc_); {
+ // Create a context;
+ AllocEngine::ClientContext4 ctx(subnet_, ClientIdPtr(), hwaddr_,
+ IOAddress("0.0.0.0"), false, false,
+ "", false);
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Add client classes (if any)
+ for (auto class_name : scenario.classes_) {
+ ctx.query_->addClass(class_name);
+ }
+
+ // Add client option (if one)
+ if (scenario.requested_lft_) {
+ OptionUint32Ptr opt(new OptionUint32(Option::V4, DHO_DHCP_LEASE_TIME,
+ scenario.requested_lft_));
+ ctx.query_->addOption(opt);
+ }
+
+ Lease4Ptr lease = engine.allocateLease4(ctx);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(lease->valid_lft_, scenario.exp_valid_);
+ }
+ }
+}
+
+// This test checks that deleteRelease handles BOOTP leases.
+TEST_F(AllocEngine4Test, bootpDelete) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Make the query a BOOTP one.
+ ctx.query_->addClass("BOOTP");
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Check that is belongs to the right subnet and client.
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+ EXPECT_TRUE(subnet_->inRange(lease->addr_));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, lease->addr_));
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_TRUE(*lease->client_id_ == *clientid_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(*lease->hwaddr_ == *hwaddr_);
+
+ // Check the valid lifetime is infinite.
+ uint32_t infinity_lft = Lease::INFINITY_LFT;
+ EXPECT_EQ(infinity_lft, lease->valid_lft_);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now delete it.
+ bool deleted = false;
+ ASSERT_NO_THROW(deleted = LeaseMgrFactory::instance().deleteLease(lease));
+ EXPECT_TRUE(deleted);
+ from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ EXPECT_FALSE(from_mgr);
+}
+
+#ifdef HAVE_MYSQL
+/// @brief Extension of the fixture class to use the MySQL backend.
+class MySqlAllocEngine4Test : public AllocEngine4Test {
+public:
+ /// @brief Constructor.
+ MySqlAllocEngine4Test() {
+ // Ensure we have the proper schema with no transient data.
+ db::test::createMySQLSchema();
+ factory_.create(db::test::validMySQLConnectionString());
+ }
+
+ /// @brief Destructor.
+ ~MySqlAllocEngine4Test() {
+ // If data wipe enabled, delete transient data otherwise destroy
+ // the schema.
+ db::test::destroyMySQLSchema();
+ LeaseMgrFactory::destroy();
+ }
+};
+
+// This test checks that simple allocation handles BOOTP queries.
+TEST_F(MySqlAllocEngine4Test, bootpAlloc4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Make the query a BOOTP one.
+ ctx.query_->addClass("BOOTP");
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Check that is belongs to the right subnet and client.
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+ EXPECT_TRUE(subnet_->inRange(lease->addr_));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, lease->addr_));
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_TRUE(*lease->client_id_ == *clientid_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(*lease->hwaddr_ == *hwaddr_);
+
+ // Check the valid lifetime is infinite.
+ uint32_t infinity_lft = Lease::INFINITY_LFT;
+ EXPECT_EQ(infinity_lft, lease->valid_lft_);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+ // The MySQL database does not keep the hwtype for DHCPv4 leases.
+ from_mgr->hwaddr_->htype_ = HTYPE_FDDI;
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+}
+
+// This test checks simple renewal handles BOOTP queries.
+TEST_F(MySqlAllocEngine4Test, bootpRenew4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Make the query a BOOTP one.
+ ctx.query_->addClass("BOOTP");
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // Check that we got a lease.
+ ASSERT_TRUE(lease);
+
+ // Check that is belongs to the right subnet and client.
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+ EXPECT_TRUE(subnet_->inRange(lease->addr_));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, lease->addr_));
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_TRUE(*lease->client_id_ == *clientid_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(*lease->hwaddr_ == *hwaddr_);
+
+ // Check the valid lifetime is infinite.
+ uint32_t infinity_lft = Lease::INFINITY_LFT;
+ EXPECT_EQ(infinity_lft, lease->valid_lft_);
+
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Do it again, this should amount to the renew of an existing lease
+ Lease4Ptr lease2 = engine->allocateLease4(ctx);
+
+ // Check that we got a lease.
+ ASSERT_TRUE(lease2);
+
+ // Check that is belongs to the right subnet and client.
+ EXPECT_EQ(lease2->subnet_id_, subnet_->getID());
+ EXPECT_TRUE(subnet_->inRange(lease2->addr_));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, lease2->addr_));
+ ASSERT_TRUE(lease2->client_id_);
+ EXPECT_TRUE(*lease2->client_id_ == *clientid_);
+ ASSERT_TRUE(lease2->hwaddr_);
+ EXPECT_TRUE(*lease2->hwaddr_ == *hwaddr_);
+
+ // Lease already existed, so old_lease should be set.
+ EXPECT_TRUE(ctx.old_lease_);
+
+ // Check the renewed valid lifetime has the max value.
+ EXPECT_EQ(infinity_lft, lease2->valid_lft_);
+}
+
+// This test checks that deleteRelease handles BOOTP leases.
+TEST_F(MySqlAllocEngine4Test, bootpDelete) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Make the query a BOOTP one.
+ ctx.query_->addClass("BOOTP");
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Check that is belongs to the right subnet and client.
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+ EXPECT_TRUE(subnet_->inRange(lease->addr_));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, lease->addr_));
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_TRUE(*lease->client_id_ == *clientid_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(*lease->hwaddr_ == *hwaddr_);
+
+ // Check the valid lifetime is infinite.
+ uint32_t infinity_lft = Lease::INFINITY_LFT;
+ EXPECT_EQ(infinity_lft, lease->valid_lft_);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now delete it.
+ bool deleted = false;
+ ASSERT_NO_THROW(deleted = LeaseMgrFactory::instance().deleteLease(lease));
+ EXPECT_TRUE(deleted);
+ from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ EXPECT_FALSE(from_mgr);
+}
+#endif
+
+#ifdef HAVE_PGSQL
+/// @brief Extension of the fixture class to use the PostgreSql backend.
+class PgSqlAllocEngine4Test : public AllocEngine4Test {
+public:
+ /// @brief Constructor.
+ PgSqlAllocEngine4Test() {
+ // Ensure we have the proper schema with no transient data.
+ db::test::createPgSQLSchema();
+ factory_.create(db::test::validPgSQLConnectionString());
+ }
+
+ /// @brief Destructor.
+ ~PgSqlAllocEngine4Test() {
+ // If data wipe enabled, delete transient data otherwise destroy
+ // the schema.
+ db::test::destroyPgSQLSchema();
+ LeaseMgrFactory::destroy();
+ }
+};
+
+// This test checks that simple allocation handles BOOTP queries.
+TEST_F(PgSqlAllocEngine4Test, bootpAlloc4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Make the query a BOOTP one.
+ ctx.query_->addClass("BOOTP");
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Check that is belongs to the right subnet and client.
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+ EXPECT_TRUE(subnet_->inRange(lease->addr_));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, lease->addr_));
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_TRUE(*lease->client_id_ == *clientid_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(*lease->hwaddr_ == *hwaddr_);
+
+ // Check the valid lifetime is infinite.
+ uint32_t infinity_lft = Lease::INFINITY_LFT;
+ EXPECT_EQ(infinity_lft, lease->valid_lft_);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+ // The PostgreSql database does not keep the hwtype for DHCPv4 leases.
+ from_mgr->hwaddr_->htype_ = HTYPE_FDDI;
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+}
+
+// This test checks simple renewal handles BOOTP queries.
+TEST_F(PgSqlAllocEngine4Test, bootpRenew4) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Make the query a BOOTP one.
+ ctx.query_->addClass("BOOTP");
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // Check that we got a lease.
+ ASSERT_TRUE(lease);
+
+ // Check that is belongs to the right subnet and client.
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+ EXPECT_TRUE(subnet_->inRange(lease->addr_));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, lease->addr_));
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_TRUE(*lease->client_id_ == *clientid_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(*lease->hwaddr_ == *hwaddr_);
+
+ // Check the valid lifetime is infinite.
+ uint32_t infinity_lft = Lease::INFINITY_LFT;
+ EXPECT_EQ(infinity_lft, lease->valid_lft_);
+
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Do it again, this should amount to the renew of an existing lease
+ Lease4Ptr lease2 = engine->allocateLease4(ctx);
+
+ // Check that we got a lease.
+ ASSERT_TRUE(lease2);
+
+ // Check that is belongs to the right subnet and client.
+ EXPECT_EQ(lease2->subnet_id_, subnet_->getID());
+ EXPECT_TRUE(subnet_->inRange(lease2->addr_));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, lease2->addr_));
+ ASSERT_TRUE(lease2->client_id_);
+ EXPECT_TRUE(*lease2->client_id_ == *clientid_);
+ ASSERT_TRUE(lease2->hwaddr_);
+ EXPECT_TRUE(*lease2->hwaddr_ == *hwaddr_);
+
+ // Lease already existed, so old_lease should be set.
+ EXPECT_TRUE(ctx.old_lease_);
+
+ // Check the renewed valid lifetime has the max value.
+ EXPECT_EQ(infinity_lft, lease2->valid_lft_);
+}
+
+// This test checks that deleteRelease handles BOOTP leases.
+TEST_F(PgSqlAllocEngine4Test, bootpDelete) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 0, false)));
+ ASSERT_TRUE(engine);
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ subnet_->setValid(Triplet<uint32_t>(1, 3, 5));
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ // Make the query a BOOTP one.
+ ctx.query_->addClass("BOOTP");
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+ // The new lease has been allocated, so the old lease should not exist.
+ ASSERT_FALSE(ctx.old_lease_);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Check that is belongs to the right subnet and client.
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+ EXPECT_TRUE(subnet_->inRange(lease->addr_));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, lease->addr_));
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_TRUE(*lease->client_id_ == *clientid_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(*lease->hwaddr_ == *hwaddr_);
+
+ // Check the valid lifetime is infinite.
+ uint32_t infinity_lft = Lease::INFINITY_LFT;
+ EXPECT_EQ(infinity_lft, lease->valid_lft_);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now delete it.
+ bool deleted = false;
+ ASSERT_NO_THROW(deleted = LeaseMgrFactory::instance().deleteLease(lease));
+ EXPECT_TRUE(deleted);
+ from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ EXPECT_FALSE(from_mgr);
+}
+#endif
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
diff --git a/src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc b/src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc
new file mode 100644
index 0000000..d03a8a8
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc
@@ -0,0 +1,5603 @@
+// Copyright (C) 2015-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt6.h>
+#include <dhcpsrv/host_mgr.h>
+#include <dhcpsrv/tests/alloc_engine_utils.h>
+#include <dhcpsrv/testutils/test_utils.h>
+#include <stats/stats_mgr.h>
+#include <testutils/gtest_utils.h>
+
+using namespace std;
+using namespace isc::hooks;
+using namespace isc::asiolink;
+using namespace isc::stats;
+using namespace isc::data;
+using namespace isc::util;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+// Test convenience method adding hints to IA context.
+TEST(ClientContext6Test, addHint) {
+ AllocEngine::ClientContext6 ctx;
+ ctx.currentIA().addHint(IOAddress("2001:db8:1::1"));
+ ctx.currentIA().addHint(IOAddress("3000:1::"), 64);
+ ctx.currentIA().addHint(IOAddress("3001:2::"), 64, 100, 200);
+
+ ASSERT_EQ(3, ctx.currentIA().hints_.size());
+ EXPECT_EQ("2001:db8:1::1", ctx.currentIA().hints_[0].getAddress().toText());
+ EXPECT_EQ("3000:1::", ctx.currentIA().hints_[1].getAddress().toText());
+ EXPECT_EQ("3001:2::", ctx.currentIA().hints_[2].getAddress().toText());
+ EXPECT_EQ(100, ctx.currentIA().hints_[2].getPreferred());
+ EXPECT_EQ(200, ctx.currentIA().hints_[2].getValid());
+}
+
+// Test convenience method adding allocated prefixes and addresses to
+// a context.
+TEST(ClientContext6Test, addAllocatedResource) {
+ AllocEngine::ClientContext6 ctx;
+ ctx.addAllocatedResource(IOAddress("2001:db8:1::1"));
+ ctx.addAllocatedResource(IOAddress("3000:1::"), 64);
+
+ ASSERT_EQ(2, ctx.allocated_resources_.size());
+ EXPECT_TRUE(ctx.isAllocated(IOAddress("2001:db8:1::1")));
+ EXPECT_TRUE(ctx.isAllocated(IOAddress("3000:1::"), 64));
+}
+
+// This test checks if the v6 Allocation Engine can be instantiated, parses
+// parameters string and allocators are created.
+TEST_F(AllocEngine6Test, constructor) {
+ boost::scoped_ptr<AllocEngine> x;
+
+ // Hashed and random allocators are not supported yet
+ ASSERT_THROW(x.reset(new AllocEngine(AllocEngine::ALLOC_HASHED, 5)), NotImplemented);
+ ASSERT_THROW(x.reset(new AllocEngine(AllocEngine::ALLOC_RANDOM, 5)), NotImplemented);
+
+ ASSERT_NO_THROW(x.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100, true)));
+
+ // Check that allocator for normal addresses is created
+ ASSERT_TRUE(x->getAllocator(Lease::TYPE_NA));
+
+ // Check that allocator for temporary address is created
+ ASSERT_TRUE(x->getAllocator(Lease::TYPE_TA));
+
+ // Check that allocator for prefixes is created
+ ASSERT_TRUE(x->getAllocator(Lease::TYPE_PD));
+
+ // There should be no V4 allocator
+ EXPECT_THROW(x->getAllocator(Lease::TYPE_V4), BadValue);
+}
+
+// This test checks if two simple IPv6 allocations succeed and that the
+// statistics is properly updated. Prior to the second allocation it
+// resets the pointer to the last allocated address within the address
+// pool. This causes the engine to walk over the already allocated
+// address and then pick the first available address for the second
+// allocation. Because the allocation engine checks the callouts next
+// step status after each attempt to allocate an address, this test
+// also sets this status to non-default value prior to the second
+// allocation attempt, to make sure that this unexpected status will
+// not interfere with the allocation.
+TEST_F(AllocEngine6Test, simpleAlloc6) {
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ simpleAlloc6Test(pool_, IOAddress("::"), false);
+
+ // We should have bumped the assigned counter by 1
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+
+ // Reset last allocated address to check that the other client will
+ // be refused the already allocated address and will get the one
+ // available.
+ pool_->resetLastAllocated();
+
+ // Simulate another client. This client should be assigned a different
+ // address.
+ DuidPtr duid(new DUID(std::vector<uint8_t>(8, 0x84)));
+ simpleAlloc6Test(pool_, duid, IOAddress("::"), false);
+
+ // We should have bumped the assigned counter by 2
+ EXPECT_TRUE(testStatistics("assigned-nas", 2, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+}
+
+// This test checks that simple allocation uses default lifetimes.
+TEST_F(AllocEngine6Test, defaultAlloc6) {
+ simpleAlloc6Test(pool_, IOAddress("::"), 0, 0, 300, 400);
+}
+
+// This test checks that simple allocation uses specified lifetimes.
+TEST_F(AllocEngine6Test, hintAlloc6) {
+ simpleAlloc6Test(pd_pool_, IOAddress("::"), 301, 399, 301, 399);
+}
+
+// This test checks that simple allocation uses min lifetimes.
+TEST_F(AllocEngine6Test, minAlloc6) {
+ simpleAlloc6Test(pool_, IOAddress("::"), 100, 200, 200, 300);
+}
+
+// This test checks that simple allocation uses max lifetimes.
+TEST_F(AllocEngine6Test, maxAlloc6) {
+ simpleAlloc6Test(pd_pool_, IOAddress("::"), 500, 600, 400, 500);
+}
+
+// This test checks if the simple PD allocation (REQUEST) can succeed
+// and the stats counter is properly bumped by 1
+TEST_F(AllocEngine6Test, pdSimpleAlloc6) {
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-pds", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned prefixes.
+ int64_t cumulative = getStatistics("cumulative-assigned-pds",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-pds");
+
+ simpleAlloc6Test(pd_pool_, IOAddress("::"), false);
+
+ // We should have bumped the assigned counter by 1
+ EXPECT_TRUE(testStatistics("assigned-pds", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-pds",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-pds", glbl_cumulative));
+}
+
+// This test checks if the fake allocation (for SOLICIT) can succeed
+// and the stats counter isn't bumped
+TEST_F(AllocEngine6Test, fakeAlloc6) {
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ simpleAlloc6Test(pool_, IOAddress("::"), true);
+
+ // We should not have bumped the assigned counter.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-nas", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-nas"));
+}
+
+// This test checks if the fake PD allocation (for SOLICIT) can succeed
+// and the stats counter isn't bumped
+TEST_F(AllocEngine6Test, pdFakeAlloc6) {
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-pds", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned prefixes.
+ int64_t cumulative = getStatistics("cumulative-assigned-pds",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-pds");
+
+ simpleAlloc6Test(pd_pool_, IOAddress("::"), true);
+
+ // We should not have bumped the assigned counter
+ EXPECT_TRUE(testStatistics("assigned-pds", 0, subnet_->getID()));
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-pds", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-pds"));
+}
+
+// This test checks if the allocation with a hint that is valid (in range,
+// in pool and free) can succeed
+TEST_F(AllocEngine6Test, allocWithValidHint6) {
+
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("2001:db8:1::15"),
+ false);
+ ASSERT_TRUE(lease);
+
+ // We should get what we asked for
+ EXPECT_EQ("2001:db8:1::15", lease->addr_.toText());
+}
+
+// This test checks if the address allocation with a hint that is in range,
+// in pool, but is currently used, can succeed
+TEST_F(AllocEngine6Test, allocWithUsedHint6) {
+ allocWithUsedHintTest(Lease::TYPE_NA,
+ IOAddress("2001:db8:1::1f"), // allocate this as used
+ IOAddress("2001:db8:1::1f"), // request this addr
+ 128);
+}
+
+// This test checks if the PD allocation with a hint that is in range,
+// in pool, but is currently used, can succeed
+TEST_F(AllocEngine6Test, pdAllocWithUsedHint6) {
+ allocWithUsedHintTest(Lease::TYPE_PD,
+ IOAddress("2001:db8:1:2::"), // allocate this prefix as used
+ IOAddress("2001:db8:1:2::"), // request this prefix
+ 80);
+}
+
+// This test checks if the allocation with a hint that is out the blue
+// can succeed. The invalid hint should be ignored completely.
+TEST_F(AllocEngine6Test, allocBogusHint6) {
+
+ allocBogusHint6(Lease::TYPE_NA, IOAddress("3000::abc"), 128);
+}
+
+// This test checks if the allocation with a hint that is out the blue
+// can succeed. The invalid hint should be ignored completely.
+TEST_F(AllocEngine6Test, pdAllocBogusHint6) {
+
+ allocBogusHint6(Lease::TYPE_PD, IOAddress("3000::abc"), 80);
+}
+
+// This test checks that NULL values are handled properly
+TEST_F(AllocEngine6Test, allocateAddress6Nulls) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Allocations without subnet are not allowed
+ Lease6Ptr lease;
+ AllocEngine::ClientContext6 ctx1(Subnet6Ptr(), duid_, false, false, "", false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ctx1.currentIA().iaid_ = iaid_;
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx1)));
+ ASSERT_FALSE(lease);
+
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes", 1));
+
+ // Allocations without DUID are not allowed either
+ AllocEngine::ClientContext6 ctx2(subnet_, DuidPtr(), false, false, "", false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ctx2.currentIA().iaid_ = iaid_;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx2)));
+ ASSERT_FALSE(lease);
+
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes", 1));
+}
+
+// This test verifies that the allocator picks addresses that belong to the
+// pool
+TEST_F(AllocEngine6Test, IterativeAllocator) {
+ boost::scoped_ptr<NakedAllocEngine::Allocator>
+ alloc(new NakedAllocEngine::IterativeAllocator(Lease::TYPE_NA));
+
+ for (int i = 0; i < 1000; ++i) {
+ IOAddress candidate = alloc->pickAddress(subnet_, cc_,
+ duid_, IOAddress("::"));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_NA, candidate));
+ }
+}
+
+// This test verifies that the allocator picks addresses that belong to the
+// pool using classification
+TEST_F(AllocEngine6Test, IterativeAllocator_class) {
+ boost::scoped_ptr<NakedAllocEngine::Allocator>
+ alloc(new NakedAllocEngine::IterativeAllocator(Lease::TYPE_NA));
+
+ // Restrict pool_ to the foo class. Add a second pool with bar class.
+ pool_->allowClientClass("foo");
+ Pool6Ptr pool(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::100"),
+ IOAddress("2001:db8:1::109")));
+ pool->allowClientClass("bar");
+ subnet_->addPool(pool);
+
+ // Clients are in bar
+ cc_.insert("bar");
+
+ for (int i = 0; i < 1000; ++i) {
+ 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_));
+ }
+}
+
+TEST_F(AllocEngine6Test, IterativeAllocatorAddrStep) {
+ NakedAllocEngine::NakedIterativeAllocator alloc(Lease::TYPE_NA);
+
+ subnet_->delPools(Lease::TYPE_NA); // Get rid of default pool
+
+ Pool6Ptr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ IOAddress("2001:db8:1::5")));
+ Pool6Ptr pool2(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::100"),
+ IOAddress("2001:db8:1::100")));
+ Pool6Ptr pool3(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::105"),
+ IOAddress("2001:db8:1::106")));
+ subnet_->addPool(pool1);
+ subnet_->addPool(pool2);
+ subnet_->addPool(pool3);
+
+ // Let's check the first pool (5 addresses here)
+ EXPECT_EQ("2001:db8:1::1",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::2",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::3",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::4",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::5",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // The second pool is easy - only one address here
+ EXPECT_EQ("2001:db8:1::100",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // This is the third and last pool, with 2 addresses in it
+ EXPECT_EQ("2001:db8:1::105",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::106",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // We iterated over all addresses and reached to the end of the last pool.
+ // Let's wrap around and start from the beginning
+ EXPECT_EQ("2001:db8:1::1",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::2",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+}
+
+TEST_F(AllocEngine6Test, IterativeAllocatorAddrStepInClass) {
+ NakedAllocEngine::NakedIterativeAllocator alloc(Lease::TYPE_NA);
+
+ subnet_->delPools(Lease::TYPE_NA); // Get rid of default pool
+
+ Pool6Ptr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ IOAddress("2001:db8:1::5")));
+ Pool6Ptr pool2(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::100"),
+ IOAddress("2001:db8:1::100")));
+ Pool6Ptr pool3(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::105"),
+ IOAddress("2001:db8:1::106")));
+ // Set pool1 and pool3 but not pool2 in foo class
+ pool1->allowClientClass("foo");
+ pool3->allowClientClass("foo");
+ subnet_->addPool(pool1);
+ subnet_->addPool(pool2);
+ subnet_->addPool(pool3);
+
+ // Clients are in foo
+ cc_.insert("foo");
+
+ // Let's check the first pool (5 addresses here)
+ EXPECT_EQ("2001:db8:1::1",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::2",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::3",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::4",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::5",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // The second pool is easy - only one address here
+ EXPECT_EQ("2001:db8:1::100",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // This is the third and last pool, with 2 addresses in it
+ EXPECT_EQ("2001:db8:1::105",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::106",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // We iterated over all addresses and reached to the end of the last pool.
+ // Let's wrap around and start from the beginning
+ EXPECT_EQ("2001:db8:1::1",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::2",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+}
+
+TEST_F(AllocEngine6Test, IterativeAllocatorAddrStepOutClass) {
+ NakedAllocEngine::NakedIterativeAllocator alloc(Lease::TYPE_NA);
+
+ subnet_->delPools(Lease::TYPE_NA); // Get rid of default pool
+
+ Pool6Ptr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ IOAddress("2001:db8:1::5")));
+ Pool6Ptr pool2(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::100"),
+ IOAddress("2001:db8:1::100")));
+ Pool6Ptr pool3(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::105"),
+ IOAddress("2001:db8:1::106")));
+ // Set pool2 in foo
+ pool2->allowClientClass("foo");
+ subnet_->addPool(pool1);
+ subnet_->addPool(pool2);
+ subnet_->addPool(pool3);
+
+ // Let's check the first pool (5 addresses here)
+ EXPECT_EQ("2001:db8:1::1",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::2",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::3",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::4",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::5",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // The second pool is skipped
+
+ // This is the third and last pool, with 2 addresses in it
+ EXPECT_EQ("2001:db8:1::105",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::106",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // We iterated over all addresses and reached to the end of the last pool.
+ // Let's wrap around and start from the beginning
+ EXPECT_EQ("2001:db8:1::1",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::2",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+}
+
+TEST_F(AllocEngine6Test, IterativeAllocatorPrefixStep) {
+ NakedAllocEngine::NakedIterativeAllocator alloc(Lease::TYPE_PD);
+
+ subnet_.reset(new Subnet6(IOAddress("2001:db8::"), 32, 1, 2, 3, 4));
+
+ Pool6Ptr pool1(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8::"), 56, 60));
+ Pool6Ptr pool2(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:1::"), 48, 48));
+ Pool6Ptr pool3(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:2::"), 56, 64));
+ subnet_->addPool(pool1);
+ subnet_->addPool(pool2);
+ subnet_->addPool(pool3);
+
+ // We have a 2001:db8::/48 subnet that has 3 pools defined in it:
+ // 2001:db8::/56 split into /60 prefixes (16 leases) (or 2001:db8:0:X0::)
+ // 2001:db8:1::/48 split into a single /48 prefix (just 1 lease)
+ // 2001:db8:2::/56 split into /64 prefixes (256 leases) (or 2001:db8:2:XX::)
+
+ // First pool check (Let's check over all 16 leases)
+ EXPECT_EQ("2001:db8::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:10::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:20::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:30::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:40::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:50::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:60::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:70::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:80::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:90::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:a0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:b0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:c0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:d0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:e0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:f0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // Second pool (just one lease here)
+ EXPECT_EQ("2001:db8:1::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // Third pool (256 leases, let's check first and last explicitly and the
+ // rest over in a pool
+ EXPECT_EQ("2001:db8:2::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ for (int i = 1; i < 255; i++) {
+ stringstream exp;
+ exp << "2001:db8:2:" << hex << i << dec << "::";
+ EXPECT_EQ(exp.str(),
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ }
+ EXPECT_EQ("2001:db8:2:ff::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // Ok, we've iterated over all prefixes in all pools. We now wrap around.
+ // We're looping over now (iterating over first pool again)
+ EXPECT_EQ("2001:db8::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:10::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+}
+
+TEST_F(AllocEngine6Test, IterativeAllocatorPrefixStepInClass) {
+ NakedAllocEngine::NakedIterativeAllocator alloc(Lease::TYPE_PD);
+
+ subnet_.reset(new Subnet6(IOAddress("2001:db8::"), 32, 1, 2, 3, 4));
+
+ Pool6Ptr pool1(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8::"), 56, 60));
+ Pool6Ptr pool2(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:1::"), 48, 48));
+ Pool6Ptr pool3(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:2::"), 56, 64));
+ // Set pool1 and pool3 but not pool2 in foo class
+ pool1->allowClientClass("foo");
+ pool3->allowClientClass("foo");
+ subnet_->addPool(pool1);
+ subnet_->addPool(pool2);
+ subnet_->addPool(pool3);
+
+ // Clients are in foo
+ cc_.insert("foo");
+
+ // We have a 2001:db8::/48 subnet that has 3 pools defined in it:
+ // 2001:db8::/56 split into /60 prefixes (16 leases) (or 2001:db8:0:X0::)
+ // 2001:db8:1::/48 split into a single /48 prefix (just 1 lease)
+ // 2001:db8:2::/56 split into /64 prefixes (256 leases) (or 2001:db8:2:XX::)
+
+ // First pool check (Let's check over all 16 leases)
+ EXPECT_EQ("2001:db8::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:10::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:20::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:30::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:40::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:50::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:60::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:70::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:80::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:90::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:a0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:b0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:c0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:d0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:e0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:f0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // Second pool (just one lease here)
+ EXPECT_EQ("2001:db8:1::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // Third pool (256 leases, let's check first and last explicitly and the
+ // rest over in a pool
+ EXPECT_EQ("2001:db8:2::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ for (int i = 1; i < 255; i++) {
+ stringstream exp;
+ exp << "2001:db8:2:" << hex << i << dec << "::";
+ EXPECT_EQ(exp.str(),
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ }
+ EXPECT_EQ("2001:db8:2:ff::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // Ok, we've iterated over all prefixes in all pools. We now wrap around.
+ // We're looping over now (iterating over first pool again)
+ EXPECT_EQ("2001:db8::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:10::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+}
+
+TEST_F(AllocEngine6Test, IterativeAllocatorPrefixStepOutClass) {
+ NakedAllocEngine::NakedIterativeAllocator alloc(Lease::TYPE_PD);
+
+ subnet_.reset(new Subnet6(IOAddress("2001:db8::"), 32, 1, 2, 3, 4));
+
+ Pool6Ptr pool1(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8::"), 56, 60));
+ Pool6Ptr pool2(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:1::"), 48, 48));
+ Pool6Ptr pool3(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:2::"), 56, 64));
+ // Set pool2 in foo
+ pool2->allowClientClass("foo");
+ subnet_->addPool(pool1);
+ subnet_->addPool(pool2);
+ subnet_->addPool(pool3);
+
+ // We have a 2001:db8::/48 subnet that has 3 pools defined in it:
+ // 2001:db8::/56 split into /60 prefixes (16 leases) (or 2001:db8:0:X0::)
+ // 2001:db8:1::/48 split into a single /48 prefix (just 1 lease)
+ // 2001:db8:2::/56 split into /64 prefixes (256 leases) (or 2001:db8:2:XX::)
+
+ // First pool check (Let's check over all 16 leases)
+ EXPECT_EQ("2001:db8::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:10::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:20::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:30::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:40::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:50::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:60::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:70::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:80::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:90::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:a0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:b0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:c0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:d0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:e0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:f0::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // The second pool is skipped
+
+ // Third pool (256 leases, let's check first and last explicitly and the
+ // rest over in a pool
+ EXPECT_EQ("2001:db8:2::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ for (int i = 1; i < 255; i++) {
+ stringstream exp;
+ exp << "2001:db8:2:" << hex << i << dec << "::";
+ EXPECT_EQ(exp.str(),
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ }
+ EXPECT_EQ("2001:db8:2:ff::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+
+ // Ok, we've iterated over all prefixes in all pools. We now wrap around.
+ // We're looping over now (iterating over first pool again)
+ EXPECT_EQ("2001:db8::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:10::",
+ alloc.pickAddress(subnet_, cc_, duid_, IOAddress("::")).toText());
+}
+
+// This test verifies that the iterative allocator can step over addresses
+TEST_F(AllocEngine6Test, IterativeAllocatorAddressIncrease) {
+ NakedAllocEngine::NakedIterativeAllocator alloc(Lease::TYPE_NA);
+
+ // Let's pick the first address
+ IOAddress addr1 = alloc.pickAddress(subnet_, cc_, duid_, IOAddress("2001:db8:1::10"));
+
+ // Check that we can indeed pick the first address from the pool
+ EXPECT_EQ("2001:db8:1::10", addr1.toText());
+
+ // Check that addresses can be increased properly
+ checkAddrIncrease(alloc, "2001:db8::9", "2001:db8::a");
+ checkAddrIncrease(alloc, "2001:db8::f", "2001:db8::10");
+ checkAddrIncrease(alloc, "2001:db8::10", "2001:db8::11");
+ checkAddrIncrease(alloc, "2001:db8::ff", "2001:db8::100");
+ checkAddrIncrease(alloc, "2001:db8::ffff", "2001:db8::1:0");
+ checkAddrIncrease(alloc, "::", "::1");
+ checkAddrIncrease(alloc, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "::");
+}
+
+// This test verifies that the allocator can step over prefixes
+TEST_F(AllocEngine6Test, IterativeAllocatorPrefixIncrease) {
+ NakedAllocEngine::NakedIterativeAllocator alloc(Lease::TYPE_PD);
+
+ // For /128 prefix, increasePrefix should work the same as addressIncrease
+ checkPrefixIncrease(alloc, "2001:db8::9", 128, "2001:db8::a");
+ checkPrefixIncrease(alloc, "2001:db8::f", 128, "2001:db8::10");
+ checkPrefixIncrease(alloc, "2001:db8::10", 128, "2001:db8::11");
+ checkPrefixIncrease(alloc, "2001:db8::ff", 128, "2001:db8::100");
+ checkPrefixIncrease(alloc, "2001:db8::ffff", 128, "2001:db8::1:0");
+ checkPrefixIncrease(alloc, "::", 128, "::1");
+ checkPrefixIncrease(alloc, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 128, "::");
+
+ // Check that /64 prefixes can be generated
+ checkPrefixIncrease(alloc, "2001:db8::", 64, "2001:db8:0:1::");
+
+ // Check that prefix length not divisible by 8 are working
+ checkPrefixIncrease(alloc, "2001:db8::", 128, "2001:db8::1");
+ checkPrefixIncrease(alloc, "2001:db8::", 127, "2001:db8::2");
+ checkPrefixIncrease(alloc, "2001:db8::", 126, "2001:db8::4");
+ checkPrefixIncrease(alloc, "2001:db8::", 125, "2001:db8::8");
+ checkPrefixIncrease(alloc, "2001:db8::", 124, "2001:db8::10");
+ checkPrefixIncrease(alloc, "2001:db8::", 123, "2001:db8::20");
+ checkPrefixIncrease(alloc, "2001:db8::", 122, "2001:db8::40");
+ checkPrefixIncrease(alloc, "2001:db8::", 121, "2001:db8::80");
+ checkPrefixIncrease(alloc, "2001:db8::", 120, "2001:db8::100");
+
+ // These are not really useful cases, because there are bits set
+ // int the last (128 - prefix_len) bits. Nevertheless, it shows
+ // that the algorithm is working even in such cases
+ checkPrefixIncrease(alloc, "2001:db8::1", 128, "2001:db8::2");
+ checkPrefixIncrease(alloc, "2001:db8::1", 127, "2001:db8::3");
+ checkPrefixIncrease(alloc, "2001:db8::1", 126, "2001:db8::5");
+ checkPrefixIncrease(alloc, "2001:db8::1", 125, "2001:db8::9");
+ checkPrefixIncrease(alloc, "2001:db8::1", 124, "2001:db8::11");
+ checkPrefixIncrease(alloc, "2001:db8::1", 123, "2001:db8::21");
+ checkPrefixIncrease(alloc, "2001:db8::1", 122, "2001:db8::41");
+ checkPrefixIncrease(alloc, "2001:db8::1", 121, "2001:db8::81");
+ checkPrefixIncrease(alloc, "2001:db8::1", 120, "2001:db8::101");
+
+ // Let's try out couple real life scenarios
+ checkPrefixIncrease(alloc, "2001:db8:1:abcd::", 64, "2001:db8:1:abce::");
+ checkPrefixIncrease(alloc, "2001:db8:1:abcd::", 60, "2001:db8:1:abdd::");
+ checkPrefixIncrease(alloc, "2001:db8:1:abcd::", 56, "2001:db8:1:accd::");
+ checkPrefixIncrease(alloc, "2001:db8:1:abcd::", 52, "2001:db8:1:bbcd::");
+
+ // And now let's try something over the top
+ checkPrefixIncrease(alloc, "::", 1, "8000::");
+}
+
+// This test verifies that the iterative allocator really walks over all addresses
+// in all pools in specified subnet. It also must not pick the same address twice
+// unless it runs out of pool space and must start over.
+TEST_F(AllocEngine6Test, IterativeAllocator_manyPools6) {
+ NakedAllocEngine::IterativeAllocator alloc(Lease::TYPE_NA);
+
+ // let's start from 2, as there is 2001:db8:1::10 - 2001:db8:1::20 pool already.
+ for (int i = 2; i < 10; ++i) {
+ stringstream min, max;
+
+ min << "2001:db8:1::" << hex << i*16 + 1;
+ max << "2001:db8:1::" << hex << i*16 + 9;
+
+ Pool6Ptr pool(new Pool6(Lease::TYPE_NA, IOAddress(min.str()),
+ IOAddress(max.str())));
+ subnet_->addPool(pool);
+ }
+
+ int total = 17 + 8 * 9; // First pool (::10 - ::20) has 17 addresses in it,
+ // there are 8 extra pools with 9 addresses in each.
+
+ // Let's keep picked addresses here and check their uniqueness.
+ std::set<IOAddress> generated_addrs;
+ int cnt = 0;
+ while (++cnt) {
+ IOAddress candidate = alloc.pickAddress(subnet_, cc_,
+ duid_, IOAddress("::"));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_NA, candidate));
+
+ // One way to easily verify that the iterative allocator really works is
+ // to uncomment the following line and observe its output that it
+ // covers all defined pools.
+ // cout << candidate.toText() << endl;
+
+ if (generated_addrs.find(candidate) == generated_addrs.end()) {
+ // We haven't had this.
+ generated_addrs.insert(candidate);
+ } else {
+ // We have seen this address before. That should mean that we
+ // iterated over all addresses.
+ if (generated_addrs.size() == total) {
+ // We have exactly the number of address in all pools.
+ break;
+ }
+ ADD_FAILURE() << "Too many or not enough unique addresses generated.";
+ break;
+ }
+
+ if ( cnt>total ) {
+ ADD_FAILURE() << "Too many unique addresses generated.";
+ break;
+ }
+ }
+}
+
+// This test checks if really small pools are working
+TEST_F(AllocEngine6Test, smallPool6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ IOAddress addr("2001:db8:1::ad");
+
+ // Create a subnet with a pool that has one address.
+ initSubnet(IOAddress("2001:db8:1::"), addr, addr);
+
+ // Initialize FQDN for a lease.
+ initFqdn("myhost.example.com", true, true);
+
+ Lease6Ptr lease;
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, fqdn_fwd_, fqdn_rev_,
+ hostname_, false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ctx.currentIA().iaid_ = iaid_;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+
+ // Check that we got that single lease
+ ASSERT_TRUE(lease);
+
+ EXPECT_EQ("2001:db8:1::ad", lease->addr_.toText());
+
+ // Do all checks on the lease
+ checkLease6(duid_, lease, Lease::TYPE_NA, 128);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+
+ // This is a new lease allocation. The old lease corresponding to a newly
+ // allocated lease should be NULL.
+ ASSERT_TRUE(ctx.currentIA().old_leases_.empty());
+}
+
+// This test checks if all addresses in a pool are currently used, the attempt
+// to find out a new lease fails.
+TEST_F(AllocEngine6Test, outOfAddresses6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ IOAddress addr("2001:db8:1::ad");
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ cfg_mgr.clear(); // Get rid of the default test configuration
+
+ // Create configuration similar to other tests, but with a single address pool
+ subnet_ = Subnet6Ptr(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+ pool_ = Pool6Ptr(new Pool6(Lease::TYPE_NA, addr, addr)); // just a single address
+ subnet_->addPool(pool_);
+ cfg_mgr.getStagingCfg()->getCfgSubnets6()->add(subnet_);
+
+ // Just a different duid
+ DuidPtr other_duid = DuidPtr(new DUID(vector<uint8_t>(12, 0xff)));
+ const uint32_t other_iaid = 3568;
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, other_duid, other_iaid,
+ 501, 502, subnet_->getID(),
+ HWAddrPtr(), 0));
+ lease->cltt_ = time(NULL) - 10; // Allocated 10 seconds ago
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // There is just a single address in the pool and allocated it to someone
+ // else, so the allocation should fail
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ Lease6Ptr lease2;
+ EXPECT_NO_THROW(lease2 = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_FALSE(lease2);
+
+ EXPECT_EQ(1, getStatistics("v6-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-shared-network"));
+ EXPECT_EQ(1, getStatistics("v6-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes"));
+
+ EXPECT_EQ(1, getStatistics("v6-allocation-fail", 2));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-shared-network", 2));
+ EXPECT_EQ(1, getStatistics("v6-allocation-fail-subnet", 2));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools", 2));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes", 2));
+}
+
+// This test checks if an expired lease can be reused in SOLICIT (fake allocation)
+TEST_F(AllocEngine6Test, solicitReuseExpiredLease6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ IOAddress addr("2001:db8:1::ad");
+
+ // Create one subnet with a pool holding one address.
+ initSubnet(IOAddress("2001:db8:1::"), addr, addr);
+
+ // Initialize FQDN data for the lease.
+ initFqdn("myhost.example.com", true, true);
+
+ // Verify the all of relevant stats are zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 0));
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Just a different duid
+ DuidPtr other_duid = DuidPtr(new DUID(vector<uint8_t>(12, 0xff)));
+ const uint32_t other_iaid = 3568;
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, other_duid, other_iaid,
+ 501, 502, subnet_->getID(),
+ HWAddrPtr(), 0));
+ lease->cltt_ = time(NULL) - 500; // Allocated 500 seconds ago
+ lease->valid_lft_ = 495; // Lease was valid for 495 seconds
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Make sure that we really created expired lease
+ ASSERT_TRUE(lease->expired());
+
+ // CASE 1: Asking for any address
+ AllocEngine::ClientContext6 ctx1(subnet_, duid_, fqdn_fwd_, fqdn_rev_, hostname_,
+ true, Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)));
+ ctx1.currentIA().iaid_ = iaid_;
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx1)));
+
+ // Check that we got that single lease
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // Do all checks on the lease (if subnet-id, preferred/valid times are ok etc.)
+ checkLease6(duid_, lease, Lease::TYPE_NA, 128);
+
+ // CASE 2: Asking specifically for this address
+ AllocEngine::ClientContext6 ctx2(subnet_, duid_, false, false, "", true,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ctx2.currentIA().iaid_ = iaid_;
+ ctx2.currentIA().addHint(addr);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx2)));
+
+ // Check that we got that single lease
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // Verify the none of relevant stats were altered.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-nas", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-nas"));
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 0));
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 0, subnet_->getID()));
+}
+
+// This test checks if an expired lease can be reused using default lifetimes.
+TEST_F(AllocEngine6Test, defaultReuseExpiredLease6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ IOAddress addr("2001:db8:1::ad");
+
+ // Create one subnet with a pool holding one address.
+ initSubnet(IOAddress("2001:db8:1::"), addr, addr);
+ subnet_->setPreferred(Triplet<uint32_t>(200, 300, 400));
+ subnet_->setValid(Triplet<uint32_t>(300, 400, 500));
+
+ // Initialize FQDN data for the lease.
+ initFqdn("myhost.example.com", true, true);
+
+ // Just a different duid
+ DuidPtr other_duid = DuidPtr(new DUID(vector<uint8_t>(12, 0xff)));
+ const uint32_t other_iaid = 3568;
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, other_duid, other_iaid,
+ 501, 502, subnet_->getID(),
+ HWAddrPtr(), 0));
+ lease->cltt_ = time(NULL) - 500; // Allocated 500 seconds ago
+ lease->valid_lft_ = 495; // Lease was valid for 495 seconds
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Make sure that we really created expired lease
+ ASSERT_TRUE(lease->expired());
+
+ // Asking specifically for this address with zero lifetimes
+ AllocEngine::ClientContext6 ctx2(subnet_, duid_, false, false, "", true,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ctx2.currentIA().iaid_ = iaid_;
+ ctx2.currentIA().addHint(addr, 128, 0, 0);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx2)));
+
+ // Check that we got that single lease
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // Check lifetimes: defaults are expected.
+ EXPECT_EQ(300, lease->preferred_lft_);
+ EXPECT_EQ(400, lease->valid_lft_);
+}
+
+// This test checks if an expired lease can be reused using specified lifetimes.
+TEST_F(AllocEngine6Test, hintReuseExpiredLease6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ IOAddress addr("2001:db8:1::ad");
+
+ // Create one subnet with a pool holding one address.
+ initSubnet(IOAddress("2001:db8:1::"), addr, addr);
+ subnet_->setPreferred(Triplet<uint32_t>(200, 300, 400));
+ subnet_->setValid(Triplet<uint32_t>(300, 400, 500));
+
+ // Initialize FQDN data for the lease.
+ initFqdn("myhost.example.com", true, true);
+
+ // Just a different duid
+ DuidPtr other_duid = DuidPtr(new DUID(vector<uint8_t>(12, 0xff)));
+ const uint32_t other_iaid = 3568;
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, other_duid, other_iaid,
+ 501, 502, subnet_->getID(),
+ HWAddrPtr(), 0));
+ lease->cltt_ = time(NULL) - 500; // Allocated 500 seconds ago
+ lease->valid_lft_ = 495; // Lease was valid for 495 seconds
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Make sure that we really created expired lease
+ ASSERT_TRUE(lease->expired());
+
+ // Asking specifically for this address with zero lifetimes
+ AllocEngine::ClientContext6 ctx2(subnet_, duid_, false, false, "", true,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ctx2.currentIA().iaid_ = iaid_;
+ ctx2.currentIA().addHint(addr, 128, 299, 401);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx2)));
+
+ // Check that we got that single lease
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // Check lifetimes: specified values are expected.
+ EXPECT_EQ(299, lease->preferred_lft_);
+ EXPECT_EQ(401, lease->valid_lft_);
+}
+
+// This test checks if an expired lease can be reused using min lifetimes.
+TEST_F(AllocEngine6Test, minReuseExpiredLease6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ IOAddress addr("2001:db8:1::ad");
+
+ // Create one subnet with a pool holding one address.
+ initSubnet(IOAddress("2001:db8:1::"), addr, addr);
+ subnet_->setPreferred(Triplet<uint32_t>(200, 300, 400));
+ subnet_->setValid(Triplet<uint32_t>(300, 400, 500));
+
+ // Initialize FQDN data for the lease.
+ initFqdn("myhost.example.com", true, true);
+
+ // Just a different duid
+ DuidPtr other_duid = DuidPtr(new DUID(vector<uint8_t>(12, 0xff)));
+ const uint32_t other_iaid = 3568;
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, other_duid, other_iaid,
+ 501, 502, subnet_->getID(),
+ HWAddrPtr(), 0));
+ lease->cltt_ = time(NULL) - 500; // Allocated 500 seconds ago
+ lease->valid_lft_ = 495; // Lease was valid for 495 seconds
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Make sure that we really created expired lease
+ ASSERT_TRUE(lease->expired());
+
+ // Asking specifically for this address with zero lifetimes
+ AllocEngine::ClientContext6 ctx2(subnet_, duid_, false, false, "", true,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ctx2.currentIA().iaid_ = iaid_;
+ ctx2.currentIA().addHint(addr, 128, 100, 200);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx2)));
+
+ // Check that we got that single lease
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // Check lifetimes: min values are expected.
+ EXPECT_EQ(200, lease->preferred_lft_);
+ EXPECT_EQ(300, lease->valid_lft_);
+}
+
+// This test checks if an expired lease can be reused using max lifetimes.
+TEST_F(AllocEngine6Test, maxReuseExpiredLease6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ IOAddress addr("2001:db8:1::ad");
+
+ // Create one subnet with a pool holding one address.
+ initSubnet(IOAddress("2001:db8:1::"), addr, addr);
+ subnet_->setPreferred(Triplet<uint32_t>(200, 300, 400));
+ subnet_->setValid(Triplet<uint32_t>(300, 400, 500));
+
+ // Initialize FQDN data for the lease.
+ initFqdn("myhost.example.com", true, true);
+
+ // Just a different duid
+ DuidPtr other_duid = DuidPtr(new DUID(vector<uint8_t>(12, 0xff)));
+ const uint32_t other_iaid = 3568;
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, other_duid, other_iaid,
+ 501, 502, subnet_->getID(),
+ HWAddrPtr(), 0));
+ lease->cltt_ = time(NULL) - 500; // Allocated 500 seconds ago
+ lease->valid_lft_ = 495; // Lease was valid for 495 seconds
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Make sure that we really created expired lease
+ ASSERT_TRUE(lease->expired());
+
+ // Asking specifically for this address with zero lifetimes
+ AllocEngine::ClientContext6 ctx2(subnet_, duid_, false, false, "", true,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ctx2.currentIA().iaid_ = iaid_;
+ ctx2.currentIA().addHint(addr, 128, 500, 600);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx2)));
+
+ // Check that we got that single lease
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // Check lifetimes: max values are expected.
+ EXPECT_EQ(400, lease->preferred_lft_);
+ EXPECT_EQ(500, lease->valid_lft_);
+}
+
+// This test checks if an expired lease can be reused in REQUEST (actual allocation)
+TEST_F(AllocEngine6Test, requestReuseExpiredLease6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ IOAddress addr("2001:db8:1::ad");
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ cfg_mgr.clear(); // Get rid of the default test configuration
+
+ // Create configuration similar to other tests, but with a single address pool
+ subnet_ = Subnet6Ptr(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+ pool_ = Pool6Ptr(new Pool6(Lease::TYPE_NA, addr, addr)); // just a single address
+ subnet_->addPool(pool_);
+ cfg_mgr.getStagingCfg()->getCfgSubnets6()->add(subnet_);
+ cfg_mgr.commit();
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Let's create an expired lease
+ DuidPtr other_duid = DuidPtr(new DUID(vector<uint8_t>(12, 0xff)));
+ const uint32_t other_iaid = 3568;
+
+ const SubnetID other_subnetid = 999;
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, other_duid, other_iaid,
+ 501, 502, other_subnetid, HWAddrPtr(),
+ 0));
+ int64_t other_cumulative =
+ getStatistics("cumulative-assigned-nas", other_subnetid);
+
+ lease->cltt_ = time(NULL) - 500; // Allocated 500 seconds ago
+ lease->valid_lft_ = 495; // Lease was valid for 495 seconds
+ lease->fqdn_fwd_ = true;
+ lease->fqdn_rev_ = true;
+ lease->hostname_ = "myhost.example.com.";
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // A client comes along, asking specifically for this address
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(addr);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+
+ // Check that he got that single lease
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+ // This reactivated lease should have updated FQDN data.
+ EXPECT_TRUE(lease->hostname_.empty());
+ EXPECT_FALSE(lease->fqdn_fwd_);
+ EXPECT_FALSE(lease->fqdn_rev_);
+
+ // Check that the old lease has been returned.
+ Lease6Ptr old_lease = expectOneLease(ctx.currentIA().old_leases_);
+ ASSERT_TRUE(old_lease);
+
+ // It should at least have the same IPv6 address.
+ EXPECT_EQ(lease->addr_, old_lease->addr_);
+ // Check that it carries not updated FQDN data.
+ EXPECT_EQ("myhost.example.com.", old_lease->hostname_);
+ EXPECT_TRUE(old_lease->fqdn_fwd_);
+ EXPECT_TRUE(old_lease->fqdn_rev_);
+
+ // Check that the lease is indeed updated in LeaseMgr
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+ addr);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+
+ // Verify the stats got adjusted correctly
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+ EXPECT_TRUE(testStatistics("assigned-nas", -1, other_subnetid));
+ EXPECT_EQ(other_cumulative,
+ getStatistics("cumulative-assigned-nas", other_subnetid));
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 1));
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 0, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 1, other_subnetid));
+}
+
+// Checks if the lease lifetime is extended when the client sends the
+// Request.
+TEST_F(AllocEngine6Test, requestExtendLeaseLifetime) {
+ // Create a lease for the client.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::15"),
+ duid_, iaid_, 300, 400,
+ subnet_->getID(), HWAddrPtr(), 128));
+
+ // Allocated 200 seconds ago - half of the lifetime.
+ time_t lease_cltt = time(NULL) - 200;
+ lease->cltt_ = lease_cltt;
+
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Client should receive a lease.
+ Lease6Ptr new_lease = simpleAlloc6Test(pool_, IOAddress("::"), false);
+ ASSERT_TRUE(new_lease);
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(new_lease->cltt_, lease_cltt)
+ << "Lease lifetime was not extended, but it should";
+}
+
+// Checks if the lease lifetime is extended when the client sends the
+// Request and the client has a reservation for the lease.
+TEST_F(AllocEngine6Test, requestExtendLeaseLifetimeForReservation) {
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1c"), 128);
+
+ // Create a lease for the client.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1c"),
+ duid_, iaid_, 300, 400,
+ subnet_->getID(), HWAddrPtr(), 128));
+
+ // Allocated 200 seconds ago - half of the lifetime.
+ time_t lease_cltt = time(NULL) - 200;
+ lease->cltt_ = lease_cltt;
+
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Client should receive a lease.
+ Lease6Ptr new_lease = simpleAlloc6Test(pool_, IOAddress("::"), false);
+ ASSERT_TRUE(new_lease);
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(new_lease->cltt_, lease_cltt)
+ << "Lease lifetime was not extended, but it should";
+}
+
+// Checks if the lease lifetime is extended when the client sends the
+// Renew.
+TEST_F(AllocEngine6Test, renewExtendLeaseLifetime) {
+ // Create a lease for the client.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::15"),
+ duid_, iaid_, 300, 400,
+ subnet_->getID(), HWAddrPtr(), 128));
+
+ // Allocated 200 seconds ago - half of the lifetime.
+ time_t lease_cltt = time(NULL) - 200;
+ lease->cltt_ = lease_cltt;
+
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100);
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(IOAddress("2001:db8:1::15"), 128));
+
+ // Client should receive a lease.
+ Lease6Collection renewed = renewTest(engine, pool_, hints, true);
+ ASSERT_EQ(1, renewed.size());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(renewed[0]->cltt_, lease_cltt)
+ << "Lease lifetime was not extended, but it should";
+}
+
+// Checks that a renewed lease uses default lifetimes.
+TEST_F(AllocEngine6Test, defaultRenewLeaseLifetime) {
+ // Create a lease for the client.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::15"),
+ duid_, iaid_, 300, 400,
+ subnet_->getID(), HWAddrPtr(), 128));
+
+ // Allocated 200 seconds ago - half of the lifetime.
+ time_t lease_cltt = time(NULL) - 200;
+ lease->cltt_ = lease_cltt;
+
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100);
+ subnet_->setPreferred(Triplet<uint32_t>(200, 300, 400));
+ subnet_->setValid(Triplet<uint32_t>(300, 400, 500));
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(IOAddress("2001:db8:1::15"),
+ 128, 0, 0));
+
+ // Client should receive a lease.
+ Lease6Collection renewed = renewTest(engine, pool_, hints, true);
+ ASSERT_EQ(1, renewed.size());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(renewed[0]->cltt_, lease_cltt)
+ << "Lease lifetime was not extended, but it should";
+
+ // Checks that default values are used for lifetimes.
+ EXPECT_EQ(300, renewed[0]->preferred_lft_);
+ EXPECT_EQ(400, renewed[0]->valid_lft_);
+}
+
+// Checks that a renewed lease uses specified lifetimes.
+TEST_F(AllocEngine6Test, hintRenewLeaseLifetime) {
+ // Create a lease for the client.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::15"),
+ duid_, iaid_, 300, 400,
+ subnet_->getID(), HWAddrPtr(), 128));
+
+ // Allocated 200 seconds ago - half of the lifetime.
+ time_t lease_cltt = time(NULL) - 200;
+ lease->cltt_ = lease_cltt;
+
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100);
+ subnet_->setPreferred(Triplet<uint32_t>(200, 300, 400));
+ subnet_->setValid(Triplet<uint32_t>(300, 400, 500));
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(IOAddress("2001:db8:1::15"),
+ 128, 301, 399));
+
+ // Client should receive a lease.
+ Lease6Collection renewed = renewTest(engine, pool_, hints, true);
+ ASSERT_EQ(1, renewed.size());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(renewed[0]->cltt_, lease_cltt)
+ << "Lease lifetime was not extended, but it should";
+
+ // Checks that specified values are used for lifetimes.
+ EXPECT_EQ(301, renewed[0]->preferred_lft_);
+ EXPECT_EQ(399, renewed[0]->valid_lft_);
+}
+
+// Checks that a renewed lease uses min lifetimes.
+TEST_F(AllocEngine6Test, minRenewLeaseLifetime) {
+ // Create a lease for the client.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::15"),
+ duid_, iaid_, 300, 400,
+ subnet_->getID(), HWAddrPtr(), 128));
+
+ // Allocated 200 seconds ago - half of the lifetime.
+ time_t lease_cltt = time(NULL) - 200;
+ lease->cltt_ = lease_cltt;
+
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100);
+ subnet_->setPreferred(Triplet<uint32_t>(200, 300, 400));
+ subnet_->setValid(Triplet<uint32_t>(300, 400, 500));
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(IOAddress("2001:db8:1::15"),
+ 128, 100, 200));
+
+ // Client should receive a lease.
+ Lease6Collection renewed = renewTest(engine, pool_, hints, true);
+ ASSERT_EQ(1, renewed.size());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(renewed[0]->cltt_, lease_cltt)
+ << "Lease lifetime was not extended, but it should";
+
+ // Checks that min values are used for lifetimes.
+ EXPECT_EQ(200, renewed[0]->preferred_lft_);
+ EXPECT_EQ(300, renewed[0]->valid_lft_);
+}
+
+// Checks that a renewed lease uses max lifetimes.
+TEST_F(AllocEngine6Test, maxRenewLeaseLifetime) {
+ // Create a lease for the client.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::15"),
+ duid_, iaid_, 300, 400,
+ subnet_->getID(), HWAddrPtr(), 128));
+
+ // Allocated 200 seconds ago - half of the lifetime.
+ time_t lease_cltt = time(NULL) - 200;
+ lease->cltt_ = lease_cltt;
+
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100);
+ subnet_->setPreferred(Triplet<uint32_t>(200, 300, 400));
+ subnet_->setValid(Triplet<uint32_t>(300, 400, 500));
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(IOAddress("2001:db8:1::15"),
+ 128, 500, 600));
+
+ // Client should receive a lease.
+ Lease6Collection renewed = renewTest(engine, pool_, hints, true);
+ ASSERT_EQ(1, renewed.size());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(renewed[0]->cltt_, lease_cltt)
+ << "Lease lifetime was not extended, but it should";
+
+ // Checks that max values are used for lifetimes.
+ EXPECT_EQ(400, renewed[0]->preferred_lft_);
+ EXPECT_EQ(500, renewed[0]->valid_lft_);
+}
+
+// Checks if the lease lifetime is extended when the client sends the
+// Renew and the client has a reservation for the lease.
+TEST_F(AllocEngine6Test, renewExtendLeaseLifetimeForReservation) {
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::15"), 128);
+
+ // Create a lease for the client.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::15"),
+ duid_, iaid_, 300, 400,
+ subnet_->getID(), HWAddrPtr(), 128));
+
+ // Allocated 200 seconds ago - half of the lifetime.
+ time_t lease_cltt = time(NULL) - 200;
+ lease->cltt_ = lease_cltt;
+
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100);
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(IOAddress("2001:db8:1::15"), 128));
+
+ // Client should receive a lease.
+ Lease6Collection renewed = renewTest(engine, pool_, hints, true);
+ ASSERT_EQ(1, renewed.size());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(renewed[0]->cltt_, lease_cltt)
+ << "Lease lifetime was not extended, but it should";
+}
+
+// --- v6 host reservation ---
+
+// Checks that a client gets the address reserved (in-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an in-pool reservation.
+// - Client sends SOLICIT without any hints.
+// - Client is allocated a reserved address.
+//
+// Note that a DHCPv6 client can, but doesn't have to send any hints in its
+// Solicit message.
+TEST_F(AllocEngine6Test, reservedAddressInPoolSolicitNoHint) {
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1c"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("::"), true);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8:1::1c", lease->addr_.toText());
+}
+
+// Checks that a client gets the address reserved (in-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an in-pool reservation.
+// - Client sends REQUEST without any hints.
+// - Client is allocated a reserved address.
+//
+// Note that DHCPv6 client must send an address in REQUEST that the server
+// offered in Advertise. Nevertheless, the client may ignore this requirement.
+TEST_F(AllocEngine6Test, reservedAddressInPoolRequestNoHint) {
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1c"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("::"), false);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8:1::1c", lease->addr_.toText());
+
+ // Assigned count should have been incremented by one.
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+}
+
+// Checks that a client gets the address reserved (in-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an in-pool reservation.
+// - Client sends SOLICIT with a hint that does not match reservation
+// - Client is allocated a reserved address, not the hint.
+//
+// Note that DHCPv6 client can, but don't have to send any hints in its
+// Solicit message.
+TEST_F(AllocEngine6Test, reservedAddressInPoolSolicitValidHint) {
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1c"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Let's pretend the client sends hint 2001:db8:1::10.
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("2001:db8:1::10"), true);
+ ASSERT_TRUE(lease);
+
+ // The hint should be ignored and the reserved address should be assigned
+ EXPECT_EQ("2001:db8:1::1c", lease->addr_.toText());
+
+ // Assigned count should not have been incremented.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-nas", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-nas"));
+}
+
+// Checks that a client gets the address reserved (in-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an in-pool reservation.
+// - Client sends REQUEST with a hint that does not match reservation
+// - Client is allocated a reserved address, not the hint.
+//
+// Note that DHCPv6 client must send an address in REQUEST that the server
+// offered in Advertise. Nevertheless, the client may ignore this requirement.
+TEST_F(AllocEngine6Test, reservedAddressInPoolRequestValidHint) {
+ // Create reservation for the client This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1c"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Let's pretend the client sends hint 2001:db8:1::10.
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("2001:db8:1::10"), false);
+ ASSERT_TRUE(lease);
+
+ // The hint should be ignored and the reserved address should be assigned
+ EXPECT_EQ("2001:db8:1::1c", lease->addr_.toText());
+
+ // Assigned count should have been incremented.
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+}
+
+// Checks that a client gets the address reserved (in-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an in-pool reservation.
+// - Client sends SOLICIT with a hint that does matches reservation
+// - Client is allocated a reserved address, not the hint.
+//
+// Note that DHCPv6 client can, but don't have to send any hints in its
+// Solicit message.
+TEST_F(AllocEngine6Test, reservedAddressInPoolSolicitMatchingHint) {
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1c"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Let's pretend the client sends hint 2001:db8:1::10.
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("2001:db8:1::1c"), true);
+ ASSERT_TRUE(lease);
+
+ // The hint should be ignored and the reserved address should be assigned
+ EXPECT_EQ("2001:db8:1::1c", lease->addr_.toText());
+
+ // Assigned count should not have been incremented.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-nas", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-nas"));
+}
+
+// Checks that a client gets the address reserved (in-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an in-pool reservation.
+// - Client sends REQUEST with a hint that does not match reservation
+// - Client is allocated a reserved address, not the hint.
+//
+// Note that DHCPv6 client must send an address in REQUEST that the server
+// offered in Advertise. Nevertheless, the client may ignore this requirement.
+TEST_F(AllocEngine6Test, reservedAddressInPoolRequestMatchingHint) {
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1c"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Let's pretend the client sends hint 2001:db8:1::10.
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("2001:db8:1::1c"), false);
+ ASSERT_TRUE(lease);
+
+ // The hint should be ignored and the reserved address should be assigned
+ EXPECT_EQ("2001:db8:1::1c", lease->addr_.toText());
+
+ // Assigned count should have been incremented.
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+}
+
+// Checks that a client gets the address reserved (out-of-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an out-of-pool reservation.
+// - Client sends SOLICIT without any hints.
+// - Client is allocated a reserved address.
+//
+// Note that DHCPv6 client can, but don't have to send any hints in its
+// Solicit message.
+TEST_F(AllocEngine6Test, reservedAddressOutOfPoolSolicitNoHint) {
+ // Create reservation for the client. This is out-of-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8::abcd"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("::"), true, false);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8::abcd", lease->addr_.toText());
+
+ // Assigned count should not have been incremented.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-nas", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-nas"));
+}
+
+// Checks that a client gets the address reserved (out-of-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an out-of-pool reservation.
+// - Client sends REQUEST without any hints.
+// - Client is allocated a reserved address.
+//
+// Note that DHCPv6 client must send an address in REQUEST that the server
+// offered in Advertise. Nevertheless, the client may ignore this requirement.
+TEST_F(AllocEngine6Test, reservedAddressOutOfPoolRequestNoHint) {
+ // Create reservation for the client. This is out-of-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8::abcd"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("::"), false, false);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8::abcd", lease->addr_.toText());
+
+ // We should not have bumped the address counter
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-nas", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-nas"));
+}
+
+// Checks that a client gets the address reserved (in-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an in-pool reservation.
+// - Client sends SOLICIT with a hint that does not match reservation
+// - Client is allocated a reserved address, not the hint.
+//
+// Note that DHCPv6 client can, but don't have to send any hints in its
+// Solicit message.
+TEST_F(AllocEngine6Test, reservedAddressOutOfPoolSolicitValidHint) {
+ // Create reservation for the client. This is out-of-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8::abcd"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Let's pretend the client sends hint 2001:db8:1::10.
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("2001:db8:1::10"), true, false);
+ ASSERT_TRUE(lease);
+
+ // The hint should be ignored and the reserved address should be assigned
+ EXPECT_EQ("2001:db8::abcd", lease->addr_.toText());
+
+ // We should not have bumped the address counter
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-nas", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-nas"));
+}
+
+// Checks that a client gets the address reserved (out-of-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an out-of-pool reservation.
+// - Client sends REQUEST with a hint that does not match reservation
+// - Client is allocated a reserved address, not the hint.
+//
+// Note that DHCPv6 client must send an address in REQUEST that the server
+// offered in Advertise. Nevertheless, the client may ignore this requirement.
+TEST_F(AllocEngine6Test, reservedAddressOutOfPoolRequestValidHint) {
+ // Create reservation for the client. This is out-of-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8::abcd"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Let's pretend the client sends hint 2001:db8:1::10.
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("2001:db8:1::10"), false, false);
+ ASSERT_TRUE(lease);
+
+ // The hint should be ignored and the reserved address should be assigned
+ EXPECT_EQ("2001:db8::abcd", lease->addr_.toText());
+
+ // We should not have bumped the address counter
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-nas", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-nas"));
+}
+
+// Checks that a client gets the address reserved (out-of-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an out-of-pool reservation.
+// - Client sends SOLICIT with a hint that does matches reservation
+// - Client is allocated a reserved address, not the hint.
+//
+// Note that DHCPv6 client can, but don't have to send any hints in its
+// Solicit message.
+TEST_F(AllocEngine6Test, reservedAddressOutOfPoolSolicitMatchingHint) {
+ // Create reservation for the client. This is out-of-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8::abcd"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Let's pretend the client sends hint 2001:db8:1::10.
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("2001:db8:1::1c"), true, false);
+ ASSERT_TRUE(lease);
+
+ // The hint should be ignored and the reserved address should be assigned
+ EXPECT_EQ("2001:db8::abcd", lease->addr_.toText());
+
+ // We should not have bumped the address counter
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-nas", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-nas"));
+}
+
+// Checks that a client gets the address reserved (out-of-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an out-of-pool reservation.
+// - Client sends REQUEST with a hint that does not match reservation
+// - Client is allocated a reserved address, not the hint.
+//
+// Note that DHCPv6 client must send an address in REQUEST that the server
+// offered in Advertise. Nevertheless, the client may ignore this requirement.
+TEST_F(AllocEngine6Test, reservedAddressOutOfPoolRequestMatchingHint) {
+ // Create reservation for the client. This is out-of-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8::abcd"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Let's pretend the client sends hint 2001:db8:1::10.
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("2001:db8:1::1c"), false, false);
+ ASSERT_TRUE(lease);
+
+ // The hint should be ignored and the reserved address should be assigned
+ EXPECT_EQ("2001:db8::abcd", lease->addr_.toText());
+
+ // We should not have bumped the address counter
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-nas", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-nas"));
+}
+
+// In the following situation:
+// - client is assigned an address A
+// - HR is made for *this* client to get B
+// - client tries to get address A:
+// Check that his existing lease for lease A is removed
+// Check that he is assigned a new lease for B
+// - verify that the number of assigned address behaves as expected
+TEST_F(AllocEngine6Test, reservedAddressInPoolReassignedThis) {
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Client gets an address
+ Lease6Ptr lease1 = simpleAlloc6Test(pool_, IOAddress("::"), false);
+ ASSERT_TRUE(lease1);
+
+ // We should have bumped the address counter
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+
+ // Just check that if the client requests again, it will get the same
+ // address.
+ Lease6Ptr lease2 = simpleAlloc6Test(pool_, lease1->addr_, false);
+ ASSERT_TRUE(lease2);
+ detailCompareLease(lease1, lease2);
+
+ // We should not have bumped the address counter again
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+
+ // Now admin creates a reservation for this client. This is in-pool
+ // reservation, as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1c"), 128);
+
+ // Just check that this time the client will get.
+ Lease6Ptr lease3 = simpleAlloc6Test(pool_, lease1->addr_, false);
+ ASSERT_TRUE(lease3);
+
+ // Check that previous lease was not used anymore.
+ EXPECT_NE(lease1->addr_.toText(), lease3->addr_.toText());
+
+ // Check that the reserved address was indeed assigned.
+ EXPECT_EQ("2001:db8:1::1c", lease3->addr_.toText());
+
+ // Check that the old lease is gone.
+ Lease6Ptr old = LeaseMgrFactory::instance().getLease6(lease1->type_,
+ lease1->addr_);
+ EXPECT_FALSE(old);
+
+ // Check that the reserved lease is in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease1->type_,
+ IOAddress("2001:db8:1::1c"));
+
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease3, from_mgr);
+
+ // Lastly check to see that the address counter is still 1, we should have
+ // have decremented it on the implied release and incremented it on the reserved
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+}
+
+// In the following situation:
+// - client X is assigned an address A
+// - HR is made for client Y (*other* client) to get A
+// - client X tries to get address A:
+// Check that his existing lease for lease A is removed
+// Check that he is assigned a new lease
+TEST_F(AllocEngine6Test, reservedAddressInPoolReassignedOther) {
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Assigned count should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Client gets an address
+ Lease6Ptr lease1 = simpleAlloc6Test(pool_, IOAddress("::"), false);
+ ASSERT_TRUE(lease1);
+
+ // We should have bumped the address counter
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+
+ // Just check that if the client requests again, it will get the same
+ // address.
+ Lease6Ptr lease2 = simpleAlloc6Test(pool_, lease1->addr_, false);
+ ASSERT_TRUE(lease2);
+ detailCompareLease(lease1, lease2);
+
+ // We should not have bumped the address counter again
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+
+ // Now admin creates a reservation for this client. Let's use the
+ // address client X just received. Let's generate a host, but don't add it
+ // to the HostMgr yet.
+ HostPtr host = createHost6(false, IPv6Resrv::TYPE_NA, lease1->addr_, 128);
+
+ // We need to tweak reservation id: use a different DUID for client Y
+ vector<uint8_t> other_duid(8, 0x45);
+ host->setIdentifier(&other_duid[0], other_duid.size(), Host::IDENT_DUID);
+
+ // Ok, now add it to the HostMgr
+ addHost(host);
+
+ // Just check that this time the client will get a different lease.
+ Lease6Ptr lease3 = simpleAlloc6Test(pool_, lease1->addr_, false);
+ ASSERT_TRUE(lease3);
+
+ // Check that previous lease was not used anymore.
+ EXPECT_NE(lease1->addr_.toText(), lease3->addr_.toText());
+
+ // Check that the old lease is gone.
+ Lease6Ptr old = LeaseMgrFactory::instance().getLease6(lease1->type_,
+ lease1->addr_);
+ EXPECT_FALSE(old);
+
+ // Check that the reserved lease is in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease1->type_,
+ lease3->addr_);
+
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease3, from_mgr);
+
+ // Lastly check to see that the address counter is still 1 we should have
+ // have decremented it on the implied release and incremented it on the reserved
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+}
+
+// Checks that a reserved address for client A is not assigned when
+// other clients are requesting addresses. The scenario is as follows:
+// we use a regular pool with 17 addresses in it. One of them is
+// reserved for client A. Now we try to allocate addresses for 30 clients
+// (A is not one of them). The first 16 attempts should succeed. Then
+// we run out of addresses and remaining 14 clients will get nothing.
+// Finally, we check that client A still can get his reserved address.
+TEST_F(AllocEngine6Test, reservedAddress) {
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, true);
+
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::12"), 128);
+
+ // Let's generate 30 DUIDs, each of them 16 bytes long
+ vector<DuidPtr> clients;
+ for (int i = 0; i < 30; i++) {
+ vector<uint8_t> data(16, i);
+ clients.push_back(DuidPtr(new DUID(data)));
+ }
+
+ // The default pool is 2001:db8:1::10 to 2001:db8:1::20. There's 17
+ // addresses in it. One of them is reserved, so this means that we should
+ // get 16 successes and 14 (30-16) failures.
+ int success = 0;
+ int failure = 0;
+ for (int i = 0; i < 30; i++) {
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, clients[i], false, false, "",
+ false, query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ findReservation(engine, ctx);
+ Lease6Collection leases = engine.allocateLeases6(ctx);
+ if (leases.empty()) {
+ failure++;
+ std::cout << "Alloc for client " << (int)i << " failed." << std::endl;
+ EXPECT_EQ(failure, getStatistics("v6-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-shared-network"));
+ EXPECT_EQ(failure, getStatistics("v6-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes"));
+
+ EXPECT_EQ(failure, getStatistics("v6-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-shared-network", 1));
+ EXPECT_EQ(failure, getStatistics("v6-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes", 1));
+ } else {
+ success++;
+ std::cout << "Alloc for client " << (int)i << " succeeded:"
+ << leases[0]->addr_.toText() << std::endl;
+
+ // The assigned addresses must not match the one reserved.
+ EXPECT_NE("2001:db8:1::12", leases[0]->addr_.toText());
+ }
+ }
+
+ EXPECT_EQ(16, success);
+ EXPECT_EQ(14, failure);
+
+ // We're now pretty sure that any clients other than the reserved address
+ // will not get any service. Now let's check if the client that has the
+ // address reserved, will get it (despite the pool being depleted).
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ctx.currentIA().iaid_ = iaid_;
+
+ findReservation(engine, ctx);
+ Lease6Collection leases = engine.allocateLeases6(ctx);
+ ASSERT_EQ(1, leases.size());
+ EXPECT_EQ("2001:db8:1::12", leases[0]->addr_.toText());
+}
+
+// Checks if the allocateLeases throws exceptions for invalid input data.
+TEST_F(AllocEngine6Test, allocateLeasesInvalidData) {
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, true);
+
+ // That looks like a valid context.
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ctx.currentIA().iaid_ = iaid_;
+
+ Lease6Collection leases;
+
+ // Let's break it!
+ ctx.subnet_.reset();
+
+ // Subnet is required for allocation, so we should get no leases.
+ EXPECT_NO_THROW(leases = engine.allocateLeases6(ctx));
+ ASSERT_TRUE(leases.empty());
+
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes", 1));
+
+ // Let's fix this and break it in a different way.
+ ctx.subnet_ = subnet_;
+ ctx.duid_.reset();
+
+ // We must know who we're allocating for. No duid = no service.
+ EXPECT_NO_THROW(leases = engine.allocateLeases6(ctx));
+ ASSERT_TRUE(leases.empty());
+
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes"));
+
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-shared-network", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes", 1));
+}
+
+// Checks whether an address can be renewed (simple case, no reservation tricks)
+TEST_F(AllocEngine6Test, addressRenewal) {
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100);
+
+ // Assigned count should zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ Lease6Collection leases;
+
+ leases = allocateTest(engine, pool_, IOAddress("::"), false, true);
+ ASSERT_EQ(1, leases.size());
+
+ // Assigned count should be one.
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(leases[0]->addr_, 128));
+
+ Lease6Collection renewed = renewTest(engine, pool_, hints, true);
+ ASSERT_EQ(1, renewed.size());
+
+ // Check that the lease was indeed renewed and hasn't changed
+ // (i.e. the same address, preferred and valid lifetimes)
+
+ /// @todo: use leaseCompare, but ignore cltt_
+ EXPECT_EQ(leases[0]->addr_, renewed[0]->addr_);
+ EXPECT_EQ(leases[0]->type_, renewed[0]->type_);
+ EXPECT_EQ(leases[0]->preferred_lft_, renewed[0]->preferred_lft_);
+ EXPECT_EQ(leases[0]->valid_lft_, renewed[0]->valid_lft_);
+
+ // Assigned count should still be one.
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+}
+
+// Checks whether an address can be renewed (in-pool reservation)
+TEST_F(AllocEngine6Test, reservedAddressRenewal) {
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1c"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100);
+
+ // Assigned count should zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ Lease6Collection leases;
+
+ leases = allocateTest(engine, pool_, IOAddress("::"), false, true);
+ ASSERT_EQ(1, leases.size());
+ ASSERT_EQ("2001:db8:1::1c", leases[0]->addr_.toText());
+
+ // Assigned count should be one.
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(leases[0]->addr_, 128));
+
+ Lease6Collection renewed = renewTest(engine, pool_, hints, true);
+ ASSERT_EQ(1, renewed.size());
+ ASSERT_EQ("2001:db8:1::1c", leases[0]->addr_.toText());
+
+ // Assigned count should still be one.
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+}
+
+// Checks whether a single host can have more than one reservation.
+//
+/// @todo: as of #3677, this does not work. When processing solicit with two
+/// IA_NAs and two reservations, there currently no way to indicate that
+/// the first reservation should be used for the first IA and the second
+/// reservation for the second IA. This works for Requests and Renews, though.
+/// In both of those messages, when processing of the first IA is complete,
+/// we have a lease in the database. Based on that, when processing the second
+/// IA we can detect that the first reserved address is in use already and
+/// use the second reservation.
+TEST_F(AllocEngine6Test, DISABLED_reserved2AddressesSolicit) {
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ // Two addresses are reserved: 2001:db8:1::babe and 2001:db8:1::cafe
+ HostPtr host = createHost6(true, IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::babe"), 128);
+
+ IPv6Resrv resv2(IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::cafe"), 128);
+ host->addReservation(resv2);
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100);
+
+ AllocEngine::ClientContext6 ctx1(subnet_, duid_, false, false, "", true,
+ Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)));
+ ctx1.currentIA().iaid_ = iaid_;
+ ctx1.currentIA().type_ = pool_->getType();
+
+ Lease6Collection leases1;
+ findReservation(engine, ctx1);
+ EXPECT_NO_THROW(leases1 = engine.allocateLeases6(ctx1));
+ ASSERT_EQ(1, leases1.size());
+ EXPECT_EQ("2001:db8:1::babe", leases1[0]->addr_.toText());
+
+ // Double check that repeating the same duid/type/iaid will end up with
+ // the same address.
+ AllocEngine::ClientContext6 ctx2(subnet_, duid_, false, false, "", true,
+ Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)));
+ ctx2.currentIA().iaid_ = iaid_;
+ ctx2.currentIA().type_ = pool_->getType();
+
+ Lease6Collection leases2;
+ findReservation(engine, ctx2);
+ EXPECT_NO_THROW(leases2 = engine.allocateLeases6(ctx2));
+ EXPECT_EQ(1, leases2.size());
+ EXPECT_EQ("2001:db8:1::babe", leases2[0]->addr_.toText());
+
+ // Ok, now the tricky part. Request allocation for the same duid and type, but
+ // different iaid. The second address should be assigned.
+ AllocEngine::ClientContext6 ctx3(subnet_, duid_, false, false, "", true,
+ Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)));
+ ctx3.currentIA().iaid_ = iaid_ + 1;
+ ctx3.currentIA().type_ = pool_->getType();
+
+ Lease6Collection leases3;
+ findReservation(engine, ctx3);
+ EXPECT_NO_THROW(leases3 = engine.allocateLeases6(ctx3));
+ ASSERT_EQ(1, leases3.size());
+ EXPECT_EQ("2001:db8:1::cafe", leases3[0]->addr_.toText());
+}
+
+// Checks whether a single host can have more than one reservation.
+TEST_F(AllocEngine6Test, reserved2Addresses) {
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ // Two addresses are reserved: 2001:db8:1::babe and 2001:db8:1::cafe
+ HostPtr host = createHost6(true, IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::babe"), 128);
+
+ IPv6Resrv resv2(IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::cafe"), 128);
+ host->addReservation(resv2);
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100);
+
+ AllocEngine::ClientContext6 ctx1(subnet_, duid_, false, false, "", false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ctx1.currentIA().iaid_ = iaid_;
+ ctx1.currentIA().type_ = pool_->getType();
+
+ Lease6Collection leases1;
+ findReservation(engine, ctx1);
+ EXPECT_NO_THROW(leases1 = engine.allocateLeases6(ctx1));
+ ASSERT_EQ(1, leases1.size());
+ EXPECT_EQ("2001:db8:1::babe", leases1[0]->addr_.toText());
+
+ // Double check that repeating the same duid/type/iaid will end up with
+ // the same address.
+ AllocEngine::ClientContext6 ctx2(subnet_, duid_, false, false, "", false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ctx2.currentIA().iaid_ = iaid_;
+ ctx2.currentIA().type_ = pool_->getType();
+
+ Lease6Collection leases2;
+ findReservation(engine, ctx2);
+ EXPECT_NO_THROW(leases2 = engine.allocateLeases6(ctx2));
+ EXPECT_EQ(1, leases2.size());
+ EXPECT_EQ("2001:db8:1::babe", leases2[0]->addr_.toText());
+
+ // Ok, now the tricky part. Request allocation for the same duid and type, but
+ // different iaid. The second address should be assigned.
+ AllocEngine::ClientContext6 ctx3(subnet_, duid_, false, false, "", false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ctx3.currentIA().iaid_ = iaid_ + 1;
+ ctx3.currentIA().type_ = pool_->getType();
+
+ Lease6Collection leases3;
+ findReservation(engine, ctx3);
+ EXPECT_NO_THROW(leases3 = engine.allocateLeases6(ctx3));
+ ASSERT_EQ(1, leases3.size());
+ EXPECT_EQ("2001:db8:1::cafe", leases3[0]->addr_.toText());
+}
+
+// Checks whether address can change during renew (if there is a new
+// reservation for this client)
+TEST_F(AllocEngine6Test, reservedAddressRenewChange) {
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100);
+
+ Lease6Collection leases;
+
+ leases = allocateTest(engine, pool_, IOAddress("::"), false, true);
+ ASSERT_EQ(1, leases.size());
+ ASSERT_NE("2001:db8:1::1c", leases[0]->addr_.toText());
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(leases[0]->addr_, 128));
+
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1c"), 128);
+
+ Lease6Collection renewed = renewTest(engine, pool_, hints, true);
+ ASSERT_EQ(1, renewed.size());
+ ASSERT_EQ("2001:db8:1::1c", renewed[0]->addr_.toText());
+}
+
+// Checks whether address can change during renew (if there is a new
+// reservation for this address for another client)
+TEST_F(AllocEngine6Test, reservedAddressRenewReserved) {
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100);
+
+ Lease6Collection leases;
+
+ leases = allocateTest(engine, pool_, IOAddress("::"), false, true);
+ ASSERT_EQ(1, leases.size());
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(leases[0]->addr_, 128));
+
+ // Create reservation for this address, but for another client.
+ // This is in-pool reservation, as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ HostPtr host = createHost6(false, IPv6Resrv::TYPE_NA, leases[0]->addr_, 128);
+
+ // We need to tweak reservation id: use a different DUID for client Y
+ vector<uint8_t> other_duid(8, 0x45);
+ host->setIdentifier(&other_duid[0], other_duid.size(), Host::IDENT_DUID);
+ // Ok, now add it to the HostMgr
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ Lease6Collection renewed = renewTest(engine, pool_, hints, true);
+ ASSERT_EQ(1, renewed.size());
+
+ // Check that we no longer have the reserved address.
+ ASSERT_NE(leases[0]->addr_.toText(), renewed[0]->addr_.toText());
+
+ // Check that the lease for the now reserved address is no longer in
+ // the lease database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+ leases[0]->addr_);
+ EXPECT_FALSE(from_mgr);
+}
+
+/// @todo: The following methods are tested indirectly by allocateLeases6()
+/// tests, but could use more direct testing:
+/// - AllocEngine::allocateUnreservedLeases6
+/// - AllocEngine::allocateReservedLeases6
+/// - AllocEngine::removeNonmatchingReservedLeases6
+/// - AllocEngine::removeLeases
+/// - AllocEngine::removeNonreservedLeases6
+
+// Checks that a client gets the address reserved (in-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an in-pool reservation.
+// - Client sends SOLICIT without any hints.
+// - Client is allocated a reserved address.
+//
+// Note that DHCPv6 client can, but don't have to send any hints in its
+// Solicit message.
+TEST_F(AllocEngine6Test, reservedAddressByMacInPoolSolicitNoHint) {
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6HWAddr(true, IPv6Resrv::TYPE_NA, hwaddr_,
+ IOAddress("2001:db8:1::1c"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("::"), true);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8:1::1c", lease->addr_.toText());
+}
+
+// Checks that a client gets the address reserved (in-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an in-pool reservation.
+// - Client sends REQUEST without any hints.
+// - Client is allocated a reserved address.
+//
+// Note that DHCPv6 client must send an address in REQUEST that the server
+// offered in Advertise. Nevertheless, the client may ignore this requirement.
+TEST_F(AllocEngine6Test, reservedAddressByMacInPoolRequestNoHint) {
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6HWAddr(true, IPv6Resrv::TYPE_NA, hwaddr_,
+ IOAddress("2001:db8:1::1c"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("::"), false);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8:1::1c", lease->addr_.toText());
+}
+
+// Checks that a client gets the address reserved (in-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an in-pool reservation.
+// - Client sends SOLICIT with a hint that does not match reservation
+// - Client is allocated a reserved address, not the hint.
+//
+// Note that DHCPv6 client can, but don't have to send any hints in its
+// Solicit message.
+TEST_F(AllocEngine6Test, reservedAddressByMacInPoolSolicitValidHint) {
+ // Create reservation for the client. This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6HWAddr(true, IPv6Resrv::TYPE_NA, hwaddr_,
+ IOAddress("2001:db8:1::1c"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Let's pretend the client sends hint 2001:db8:1::10.
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("2001:db8:1::10"), true);
+ ASSERT_TRUE(lease);
+
+ // The hint should be ignored and the reserved address should be assigned
+ EXPECT_EQ("2001:db8:1::1c", lease->addr_.toText());
+}
+
+// Checks that a client gets the address reserved (in-pool case)
+// This test checks the behavior of the allocation engine in the following
+// scenario:
+// - Client has no lease in the database.
+// - Client has an in-pool reservation.
+// - Client sends REQUEST with a hint that does not match reservation
+// - Client is allocated a reserved address, not the hint.
+//
+// Note that DHCPv6 client must send an address in REQUEST that the server
+// offered in Advertise. Nevertheless, the client may ignore this requirement.
+TEST_F(AllocEngine6Test, reservedAddressByMacInPoolRequestValidHint) {
+ // Create reservation for the client This is in-pool reservation,
+ // as the pool is 2001:db8:1::10 - 2001:db8:1::20.
+ createHost6HWAddr(true, IPv6Resrv::TYPE_NA, hwaddr_,
+ IOAddress("2001:db8:1::1c"), 128);
+
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, false);
+
+ // Let's pretend the client sends hint 2001:db8:1::10.
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("2001:db8:1::10"), false);
+ ASSERT_TRUE(lease);
+
+ // The hint should be ignored and the reserved address should be assigned
+ EXPECT_EQ("2001:db8:1::1c", lease->addr_.toText());
+}
+
+// This test checks that the allocation engine can delegate the long prefix.
+// The pool with prefix of 64 and with long delegated prefix has a very
+// high capacity. The number of attempts that the allocation engine makes
+// to allocate the prefix for high capacity pools is equal to the capacity
+// value. This test verifies that the prefix can be allocated in that
+// case.
+TEST_F(AllocEngine6Test, largePDPool) {
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0);
+
+ // Remove the default PD pool.
+ subnet_->delPools(Lease::TYPE_PD);
+
+ // Configure the PD pool with the prefix length of /64 and the delegated
+ // length /96.
+ Pool6Ptr pool(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:1:2::"), 80, 96));
+ subnet_->addPool(pool);
+
+ // We should have got exactly one lease.
+ Lease6Collection leases = allocateTest(engine, pool, IOAddress("::"),
+ false, true);
+ ASSERT_EQ(1, leases.size());
+}
+
+// This test checks that the allocation engine can delegate addresses
+// from ridiculously large pool. The configuration provides 2^80 or
+// 1208925819614629174706176 addresses. We used to have a bug that would
+// confuse the allocation engine if the number of available addresses
+// was larger than 2^32.
+TEST_F(AllocEngine6Test, largePoolOver32bits) {
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 0);
+
+ // Configure 2001:db8::/32 subnet
+ subnet_.reset(new Subnet6(IOAddress("2001:db8::"), 32, 1, 2, 3, 4));
+
+ // Configure the NA pool of /48. So there are 2^80 addresses there. Make
+ // sure that we still can handle cases where number of available addresses
+ // is over max_uint64
+ Pool6Ptr pool(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::"), 48));
+ subnet_->addPool(pool);
+
+ // We should have got exactly one lease.
+ Lease6Collection leases = allocateTest(engine, pool, IOAddress("::"),
+ false, true);
+ ASSERT_EQ(1, leases.size());
+}
+
+// This test verifies that it is possible to override the number of allocation
+// attempts made by the allocation engine for a single lease.
+TEST_F(AllocEngine6Test, largeAllocationAttemptsOverride) {
+ // Remove the default NA pools.
+ subnet_->delPools(Lease::TYPE_NA);
+ subnet_->delPools(Lease::TYPE_PD);
+
+ // Add exactly one pool with many addresses.
+ Pool6Ptr pool(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::"), 56));
+ subnet_->addPool(pool);
+
+ // Allocate 5 addresses from the pool configured.
+ for (int i = 0; i < 5; ++i) {
+ DuidPtr duid = DuidPtr(new DUID(vector<uint8_t>(12,
+ static_cast<uint8_t>(i))));
+ // Get the unique IAID.
+ const uint32_t iaid = 3568 + i;
+
+ // Construct the unique address from the pool.
+ std::ostringstream address;
+ address << "2001:db8:1::";
+ address << i;
+
+ // Allocate the lease.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress(address.str()),
+ duid, iaid, 501, 502, subnet_->getID(),
+ HWAddrPtr(), 0));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+ }
+
+ // Try to use the allocation engine to allocate the lease. The iterative
+ // allocator will pick the addresses already allocated until it finds the
+ // available address. Since, we have restricted the number of attempts the
+ // allocation should fail.
+ AllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 3);
+ Lease6Collection leases = allocateTest(engine, pool_, IOAddress("::"),
+ false, true);
+ ASSERT_TRUE(leases.empty());
+
+ EXPECT_EQ(1, getStatistics("v6-allocation-fail"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-shared-network"));
+ EXPECT_EQ(1, getStatistics("v6-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes"));
+
+ EXPECT_EQ(1, getStatistics("v6-allocation-fail", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-shared-network", 1));
+ EXPECT_EQ(1, getStatistics("v6-allocation-fail-subnet", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools", 1));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes", 1));
+
+ // This time, lets allow more attempts, and expect that the allocation will
+ // be successful.
+ AllocEngine engine2(AllocEngine::ALLOC_ITERATIVE, 6);
+ leases = allocateTest(engine2, pool_, IOAddress("::"), false, true);
+ ASSERT_EQ(1, leases.size());
+}
+
+// This test checks if an expired declined lease can be reused in SOLICIT (fake allocation)
+TEST_F(AllocEngine6Test, solicitReuseDeclinedLease6) {
+
+ AllocEnginePtr engine(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100));
+ ASSERT_TRUE(engine);
+
+ // Now prepare a configuration with single address pool.
+ // Create one subnet with a pool holding one address.
+ string addr_txt("2001:db8:1::ad");
+ IOAddress addr(addr_txt);
+ initSubnet(IOAddress("2001:db8:1::"), addr, addr);
+
+ // Use information that is different than what we'll request
+ Lease6Ptr declined = generateDeclinedLease(addr_txt, 100, -10);
+ ASSERT_TRUE(declined->expired());
+
+ // CASE 1: Asking for any address
+ Lease6Ptr assigned;
+ testReuseLease6(engine, declined, "::", true, SHOULD_PASS, assigned);
+
+ // Check that we got that single lease
+ ASSERT_TRUE(assigned);
+ EXPECT_EQ(addr, assigned->addr_);
+
+ // Do all checks on the lease (if subnet-id, preferred/valid times are ok etc.)
+ checkLease6(duid_, assigned, Lease::TYPE_NA, 128);
+
+ // CASE 2: Asking specifically for this address
+ testReuseLease6(engine, declined, addr_txt, true, SHOULD_PASS, assigned);
+
+ // Check that we got that single lease
+ ASSERT_TRUE(assigned);
+ EXPECT_EQ(addr, assigned->addr_);
+}
+
+// This test checks if an expired declined lease can be reused when responding
+// to REQUEST (actual allocation)
+TEST_F(AllocEngine6Test, requestReuseDeclinedLease6) {
+
+ AllocEnginePtr engine(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100, true));
+ ASSERT_TRUE(engine);
+
+ // Now prepare a configuration with single address pool.
+ string addr_txt("2001:db8::7");
+ IOAddress addr(addr_txt);
+ initSubnet(IOAddress("2001:db8::"), addr, addr);
+
+ // Now create a declined lease, decline it and rewind its cltt, so it
+ // is expired.
+ Lease6Ptr declined = generateDeclinedLease(addr_txt, 100, -10);
+
+ // Asking specifically for this address
+ Lease6Ptr assigned;
+ testReuseLease6(engine, declined, addr_txt, false, SHOULD_PASS, assigned);
+ // Check that we got it.
+ ASSERT_TRUE(assigned);
+ EXPECT_EQ(addr, assigned->addr_);
+
+ // Check that the lease is indeed updated in LeaseMgr
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+ addr);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(assigned, from_mgr);
+}
+
+// This test checks if statistics are not updated when expired declined lease
+// is reused when responding to SOLICIT (fake allocation)
+TEST_F(AllocEngine6Test, solicitReuseDeclinedLease6Stats) {
+
+ // Now prepare for SOLICIT processing
+ AllocEnginePtr engine(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, true));
+ ASSERT_TRUE(engine);
+
+ // Now prepare a configuration with single address pool.
+ string addr_txt("2001:db8:1::1");
+ IOAddress addr(addr_txt);
+ initSubnet(IOAddress("2001:db8:1::"), addr, addr);
+
+ // Stats should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("declined-addresses", 0));
+ EXPECT_TRUE(testStatistics("reclaimed-declined-addresses", 0));
+ EXPECT_TRUE(testStatistics("declined-addresses", 0, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("reclaimed-declined-addresses", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Now create a declined lease, decline it and rewind its cltt, so it
+ // is expired.
+ Lease6Ptr declined = generateDeclinedLease(addr_txt, 100, -10);
+
+ // Ask for any address. There's only one address in the pool, so it doesn't
+ // matter much.
+ Lease6Ptr assigned;
+ testReuseLease6(engine, declined, "::", true, SHOULD_PASS, assigned);
+
+ // Check that the stats were not modified
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_EQ(cumulative,
+ getStatistics("cumulative-assigned-nas", subnet_->getID()));
+ EXPECT_EQ(glbl_cumulative, getStatistics("cumulative-assigned-nas"));
+ EXPECT_TRUE(testStatistics("declined-addresses", 0));
+ EXPECT_TRUE(testStatistics("reclaimed-declined-addresses", 0));
+ EXPECT_TRUE(testStatistics("declined-addresses", 0, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("reclaimed-declined-addresses", 0, subnet_->getID()));
+}
+
+// This test checks if statistics are updated when expired declined lease
+// is reused when responding to REQUEST (actual allocation)
+TEST_F(AllocEngine6Test, requestReuseDeclinedLease6Stats) {
+
+ // Prepare for REQUEST processing.
+ AllocEnginePtr engine(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, true));
+ ASSERT_TRUE(engine);
+
+ // Now prepare a configuration with single address pool.
+ string addr_txt("2001:db8::1");
+ IOAddress addr(addr_txt);
+ initSubnet(IOAddress("2001:db8::"), addr, addr);
+
+ // Stats should be zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("declined-addresses", 0));
+ EXPECT_TRUE(testStatistics("reclaimed-declined-addresses", 0));
+ EXPECT_TRUE(testStatistics("declined-addresses", 0, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("reclaimed-declined-addresses", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Now create a declined lease, decline it and rewind its cltt, so it
+ // is expired.
+ Lease6Ptr declined = generateDeclinedLease(addr_txt, 100, -10);
+
+ // Ask for any address. There's only one address in the pool, so it doesn't
+ // matter much.
+ Lease6Ptr assigned;
+ testReuseLease6(engine, declined, "::", false, SHOULD_PASS, assigned);
+
+ // Check that the stats were modified as expected.
+ // assigned-nas should NOT get incremented. Currently we do not adjust assigned
+ // counts when we declines
+ // declined-addresses will -1, as the artificial creation of declined lease
+ // doesn't increment it from zero. reclaimed-declined-addresses will be 1
+ // because the leases are implicitly reclaimed before they can be assigned.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+ EXPECT_TRUE(testStatistics("declined-addresses", -1));
+ EXPECT_TRUE(testStatistics("reclaimed-declined-addresses", 1));
+ EXPECT_TRUE(testStatistics("declined-addresses", -1, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("reclaimed-declined-addresses", 1, subnet_->getID()));
+}
+
+// This test checks if an expired-reclaimed lease can be reused by
+// a returning client via REQUEST, rather than renew/rebind. This
+// would be typical of cable modem clients which do not retain lease
+// data across reboots.
+TEST_F(AllocEngine6Test, reuseReclaimedExpiredViaRequest) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ IOAddress addr("2001:db8:1::ad");
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ cfg_mgr.clear(); // Get rid of the default test configuration
+
+ // Create configuration similar to other tests, but with a single address pool
+ subnet_ = Subnet6Ptr(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+ pool_ = Pool6Ptr(new Pool6(Lease::TYPE_NA, addr, addr)); // just a single address
+ subnet_->addPool(pool_);
+ cfg_mgr.getStagingCfg()->getCfgSubnets6()->add(subnet_);
+ cfg_mgr.commit();
+
+ // Verify relevant stats are zero.
+ EXPECT_TRUE(testStatistics("assigned-nas", 0, subnet_->getID()));
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 0));
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 0, subnet_->getID()));
+
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Let's create an expired lease
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid_,
+ 501, 502, subnet_->getID(), HWAddrPtr(),
+ 0));
+ lease->cltt_ = time(NULL) - 500; // Allocated 500 seconds ago
+ lease->valid_lft_ = 495; // Lease was valid for 495 seconds
+ lease->fqdn_fwd_ = true;
+ lease->fqdn_rev_ = true;
+ lease->hostname_ = "myhost.example.com.";
+ lease->state_ = Lease::STATE_EXPIRED_RECLAIMED;
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Verify that the lease state is indeed expired-reclaimed
+ EXPECT_EQ(lease->state_, Lease::STATE_EXPIRED_RECLAIMED);
+
+ // Same client comes along and issues a request
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+
+ // Check that he got the original lease back.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // Check that the lease is indeed updated in LeaseMgr
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+ addr);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+
+ // Verify that the lease state has been set back to the default.
+ EXPECT_EQ(lease->state_, Lease::STATE_DEFAULT);
+
+ // Verify assigned-nas got bumped. Reclaimed stats should still
+ // be zero as we artificially marked it reclaimed.
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 0));
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 0, subnet_->getID()));
+}
+
+/// @brief This test class is dedicated to testing shared networks
+///
+/// It uses one common configuration:
+/// 1 shared network with 2 subnets:
+/// - 2001:db8:1::/56 subnet with a small pool of single address
+/// - 2001:db8:1::/56 subnet with pool with 64K addresses.
+class SharedNetworkAlloc6Test : public AllocEngine6Test {
+public:
+ SharedNetworkAlloc6Test()
+ :engine_(AllocEngine::ALLOC_ITERATIVE, 0) {
+
+ subnet1_.reset(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+ subnet2_.reset(new Subnet6(IOAddress("2001:db8:2::"), 56, 1, 2, 3, 4));
+ pool1_.reset(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ IOAddress("2001:db8:1::1")));
+ pool2_.reset(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:2::"),
+ IOAddress("2001:db8:2::FF")));
+ subnet1_->addPool(pool1_);
+ subnet2_->addPool(pool2_);
+
+ // Both subnets belong to the same network so they can be used
+ // interchangeably.
+ network_.reset(new SharedNetwork6("test_network"));
+ network_->add(subnet1_);
+ network_->add(subnet2_);
+ }
+
+ Lease6Ptr
+ insertLease(std::string addr, SubnetID subnet_id) {
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress(addr), duid_, iaid_,
+ 501, 502, subnet_->getID(),
+ HWAddrPtr(), 0));
+ lease->cltt_ = time(NULL) - 10; // Allocated 10 seconds ago
+ if (!LeaseMgrFactory::instance().addLease(lease)) {
+ ADD_FAILURE() << "Failed to add a lease for address " << addr
+ << " in subnet with subnet-id " << subnet_id;
+ return (Lease6Ptr());
+ }
+ return (lease);
+ }
+
+ /// Convenience pointers to configuration elements. These are initialized
+ /// in the constructor and are used throughout the tests.
+ AllocEngine engine_;
+ Subnet6Ptr subnet1_;
+ Subnet6Ptr subnet2_;
+ Pool6Ptr pool1_;
+ Pool6Ptr pool2_;
+ SharedNetwork6Ptr network_;
+};
+
+// This test verifies that the server can offer an address from a
+// subnet and the introduction of shared network doesn't break anything here.
+TEST_F(SharedNetworkAlloc6Test, solicitSharedNetworkSimple) {
+
+ // Create context which will be used to try to allocate leases from the
+ // shared network. The context points to subnet1, which address space
+ // is exhausted. We expect the allocation engine to find another subnet
+ // within the same shared network and offer an address from there.
+ Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+ AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", true,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ ASSERT_TRUE(subnet1_->inRange(lease->addr_));
+}
+
+// This test verifies that the server can pick a subnet from shared subnets
+// based on hints.
+TEST_F(SharedNetworkAlloc6Test, solicitSharedNetworkHint) {
+
+ // Create context which will be used to try to allocate leases from the
+ // shared network. There's a hint that points to the subnet2. The
+ // shared network mechanism should be able to pick the second subnet
+ // based on it.
+ Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+ AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", true,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(IOAddress("2001:db8:2::12"));
+
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+
+ // The second subnet should be selected.
+ ASSERT_TRUE(subnet2_->inRange(lease->addr_));
+}
+
+// This test verifies that the client is offered an address from an
+// alternative subnet within shared network when the address pool is
+// exhausted in the first address pool.
+TEST_F(SharedNetworkAlloc6Test, solicitSharedNetworkOutOfAddresses) {
+
+ // Create a lease for a single address in the first address pool. The
+ // pool is now exhausted.
+ DuidPtr other_duid(new DUID(vector<uint8_t>(12, 0xff)));
+ const uint32_t other_iaid = 3568;
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ other_duid, other_iaid, 501, 502,
+ subnet1_->getID(),
+ HWAddrPtr(), 0));
+ lease->cltt_ = time(NULL) - 10; // Allocated 10 seconds ago
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create context which will be used to try to allocate leases from the
+ // shared network. The context points to subnet1, which address space
+ // is exhausted. We expect the allocation engine to find another subnet
+ // within the same shared network and offer an address from there.
+ Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+ AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", true,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ Lease6Ptr lease2;
+ ASSERT_NO_THROW(lease2 = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease2);
+ ASSERT_TRUE(subnet2_->inRange(lease2->addr_));
+
+ // The client having a lease should be offered this lease, even if
+ // the client starts allocation from the second subnet. The code should
+ // determine that the client has a lease in subnet1 and use this subnet
+ // instead.
+ AllocEngine::ClientContext6 ctx2(subnet2_, other_duid, false, false, "",
+ true, query);
+ ctx2.currentIA().iaid_ = other_iaid;
+ ASSERT_NO_THROW(lease2 = expectOneLease(engine_.allocateLeases6(ctx2)));
+ ASSERT_TRUE(lease2);
+ ASSERT_EQ("2001:db8:1::1", lease2->addr_.toText());
+
+ // Delete the lease in the first subnet.
+ ASSERT_TRUE(LeaseMgrFactory::instance().deleteLease(lease));
+
+ // Now, try requesting this address by providing a hint. The engine
+ // should try to honor the hint even though we start from the subnet2.
+ ctx.subnet_ = subnet2_;
+ ctx.currentIA().addHint(IOAddress("2001:db8:1::1"));
+ ASSERT_NO_THROW(lease2 = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease2);
+ ASSERT_TRUE(subnet1_->inRange(lease2->addr_));
+}
+
+// This test verifies that the server can offer an address from a
+// different subnet than orginally selected, when the address pool in
+// the first subnet is exhausted.
+TEST_F(SharedNetworkAlloc6Test, solicitSharedNetworkClassification) {
+ // Try to offer address from subnet1. There is an address available so
+ // it should be offered.
+ Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+ AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", true,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ ASSERT_TRUE(subnet1_->inRange(lease->addr_));
+
+ // Apply restrictions on the subnet1. This should be only assigned
+ // to clients belonging to cable-modem class.
+ subnet1_->allowClientClass("cable-modem");
+
+ // The allocation engine should determine that the subnet1 is not
+ // available for the client not belonging to the cable-modem class.
+ // Instead, it should offer an address from subnet2 that belongs
+ // to the same shared network.
+ AllocEngine::ClientContext6 ctx2(subnet1_, duid_, false, false, "", true,
+ query);
+ ctx2.currentIA().iaid_ = iaid_;
+ ctx2.query_ = query;
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx2)));
+ ASSERT_TRUE(lease);
+ ASSERT_TRUE(subnet2_->inRange(lease->addr_));
+
+ AllocEngine::ClientContext6 ctx3(subnet1_, duid_, false, false, "", true,
+ query);
+ ctx3.currentIA().iaid_ = iaid_;
+ ctx3.query_ = query;
+
+ // Create host reservation in the first subnet for this client. The
+ // allocation engine should not assign reserved address to the client
+ // because client classification doesn't allow that.
+ subnet_ = subnet1_;
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1"), 128);
+ AllocEngine::findReservation(ctx3);
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ ASSERT_TRUE(subnet2_->inRange(lease->addr_));
+
+ AllocEngine::ClientContext6 ctx4(subnet1_, duid_, false, false, "", true,
+ query);
+ ctx4.currentIA().iaid_ = iaid_;
+ ctx4.query_ = query;
+
+ // Assign cable-modem class and try again. This time, we should
+ // offer an address from the subnet1_.
+ ctx4.query_->addClass(ClientClass("cable-modem"));
+
+ AllocEngine::findReservation(ctx4);
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx4)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8:1::1", lease->addr_.toText());
+}
+
+// This test verifies that the server can offer an address from a
+// different subnet than orginally selected, when the address pool in
+// the first subnet requires another class.
+TEST_F(SharedNetworkAlloc6Test, solicitSharedNetworkPoolClassification) {
+ // Try to offer address from subnet1. There is an address available so
+ // it should be offered.
+ Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+ AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", true,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ ASSERT_TRUE(subnet1_->inRange(lease->addr_));
+
+ // Apply restrictions on the pool1. This should be only assigned
+ // to clients belonging to cable-modem class.
+ pool1_->allowClientClass("cable-modem");
+
+ // The allocation engine should determine that the pool1 is not
+ // available for the client not belonging to the cable-modem class.
+ // Instead, it should offer an address from subnet2 that belongs
+ // to the same shared network.
+ AllocEngine::ClientContext6 ctx2(subnet1_, duid_, false, false, "", true,
+ query);
+ ctx2.currentIA().iaid_ = iaid_;
+ ctx2.query_ = query;
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx2)));
+ ASSERT_TRUE(lease);
+ ASSERT_TRUE(subnet2_->inRange(lease->addr_));
+
+ AllocEngine::ClientContext6 ctx3(subnet1_, duid_, false, false, "", true,
+ query);
+ ctx3.currentIA().iaid_ = iaid_;
+ ctx3.query_ = query;
+
+ AllocEngine::ClientContext6 ctx4(subnet1_, duid_, false, false, "", true,
+ query);
+ ctx4.currentIA().iaid_ = iaid_;
+ ctx4.query_ = query;
+
+ // Assign cable-modem class and try again. This time, we should
+ // offer an address from the pool1_.
+ ctx4.query_->addClass(ClientClass("cable-modem"));
+
+ // Restrict access to pool2 for this client, to make sure that the
+ // server doesn't accidentally get a lease from this pool.
+ pool2_->allowClientClass("telephone");
+
+ AllocEngine::findReservation(ctx4);
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx4)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8:1::1", lease->addr_.toText());
+}
+
+// This test verifies that the client is offered a reserved address
+// even if this address belongs to another subnet within the same
+// shared network.
+TEST_F(SharedNetworkAlloc6Test, solicitSharedNetworkReservations) {
+ EXPECT_FALSE(HostMgr::instance().getDisableSingleQuery());
+
+ // Create reservation for the client in the second subnet.
+ subnet_ = subnet2_;
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:2::15"), 128);
+
+ // Start allocation from subnet1_. The engine should determine that the
+ // client has reservations in subnet2_ and should rather assign reserved
+ // addresses.
+ Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+ AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", true,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ // Find reservations for this subnet/shared network.
+ AllocEngine::findReservation(ctx);
+
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ ASSERT_EQ("2001:db8:2::15", lease->addr_.toText());
+}
+
+// This test verifies that the client is offered a reserved address
+// even if this address belongs to another subnet within the same
+// shared network. Host lookups returning a collection are disabled.
+// As it is only an optimization the behavior (so the test) must stay
+// unchanged.
+TEST_F(SharedNetworkAlloc6Test, solicitSharedNetworkReservationsNoColl) {
+ // Disable host lookups returning a collection.
+ ASSERT_FALSE(HostMgr::instance().getDisableSingleQuery());
+ HostMgr::instance().setDisableSingleQuery(true);
+
+ // Create reservation for the client in the second subnet.
+ subnet_ = subnet2_;
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:2::15"), 128);
+
+ // Start allocation from subnet1_. The engine should determine that the
+ // client has reservations in subnet2_ and should rather assign reserved
+ // addresses.
+ Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+ AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", true,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ // Find reservations for this subnet/shared network.
+ AllocEngine::findReservation(ctx);
+
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ ASSERT_EQ("2001:db8:2::15", lease->addr_.toText());
+}
+
+// This test verifies that the client is allocated a reserved address
+// even if this address belongs to another subnet within the same
+// shared network.
+TEST_F(SharedNetworkAlloc6Test, requestSharedNetworkReservations) {
+ EXPECT_FALSE(HostMgr::instance().getDisableSingleQuery());
+
+ // Create reservation for the client in the second subnet.
+ subnet_ = subnet2_;
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:2::15"), 128);
+
+ // Start allocation from subnet1_. The engine should determine that the
+ // client has reservations in subnet2_ and should rather assign reserved
+ // addresses.
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ // Find reservations for this subnet/shared network.
+ AllocEngine::findReservation(ctx);
+
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ ASSERT_EQ("2001:db8:2::15", lease->addr_.toText());
+}
+
+// This test verifies that the client is allocated a reserved address
+// even if this address belongs to another subnet within the same
+// shared network. Host lookups returning a collection are disabled.
+// As it is only an optimization the behavior (so the test) must stay
+// unchanged.
+TEST_F(SharedNetworkAlloc6Test, requestSharedNetworkReservationsNoColl) {
+ // Disable host lookups returning a collection.
+ ASSERT_FALSE(HostMgr::instance().getDisableSingleQuery());
+ HostMgr::instance().setDisableSingleQuery(true);
+
+ // Create reservation for the client in the second subnet.
+ subnet_ = subnet2_;
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:2::15"), 128);
+
+ // Start allocation from subnet1_. The engine should determine that the
+ // client has reservations in subnet2_ and should rather assign reserved
+ // addresses.
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ // Find reservations for this subnet/shared network.
+ AllocEngine::findReservation(ctx);
+
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ ASSERT_EQ("2001:db8:2::15", lease->addr_.toText());
+}
+
+// This test verifies that client is assigned an existing lease from a
+// shared network, regardless of the default subnet. It also verifies that
+// the client is assigned a reserved address from a shared network which
+// replaces existing lease within this shared network.
+TEST_F(SharedNetworkAlloc6Test, requestSharedNetworkExistingLeases) {
+ // Get the cumulative count of assigned addresses.
+ int64_t cumulative = getStatistics("cumulative-assigned-nas",
+ subnet2_->getID());
+ int64_t glbl_cumulative = getStatistics("cumulative-assigned-nas");
+
+ // Create a lease in subnet 2 for this client. The lease is in expired
+ // reclaimed state initially to allow for checking whether the lease
+ // gets renewed.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:2::1"),
+ duid_, iaid_, 501, 502,
+ subnet2_->getID(), HWAddrPtr(), 128));
+ lease->state_ = Lease::STATE_EXPIRED_RECLAIMED;
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create context which will be used to try to allocate leases from the
+ // shared network. The context points to subnet 1 initially, but the
+ // allocation engine should determine that there are existing leases
+ // in subnet 2 and renew those.
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ // Check that we have been allocated the existing lease.
+ Lease6Ptr lease2;
+ ASSERT_NO_THROW(lease2 = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease2);
+ EXPECT_EQ("2001:db8:2::1", lease2->addr_.toText());
+
+ // Statistics should be bumped when the lease is re-assigned.
+ EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet2_->getID()));
+ cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas",
+ cumulative, subnet2_->getID()));
+ glbl_cumulative += 1;
+ EXPECT_TRUE(testStatistics("cumulative-assigned-nas", glbl_cumulative));
+
+ // Another interesting case is when there is a reservation in a different
+ // subnet than the one from which the ease has been assigned.
+ subnet_ = subnet1_;
+ createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1"), 128);
+
+ // The reserved lease should take precedence.
+ ctx.subnet_ = subnet1_;
+ ctx.currentIA().iaid_ = iaid_;
+ AllocEngine::findReservation(ctx);
+ ASSERT_NO_THROW(lease2 = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease2);
+ EXPECT_EQ("2001:db8:1::1", lease2->addr_.toText());
+
+ // The previous lease should have been removed.
+ ASSERT_EQ(1, ctx.currentIA().old_leases_.size());
+ EXPECT_EQ("2001:db8:2::1", ctx.currentIA().old_leases_[0]->addr_.toText());
+}
+
+// This test verifies that the server can offer an address from a shared
+// subnet if there's at least 1 address left there, but will not offer
+// anything if both subnets are completely full.
+TEST_F(SharedNetworkAlloc6Test, requestRunningOut) {
+
+ // Allocate everything in subnet1
+ insertLease("2001:db8:1::1", subnet1_->getID());
+
+ // Allocate everything, except one address in subnet2.
+ for (int i = 0; i < 255; i++) {
+ stringstream tmp;
+ tmp << "2001:db8:2::" << hex << i;
+ insertLease(tmp.str(), subnet2_->getID());
+ }
+
+ // Create context which will be used to try to allocate leases from the
+ // shared network. The context points to subnet 1 initially, but the
+ // allocation engine should determine that there are existing leases
+ // in subnet 2 and renew those.
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx1(subnet1_, duid_, false, false, "", false,
+ query);
+ ctx1.currentIA().iaid_ = iaid_;
+
+ // Check that we have been allocated the existing lease (there's only
+ // one lease left, so we know exactly which one will be given out.
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx1)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8:2::ff", lease->addr_.toText());
+
+ // Ok, now try for another client. We should be completely full.
+ DuidPtr other_duid(new DUID(vector<uint8_t>(12, 0xff)));
+ AllocEngine::ClientContext6 ctx2(subnet2_, other_duid, false, false, "", false,
+ query);
+ Lease6Collection leases = engine_.allocateLeases6(ctx2);
+
+ // Bugger off, we're full!
+ ASSERT_TRUE(leases.empty());
+
+ EXPECT_EQ(1, getStatistics("v6-allocation-fail"));
+ EXPECT_EQ(1, getStatistics("v6-allocation-fail-shared-network"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-subnet"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools"));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes"));
+
+ EXPECT_EQ(1, getStatistics("v6-allocation-fail", 3));
+ EXPECT_EQ(1, getStatistics("v6-allocation-fail-shared-network", 3));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-subnet", 3));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-no-pools", 3));
+ EXPECT_EQ(0, getStatistics("v6-allocation-fail-classes", 3));
+}
+
+// Verifies that client with a hostname reservation can
+// 1. Get a dynamic lease
+// 2. Renew the same lease via REQUEST (calls allocateLease6)
+// 3. Renew the same lease via RENEW/REBIND (calls renewLeases6)
+// renew a dynamic lease from their selected subnet.
+TEST_F(AllocEngine6Test, hostDynamicAddress) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ HostPtr host(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
+ Host::IDENT_DUID, SUBNET_ID_UNUSED, subnet_->getID(),
+ asiolink::IOAddress("0.0.0.0")));
+ host->setHostname("host1");
+
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ subnet_->setReservationsInSubnet(true);
+
+ // Create context which will be used to try to allocate leases
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false, query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ // Look up the reservation.
+ findReservation(*engine, ctx);
+
+ // Make sure we found our host.
+ ConstHostPtr current = ctx.currentHost();
+ ASSERT_TRUE(current);
+ ASSERT_EQ("host1", current->getHostname());
+
+ // Check that we have been allocated a dynamic address.
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8:1::10", lease->addr_.toText());
+
+ // We're going to rollback the clock a little so we can verify a renewal.
+ ASSERT_NO_FATAL_FAILURE(rollbackPersistedCltt(lease));
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(IOAddress("2001:db8:1::10"), 128));
+
+ // Set test fixture hostname_ to the expected value. This gets checked in
+ // renewTest.
+ hostname_ = "host1";
+
+ // Client should receive a lease.
+ Lease6Collection renewed = renewTest(*engine, pool_, hints, true);
+ ASSERT_EQ(1, renewed.size());
+
+ Lease6Ptr renewed_lease = renewed[0];
+ EXPECT_EQ("2001:db8:1::10", renewed_lease->addr_.toText());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(renewed_lease->cltt_, lease->cltt_)
+ << "Lease lifetime was not extended, but it should";
+
+ // Now let's verify that if the client returns via SARR, they get the same
+ // lease and the cltt gets extended.
+
+ // First, we're going to rollback the clock again so we can verify the
+ // allocation updates the expiry.
+ ASSERT_NO_FATAL_FAILURE(rollbackPersistedCltt(renewed_lease));
+
+ // Create a new context which will be used to try to allocate leases
+ query.reset(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx2(subnet_, duid_, false, false, "", false, query);
+ ctx2.currentIA().iaid_ = iaid_;
+
+ // Look up the reservation.
+ findReservation(*engine, ctx2);
+
+ // Make sure we found our host.
+ current = ctx2.currentHost();
+ ASSERT_TRUE(current);
+ ASSERT_EQ("host1", current->getHostname());
+
+ // Check that we have been allocated the original dynamic address.
+ Lease6Ptr lease2;
+ ASSERT_NO_THROW(lease2 = expectOneLease(engine->allocateLeases6(ctx2)));
+ ASSERT_TRUE(lease2);
+ EXPECT_EQ("2001:db8:1::10", lease2->addr_.toText());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(lease2->cltt_, renewed_lease->cltt_)
+ << "Lease lifetime was not extended, but it should";
+}
+
+// Verifies that client with a global hostname reservation can:
+// 1. Get a dynamic lease
+// 2. Renew the same lease via REQUEST (calls allocateLease6)
+// 3. Renew the same lease via RENEW/REBIND (calls renewLeases6)
+TEST_F(AllocEngine6Test, globalHostDynamicAddress) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ HostPtr host(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
+ Host::IDENT_DUID, SUBNET_ID_UNUSED, SUBNET_ID_GLOBAL,
+ asiolink::IOAddress("0.0.0.0")));
+ host->setHostname("ghost1");
+
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ subnet_->setReservationsGlobal(true);
+
+ // Create context which will be used to try to allocate leases
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false, query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ // Look up the reservation.
+ findReservation(*engine, ctx);
+ // Make sure we found our host.
+ ConstHostPtr current = ctx.currentHost();
+ ASSERT_TRUE(current);
+ ASSERT_EQ("ghost1", current->getHostname());
+
+ // Check that we have been allocated a dynamic address.
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8:1::10", lease->addr_.toText());
+
+ // We're going to rollback the clock a little so we can verify a renewal.
+ ASSERT_NO_FATAL_FAILURE(rollbackPersistedCltt(lease));
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(IOAddress("2001:db8:1::10"), 128));
+
+ // Set test fixture hostname_ to the expected value. This gets checked in
+ // renewTest.
+ hostname_ = "ghost1";
+
+ // Client should receive a lease.
+ Lease6Collection renewed = renewTest(*engine, pool_, hints, true);
+ ASSERT_EQ(1, renewed.size());
+
+ // And the lease lifetime should be extended.
+ ASSERT_GT(renewed[0]->cltt_, lease->cltt_)
+ << "Lease lifetime was not extended, but it should";
+
+ // Now let's verify that if the client returns via SARR, they get the same
+ // lease. Create a new context which will be used to try to allocate leases
+
+ // First, we're going to rollback the clock again so we can verify the
+ // allocation updates the expiry.
+ Lease6Ptr renewed_lease = renewed[0];
+ ASSERT_NO_FATAL_FAILURE(rollbackPersistedCltt(renewed_lease));
+
+ query.reset(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx2(subnet_, duid_, false, false, "", false, query);
+ ctx2.currentIA().iaid_ = iaid_;
+
+ // Look up the reservation.
+ findReservation(*engine, ctx2);
+
+ // Make sure we found our host.
+ current = ctx2.currentHost();
+ ASSERT_TRUE(current);
+ ASSERT_EQ("ghost1", current->getHostname());
+
+ // Check that we have been allocated a dynamic address.
+ Lease6Ptr lease2;
+ ASSERT_NO_THROW(lease2 = expectOneLease(engine->allocateLeases6(ctx2)));
+ ASSERT_TRUE(lease2);
+ EXPECT_EQ("2001:db8:1::10", lease2->addr_.toText());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(lease2->cltt_, renewed_lease->cltt_)
+ << "Lease lifetime was not extended, but it should";
+}
+
+// Verifies that client with a global address reservation can get and
+// renew a lease for an arbitrary address.
+TEST_F(AllocEngine6Test, globalHostReservedAddress) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ HostPtr host(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
+ Host::IDENT_DUID, SUBNET_ID_UNUSED, SUBNET_ID_GLOBAL,
+ asiolink::IOAddress("0.0.0.0")));
+ host->setHostname("ghost1");
+ IPv6Resrv resv(IPv6Resrv::TYPE_NA, asiolink::IOAddress("3001::1"), 128);
+ host->addReservation(resv);
+
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ subnet_->setReservationsGlobal(true);
+
+ // Create context which will be used to try to allocate leases
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false, query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ // Look up the reservation.
+ findReservation(*engine, ctx);
+ // Make sure we found our host.
+ ConstHostPtr current = ctx.currentHost();
+ ASSERT_TRUE(current);
+ ASSERT_EQ("ghost1", current->getHostname());
+
+ // Check that we have been allocated the fixed address.
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("3001::1", lease->addr_.toText());
+
+ // We're going to rollback the clock a little so we can verify a renewal.
+ --lease->cltt_;
+ EXPECT_NO_THROW(LeaseMgrFactory::instance().updateLease6(lease));
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(IOAddress("3001::1"), 128));
+
+ // Set test fixture hostname_ to the expected value. This gets checked in
+ // renewTest.
+ hostname_ = "ghost1";
+
+ // Client should receive a lease.
+ Lease6Collection renewed = renewTest(*engine, pool_, hints, false);
+ ASSERT_EQ(1, renewed.size());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(renewed[0]->cltt_, lease->cltt_)
+ << "Lease lifetime was not extended, but it should";
+}
+
+// Verifies that client with a global prefix reservation can get and
+// renew a lease for an arbitrary prefix.
+TEST_F(AllocEngine6Test, globalHostReservedPrefix) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ HostPtr host(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
+ Host::IDENT_DUID, SUBNET_ID_UNUSED, SUBNET_ID_GLOBAL,
+ asiolink::IOAddress("0.0.0.0")));
+ host->setHostname("ghost1");
+ IPv6Resrv resv(IPv6Resrv::TYPE_PD, asiolink::IOAddress("3001::"), 64);
+ host->addReservation(resv);
+
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ subnet_->setReservationsGlobal(true);
+
+ // Create context which will be used to try to allocate leases
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false, query);
+ ctx.currentIA().type_ = Lease::TYPE_PD;
+ ctx.currentIA().iaid_ = iaid_;
+
+ // Look up the reservation.
+ findReservation(*engine, ctx);
+ // Make sure we found our host.
+ ConstHostPtr current = ctx.currentHost();
+ ASSERT_TRUE(current);
+ ASSERT_EQ("ghost1", current->getHostname());
+
+ // Check that we have been allocated the fixed address.
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("3001::", lease->addr_.toText());
+
+ // We're going to rollback the clock a little so we can verify a renewal.
+ --lease->cltt_;
+ EXPECT_NO_THROW(LeaseMgrFactory::instance().updateLease6(lease));
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(IOAddress("3001::"), 64));
+
+ // Set test fixture hostname_ to the expected value. This gets checked via
+ // renewTest.
+ hostname_ = "ghost1";
+
+ // We need a PD pool to fake renew_test
+ Pool6Ptr dummy_pool(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8::"), 64, 64));
+
+ // Client should receive a lease.
+ Lease6Collection renewed = renewTest(*engine, dummy_pool, hints, false);
+ ASSERT_EQ(1, renewed.size());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(renewed[0]->cltt_, lease->cltt_)
+ << "Lease lifetime was not extended, but it should";
+}
+
+// Verifies that client with a subnet address reservation can get and
+// renew a lease for an address in the subnet.
+TEST_F(AllocEngine6Test, mixedHostReservedAddress) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ HostPtr host(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
+ Host::IDENT_DUID, SUBNET_ID_UNUSED, subnet_->getID(),
+ asiolink::IOAddress("0.0.0.0")));
+ host->setHostname("mhost1");
+ IPv6Resrv resv(IPv6Resrv::TYPE_NA, asiolink::IOAddress("2001:db8:1::1c"), 128);
+ host->addReservation(resv);
+
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ subnet_->setReservationsGlobal(true);
+ subnet_->setReservationsInSubnet(true);
+ subnet_->setReservationsOutOfPool(false);
+
+ // Create context which will be used to try to allocate leases
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false, query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ // Look up the reservation.
+ findReservation(*engine, ctx);
+ // Make sure we found our host.
+ ConstHostPtr current = ctx.currentHost();
+ ASSERT_TRUE(current);
+ ASSERT_EQ("mhost1", current->getHostname());
+
+ // Check that we have been allocated the fixed address.
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8:1::1c", lease->addr_.toText());
+
+ // We're going to rollback the clock a little so we can verify a renewal.
+ --lease->cltt_;
+ EXPECT_NO_THROW(LeaseMgrFactory::instance().updateLease6(lease));
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(IOAddress("2001:db8:1::1c"), 128));
+
+ // Set test fixture hostname_ to the expected value. This gets checked in
+ // renewTest.
+ hostname_ = "mhost1";
+
+ // Client should receive a lease.
+ Lease6Collection renewed = renewTest(*engine, pool_, hints, true);
+ ASSERT_EQ(1, renewed.size());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(renewed[0]->cltt_, lease->cltt_)
+ << "Lease lifetime was not extended, but it should";
+}
+
+// Verifies that client with a subnet prefix reservation can get and
+// renew a lease for a prefix in the subnet.
+TEST_F(AllocEngine6Test, mixedHostReservedPrefix) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ HostPtr host(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
+ Host::IDENT_DUID, SUBNET_ID_UNUSED, subnet_->getID(),
+ asiolink::IOAddress("0.0.0.0")));
+ host->setHostname("mhost1");
+ IPv6Resrv resv(IPv6Resrv::TYPE_PD, asiolink::IOAddress("2001:db8:1:2::"), 64);
+ host->addReservation(resv);
+
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ subnet_->setReservationsGlobal(true);
+ subnet_->setReservationsInSubnet(true);
+
+ // Create context which will be used to try to allocate leases
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false, query);
+ ctx.currentIA().type_ = Lease::TYPE_PD;
+ ctx.currentIA().iaid_ = iaid_;
+
+ // Look up the reservation.
+ findReservation(*engine, ctx);
+ // Make sure we found our host.
+ ConstHostPtr current = ctx.currentHost();
+ ASSERT_TRUE(current);
+ ASSERT_EQ("mhost1", current->getHostname());
+
+ // Check that we have been allocated the fixed prefix.
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8:1:2::", lease->addr_.toText());
+
+ // We're going to rollback the clock a little so we can verify a renewal.
+ --lease->cltt_;
+ EXPECT_NO_THROW(LeaseMgrFactory::instance().updateLease6(lease));
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(IOAddress("2001:db8:1:2::"), 64));
+
+ // Set test fixture hostname_ to the expected value. This gets checked via
+ // renewTest.
+ hostname_ = "mhost1";
+
+ // We need a PD pool to fake renew_test
+ Pool6Ptr dummy_pool(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8::"), 64, 64));
+
+ // Client should receive a lease.
+ Lease6Collection renewed = renewTest(*engine, dummy_pool, hints, true);
+ ASSERT_EQ(1, renewed.size());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(renewed[0]->cltt_, lease->cltt_)
+ << "Lease lifetime was not extended, but it should";
+}
+
+// Verifies that client with a subnet and a global address reservation
+// can get and renew a lease for an address in the subnet.
+TEST_F(AllocEngine6Test, bothHostReservedAddress) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ HostPtr ghost(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
+ Host::IDENT_DUID, SUBNET_ID_UNUSED, SUBNET_ID_GLOBAL,
+ asiolink::IOAddress("0.0.0.0")));
+ ghost->setHostname("ghost1");
+ IPv6Resrv gresv(IPv6Resrv::TYPE_NA, asiolink::IOAddress("3001::1"), 128);
+ ghost->addReservation(gresv);
+
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(ghost);
+
+ HostPtr host(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
+ Host::IDENT_DUID, SUBNET_ID_UNUSED, subnet_->getID(),
+ asiolink::IOAddress("0.0.0.0")));
+ host->setHostname("mhost1");
+ IPv6Resrv resv(IPv6Resrv::TYPE_NA, asiolink::IOAddress("2001:db8:1::1c"), 128);
+ host->addReservation(resv);
+
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ subnet_->setReservationsGlobal(true);
+ subnet_->setReservationsInSubnet(true);
+ subnet_->setReservationsOutOfPool(false);
+
+ // Create context which will be used to try to allocate leases
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false, query);
+ ctx.currentIA().iaid_ = iaid_;
+
+ // Look up the reservation.
+ findReservation(*engine, ctx);
+ // Make sure we found our host.
+ ConstHostPtr current = ctx.currentHost();
+ ASSERT_TRUE(current);
+ ASSERT_EQ("mhost1", current->getHostname());
+
+ // Check that we have been allocated the fixed address.
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8:1::1c", lease->addr_.toText());
+
+ // We're going to rollback the clock a little so we can verify a renewal.
+ --lease->cltt_;
+ EXPECT_NO_THROW(LeaseMgrFactory::instance().updateLease6(lease));
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(IOAddress("2001:db8:1::1c"), 128));
+
+ // Set test fixture hostname_ to the expected value. This gets checked in
+ // renewTest.
+ hostname_ = "mhost1";
+
+ // Client should receive a lease.
+ Lease6Collection renewed = renewTest(*engine, pool_, hints, true);
+ ASSERT_EQ(1, renewed.size());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(renewed[0]->cltt_, lease->cltt_)
+ << "Lease lifetime was not extended, but it should";
+}
+
+// Verifies that client with a subnet and a global prefix reservation
+// can get and renew a lease for a prefix in the subnet.
+TEST_F(AllocEngine6Test, bothHostReservedPrefix) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ HostPtr ghost(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
+ Host::IDENT_DUID, SUBNET_ID_UNUSED, SUBNET_ID_GLOBAL,
+ asiolink::IOAddress("0.0.0.0")));
+ ghost->setHostname("ghost1");
+ IPv6Resrv gresv(IPv6Resrv::TYPE_PD, asiolink::IOAddress("3001::"), 64);
+ ghost->addReservation(gresv);
+
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(ghost);
+
+ HostPtr host(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
+ Host::IDENT_DUID, SUBNET_ID_UNUSED, subnet_->getID(),
+ asiolink::IOAddress("0.0.0.0")));
+ host->setHostname("mhost1");
+ IPv6Resrv resv(IPv6Resrv::TYPE_PD, asiolink::IOAddress("2001:db8:1:2::"), 64);
+ host->addReservation(resv);
+
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+
+ subnet_->setReservationsGlobal(true);
+ subnet_->setReservationsInSubnet(true);
+
+ // Create context which will be used to try to allocate leases
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false, query);
+ ctx.currentIA().type_ = Lease::TYPE_PD;
+ ctx.currentIA().iaid_ = iaid_;
+
+ // Look up the reservation.
+ findReservation(*engine, ctx);
+ // Make sure we found our host.
+ ConstHostPtr current = ctx.currentHost();
+ ASSERT_TRUE(current);
+ ASSERT_EQ("mhost1", current->getHostname());
+
+ // Check that we have been allocated the fixed prefix.
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ("2001:db8:1:2::", lease->addr_.toText());
+
+ // We're going to rollback the clock a little so we can verify a renewal.
+ --lease->cltt_;
+ EXPECT_NO_THROW(LeaseMgrFactory::instance().updateLease6(lease));
+
+ // This is what the client will send in his renew message.
+ AllocEngine::HintContainer hints;
+ hints.push_back(AllocEngine::Resource(IOAddress("2001:db8:1:2::"), 64));
+
+ // Set test fixture hostname_ to the expected value. This gets checked via
+ // renewTest.
+ hostname_ = "mhost1";
+
+ // We need a PD pool to fake renew_test
+ Pool6Ptr dummy_pool(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8::"), 64, 64));
+
+ // Client should receive a lease.
+ Lease6Collection renewed = renewTest(*engine, dummy_pool, hints, true);
+ ASSERT_EQ(1, renewed.size());
+
+ // And the lease lifetime should be extended.
+ EXPECT_GT(renewed[0]->cltt_, lease->cltt_)
+ << "Lease lifetime was not extended, but it should";
+}
+
+/// @brief Test fixture class for testing storage of extended lease data.
+/// It primarily creates several configuration items common to the
+/// extended info tests.
+class AllocEngine6ExtendedInfoTest : public AllocEngine6Test {
+public:
+ /// @brief Constructor
+ AllocEngine6ExtendedInfoTest()
+ : engine_(AllocEngine::ALLOC_ITERATIVE, 100, true), duid1_(), duid2_(),
+ relay1_(), relay2_(), duid1_addr_("::"), duid2_addr_("::") {
+ duid1_.reset(new DUID(std::vector<uint8_t>(8, 0x84)));
+ duid2_.reset(new DUID(std::vector<uint8_t>(8, 0x74)));
+
+ relay1_.msg_type_ = DHCPV6_RELAY_FORW;
+ relay1_.hop_count_ = 33;
+ relay1_.linkaddr_ = IOAddress("2001:db8::1");
+ relay1_.peeraddr_ = IOAddress("2001:db8::2");
+ relay1_.relay_msg_len_ = 0;
+
+ uint8_t relay_opt_data[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
+ vector<uint8_t> relay_data(relay_opt_data,
+ relay_opt_data + sizeof(relay_opt_data));
+ OptionPtr optRelay1(new Option(Option::V6, 200, relay_data));
+
+ relay1_.options_.insert(make_pair(optRelay1->getType(), optRelay1));
+
+ relay2_.msg_type_ = DHCPV6_RELAY_FORW;
+ relay2_.hop_count_ = 77;
+ relay2_.linkaddr_ = IOAddress("2001:db8::3");
+ relay2_.peeraddr_ = IOAddress("2001:db8::4");
+ relay2_.relay_msg_len_ = 0;
+
+ duid1_addr_ = IOAddress("2001:db8:1::10");
+ duid2_addr_ = IOAddress("2001:db8:1::11");
+
+ // Create the allocation engine, context and lease.
+ NakedAllocEngine engine(AllocEngine::ALLOC_ITERATIVE, 100, true);
+ }
+
+ /// Configuration elements. These are initialized in the constructor
+ /// and are used throughout the tests.
+ NakedAllocEngine engine_;
+ DuidPtr duid1_;
+ DuidPtr duid2_;
+ Pkt6::RelayInfo relay1_;
+ Pkt6::RelayInfo relay2_;
+ IOAddress duid1_addr_;
+ IOAddress duid2_addr_;
+};
+
+// Exercises AllocEnginer6Test::updateExtendedInfo6() through various
+// permutations of client packet content.
+TEST_F(AllocEngine6ExtendedInfoTest, updateExtendedInfo6) {
+ // Structure that defines a test scenario.
+ struct Scenario {
+ std::string description_; // test description
+ std::string orig_context_json_; // user context the lease begins with
+ std::vector<Pkt6::RelayInfo> relays_; // vector of relays from pkt
+ std::string exp_context_json_; // expected user context on the lease
+ bool exp_ret; // expected returned value
+ };
+
+ // Test scenarios.
+ std::vector<Scenario> scenarios {
+ {
+ "no context, no relay",
+ "",
+ {},
+ "",
+ false
+ },
+ {
+ "some original context, no relay",
+ "{\"foo\": 123}",
+ {},
+ "{\"foo\": 123}",
+ false
+ },
+ {
+ "no original context, one relay",
+ "",
+ { relay1_ },
+ "{ \"ISC\": { \"relays\": [ { \"hop\": 33, \"link\": \"2001:db8::1\","
+ " \"options\": \"0x00C800080102030405060708\", \"peer\": \"2001:db8::2\" } ] } }",
+ true
+ },
+ {
+ "some original context, one relay",
+ "{\"foo\": 123}",
+ { relay1_ },
+ "{ \"ISC\": { \"relays\": [ { \"hop\": 33, \"link\": \"2001:db8::1\","
+ " \"options\": \"0x00C800080102030405060708\", \"peer\": \"2001:db8::2\" } ] },"
+ " \"foo\": 123 }",
+ true
+ },
+ {
+ "no original context, two relays",
+ "",
+ { relay1_, relay2_ },
+ "{ \"ISC\": { \"relays\": [ { \"hop\": 33, \"link\": \"2001:db8::1\","
+ " \"options\": \"0x00C800080102030405060708\", \"peer\": \"2001:db8::2\" },"
+ " {\"hop\": 77, \"link\": \"2001:db8::3\", \"peer\": \"2001:db8::4\" } ] } }",
+ true
+ },
+ {
+ "original relay context, no relay",
+ "{ \"ISC\": { \"relays\": [ { \"hop\": 33, \"link\": \"2001:db8::1\","
+ " \"options\": \"0x00C800080102030405060708\", \"peer\": \"2001:db8::2\" } ] } }",
+ {},
+ "{ \"ISC\": { \"relays\": [ { \"hop\": 33, \"link\": \"2001:db8::1\","
+ " \"options\": \"0x00C800080102030405060708\", \"peer\": \"2001:db8::2\" } ] } }",
+ false
+ },
+ {
+ "original relay context, different relay",
+ "{ \"ISC\": { \"relays\": [ { \"hop\": 33, \"link\": \"2001:db8::1\","
+ " \"options\": \"0x00C800080102030405060708\", \"peer\": \"2001:db8::2\" } ] } }",
+ { relay2_ },
+ "{ \"ISC\": { \"relays\": [ { \"hop\": 77, \"link\": \"2001:db8::3\","
+ " \"peer\": \"2001:db8::4\" } ] } }",
+ true
+ }};
+
+ // Allocate a lease.
+ Lease6Ptr lease;
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)));
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+
+ // All scenarios require storage to be enabled.
+ ctx.subnet_->setStoreExtendedInfo(true);
+
+ // Verify that the lease begins with no user context.
+ ConstElementPtr user_context = lease->getContext();
+ ASSERT_FALSE(user_context);
+
+ // Iterate over the test scenarios.
+ ElementPtr orig_context;
+ ElementPtr exp_context;
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+
+ // Create the original user context from JSON.
+ if (scenario.orig_context_json_.empty()) {
+ orig_context.reset();
+ } else {
+ ASSERT_NO_THROW(orig_context = Element::fromJSON(scenario.orig_context_json_))
+ << "invalid orig_context_json_, test is broken";
+ }
+
+ // Create the expected user context from JSON.
+ if (scenario.exp_context_json_.empty()) {
+ exp_context.reset();
+ } else {
+ ASSERT_NO_THROW(exp_context = Element::fromJSON(scenario.exp_context_json_))
+ << "invalid exp_context_json_, test is broken";
+ }
+
+ // Initialize lease's user context.
+ lease->setContext(orig_context);
+ if (!orig_context) {
+ ASSERT_FALSE(lease->getContext());
+ } else {
+ ASSERT_TRUE(lease->getContext());
+ ASSERT_TRUE(orig_context->equals(*(lease->getContext())));
+ }
+
+ // Set the client packet relay vector from the scenario.
+ ctx.query_->relay_info_ = scenario.relays_;
+
+ // Call AllocEngine::updateLease6ExtendeInfo().
+ bool ret;
+ ASSERT_NO_THROW_LOG(ret = engine_.callUpdateLease6ExtendedInfo(lease, ctx));
+ ASSERT_EQ(scenario.exp_ret, ret);
+
+ // Verify the lease has the expected user context content.
+ if (!exp_context) {
+ ASSERT_FALSE(lease->getContext());
+ } else {
+ ASSERT_TRUE(lease->getContext());
+ ASSERT_TRUE(exp_context->equals(*(lease->getContext())))
+ << "expected: " << *(exp_context) << std::endl
+ << " actual: " << *(lease->getContext()) << std::endl;
+ }
+ }
+}
+
+// Verifies that the extended data (RelayInfos for now) is
+// added to a V6 lease when leases are created and/or renewed,
+// when store-extended-info is true.
+TEST_F(AllocEngine6ExtendedInfoTest, storeExtendedInfoEnabled6) {
+ // Structure that defines a test scenario.
+ struct Scenario {
+ std::string description_; // test description
+ DuidPtr duid_; // client DUID
+ std::vector<Pkt6::RelayInfo> relays_; // vector of relays from pkt
+ std::string exp_context_json_; // expected user context on the lease
+ IOAddress exp_address_; // expected lease address
+ };
+
+ // Test scenarios.
+ std::vector<Scenario> scenarios {
+ {
+ "create client one without relays",
+ duid1_,
+ {},
+ "",
+ duid1_addr_
+ },
+ {
+ "renew client one without relays",
+ DuidPtr(),
+ {},
+ "",
+ duid1_addr_
+ },
+ {
+ "create client two with relays",
+ duid2_,
+ { relay1_, relay2_ },
+ "{ \"ISC\": { \"relays\": [ { \"hop\": 33, \"link\": \"2001:db8::1\","
+ " \"options\": \"0x00C800080102030405060708\", \"peer\": \"2001:db8::2\" },"
+ " { \"hop\": 77, \"link\": \"2001:db8::3\", \"peer\": \"2001:db8::4\" } ] } }",
+ duid2_addr_
+ },
+ {
+ "renew client two without rai",
+ DuidPtr(),
+ {},
+ "{ \"ISC\": { \"relays\": [ { \"hop\": 33, \"link\": \"2001:db8::1\","
+ " \"options\": \"0x00C800080102030405060708\", \"peer\": \"2001:db8::2\" },"
+ " { \"hop\": 77, \"link\": \"2001:db8::3\", \"peer\": \"2001:db8::4\" } ] } }",
+ duid2_addr_
+ }};
+
+ // All of the scenarios require storage to be enabled.
+ subnet_->setStoreExtendedInfo(true);
+
+ // Iterate over the test scenarios.
+ DuidPtr current_duid;
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+
+ ElementPtr exp_context;
+ // Create the expected user context from JSON.
+ if (!scenario.exp_context_json_.empty()) {
+ ASSERT_NO_THROW(exp_context = Element::fromJSON(scenario.exp_context_json_))
+ << "invalid exp_context_json_, test is broken";
+ }
+
+ Pkt6Ptr pkt;
+ if (scenario.duid_) {
+ current_duid = scenario.duid_;
+ pkt.reset(new Pkt6(DHCPV6_REQUEST, 1234));
+ } else {
+ pkt.reset(new Pkt6(DHCPV6_RENEW, 1234));
+ }
+
+ // Set packet relay vector from the scenario.
+ pkt->relay_info_ = scenario.relays_;
+
+ // Create the context;
+ AllocEngine::ClientContext6 ctx(subnet_, current_duid, false, false, "", false, pkt);
+
+ // Create or renew the lease.
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+
+ EXPECT_EQ(scenario.exp_address_, lease->addr_);
+
+ // Verify the lease has the expected user context content.
+ if (!exp_context) {
+ ASSERT_FALSE(lease->getContext());
+ } else {
+ ASSERT_TRUE(lease->getContext());
+ ASSERT_TRUE(exp_context->equals(*(lease->getContext())))
+ << "expected: " << *(exp_context) << std::endl
+ << " actual: " << *(lease->getContext()) << std::endl;
+ }
+ }
+}
+
+// Verifies that the extended data (RelayInfos for now) is
+// not added to a V6 lease when leases are created and/or renewed,
+// when store-extended-info is false.
+TEST_F(AllocEngine6ExtendedInfoTest, storeExtendedInfoDisabled6) {
+ // Structure that defines a test scenario.
+ struct Scenario {
+ std::string description_; // test description
+ DuidPtr duid_; // client DUID
+ std::vector<Pkt6::RelayInfo> relays_; // vector of relays from pkt
+ IOAddress exp_address_; // expected lease address
+ };
+
+ // Test scenarios.
+ std::vector<Scenario> scenarios {
+ {
+ "create client one without relays",
+ duid1_,
+ {},
+ duid1_addr_
+ },
+ {
+ "renew client one without relays",
+ DuidPtr(),
+ {},
+ duid1_addr_
+ },
+ {
+ "create client two with relays",
+ duid2_,
+ { relay1_, relay2_ },
+ duid2_addr_
+ },
+ {
+ "renew client two with relays",
+ DuidPtr(),
+ { relay1_, relay2_ },
+ duid2_addr_
+ }
+ };
+
+ // All of the scenarios require storage to be disabled.
+ subnet_->setStoreExtendedInfo(false);
+
+ // Iterate over the test scenarios.
+ DuidPtr current_duid;
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+
+ Pkt6Ptr pkt;
+ if (scenario.duid_) {
+ current_duid = scenario.duid_;
+ pkt.reset(new Pkt6(DHCPV6_REQUEST, 1234));
+ } else {
+ pkt.reset(new Pkt6(DHCPV6_RENEW, 1234));
+ }
+
+ // Set packet relay vector from the scenario.
+ pkt->relay_info_ = scenario.relays_;
+
+ // Create the context;
+ AllocEngine::ClientContext6 ctx(subnet_, current_duid, false, false, "", false, pkt);
+
+ // Create or renew the lease.
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+
+ EXPECT_EQ(scenario.exp_address_, lease->addr_);
+
+ // Verify the lease had no user context content.
+ ASSERT_FALSE(lease->getContext());
+ }
+}
+
+// Verifies that the extended data (RelayInfos for now) is
+// added to a V6 lease when an expired lease is reused and
+// store-extended-info is true. We don't bother testing the
+// disabled case as this is tested thoroughly elsewhere.
+TEST_F(AllocEngine6ExtendedInfoTest, reuseExpiredLease6) {
+ // Create one subnet with a pool holding one address.
+ IOAddress addr("2001:db8:1::ad");
+ initSubnet(IOAddress("2001:db8:1::"), addr, addr);
+ subnet_->setPreferred(Triplet<uint32_t>(200, 300, 400));
+ subnet_->setValid(Triplet<uint32_t>(300, 400, 500));
+
+ // Create an expired lease for duid1_.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid1_, 1234,
+ 501, 502, subnet_->getID(),
+ HWAddrPtr(), 0));
+ lease->cltt_ = time(NULL) - 500; // Allocated 500 seconds ago
+ lease->valid_lft_ = 495; // Lease was valid for 495 seconds
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Make sure that we really created expired lease
+ ASSERT_TRUE(lease->expired());
+
+ // Asking specifically for this address with zero lifetimes
+ AllocEngine::ClientContext6 ctx(subnet_, duid2_, false, false, "", false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 5678)));
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(addr, 128, 0, 0);
+
+ // Add a relay to the packet relay vector.
+ ctx.query_->relay_info_.push_back(relay1_);
+
+ // Enable extended info storage.
+ subnet_->setStoreExtendedInfo(true);
+
+ // Reuse the expired lease.
+ EXPECT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+
+ // Check that we got that single lease
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+
+ // Now let's verify that the extended info is in the user-context.
+ ASSERT_TRUE(lease->getContext());
+ std::string exp_content_json =
+ "{ \"ISC\": { \"relays\": [ { \"hop\": 33, \"link\": \"2001:db8::1\","
+ " \"options\": \"0x00C800080102030405060708\", \"peer\": \"2001:db8::2\" } ] } }";
+ ConstElementPtr exp_context;
+ ASSERT_NO_THROW(exp_context = Element::fromJSON(exp_content_json))
+ << "invalid exp_context_json_, test is broken";
+ ASSERT_TRUE(exp_context->equals(*(lease->getContext())))
+ << "expected: " << *(exp_context) << std::endl
+ << " actual: " << *(lease->getContext()) << std::endl;
+}
+
+// Checks whether fake allocation does not use the cache feature.
+TEST_F(AllocEngine6Test, solicitNoCache) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ IOAddress addr("2001:db8:1::15");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid_,
+ 300, 400, subnet_->getID()));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ // Copy the lease, so as it can be compared with.
+ Lease6Ptr original_lease(new Lease6(*lease));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for fake allocation..
+ Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", true,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(addr);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+ EXPECT_EQ(128, lease->prefixlen_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+
+ // Check the lease was not updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(original_lease, from_mgr);
+}
+
+// Checks whether a lease can be reused (request) using cache threshold.
+TEST_F(AllocEngine6Test, requestCacheThreshold6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 33%.
+ subnet_->setCacheThreshold(.33);
+
+ IOAddress addr("2001:db8:1::15");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid_,
+ 300, 400, subnet_->getID()));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ // Copy the lease, so as it can be compared with.
+ Lease6Ptr original_lease(new Lease6(*lease));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for request.
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(addr);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+ EXPECT_EQ(128, lease->prefixlen_);
+
+ // The lease was reused.
+ time_t age = lease->cltt_ - now;
+ EXPECT_GE(age, 100);
+ EXPECT_LE(age, 110);
+ EXPECT_EQ(400 - age, lease->reuseable_valid_lft_);
+ EXPECT_EQ(300 - age, lease->reuseable_preferred_lft_);
+
+ // Check other lease parameters.
+ EXPECT_TRUE(*lease->duid_ == *duid_);
+ EXPECT_TRUE(ctx.isAllocated(addr));
+
+ // Check the lease was not updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(original_lease, from_mgr);
+}
+
+// Checks whether a lease can be reused (renew) using cache threshold.
+TEST_F(AllocEngine6Test, renewCacheThreshold6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ IOAddress prefix("2001:db8:1:2::");
+ uint8_t prefixlen = 80;
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_PD, prefix, duid_, iaid_,
+ 300, 400, subnet_->getID(),
+ HWAddrPtr(), prefixlen));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ // Copy the lease, so as it can be compared with.
+ Lease6Ptr original_lease(new Lease6(*lease));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for renew.
+ Pkt6Ptr query(new Pkt6(DHCPV6_RENEW, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().type_ = Lease::TYPE_PD;
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(prefix, prefixlen);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->renewLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(prefix, lease->addr_);
+ EXPECT_EQ(prefixlen, lease->prefixlen_);
+
+ // The lease was reused.
+ time_t age = lease->cltt_ - now;
+ EXPECT_GE(age, 100);
+ EXPECT_LE(age, 110);
+ EXPECT_EQ(400 - age, lease->reuseable_valid_lft_);
+ EXPECT_EQ(300 - age, lease->reuseable_preferred_lft_);
+
+ // Check other lease parameters.
+ EXPECT_TRUE(*lease->duid_ == *duid_);
+ EXPECT_TRUE(ctx.isAllocated(prefix, prefixlen));
+
+ // Check the lease was not updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(original_lease, from_mgr);
+}
+
+// Checks whether a lease can be reused (request) using cache max age.
+TEST_F(AllocEngine6Test, requestCacheMaxAge6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the max age to 150.
+ subnet_->setCacheMaxAge(150);
+
+ IOAddress addr("2001:db8:1::15");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid_,
+ 300, 400, subnet_->getID()));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ // Copy the lease, so as it can be compared with.
+ Lease6Ptr original_lease(new Lease6(*lease));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for request.
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(addr);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+ EXPECT_EQ(128, lease->prefixlen_);
+
+ // The lease was reused.
+ time_t age = lease->cltt_ - now;
+ EXPECT_GE(age, 100);
+ EXPECT_LE(age, 110);
+ EXPECT_EQ(400 - age, lease->reuseable_valid_lft_);
+ EXPECT_EQ(300 - age, lease->reuseable_preferred_lft_);
+
+ // Check other lease parameters.
+ EXPECT_TRUE(*lease->duid_ == *duid_);
+ EXPECT_TRUE(ctx.isAllocated(addr));
+
+ // Check the lease was not updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(original_lease, from_mgr);
+}
+
+// Checks whether a lease can be reused (renew) using cache max age.
+TEST_F(AllocEngine6Test, renewCacheMaxAge6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the max age to 150.
+ subnet_->setCacheMaxAge(150);
+
+ IOAddress prefix("2001:db8:1:2::");
+ uint8_t prefixlen = 80;
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_PD, prefix, duid_, iaid_,
+ 300, 400, subnet_->getID(),
+ HWAddrPtr(), prefixlen));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ // Copy the lease, so as it can be compared with.
+ Lease6Ptr original_lease(new Lease6(*lease));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for renew.
+ Pkt6Ptr query(new Pkt6(DHCPV6_RENEW, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().type_ = Lease::TYPE_PD;
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(prefix, prefixlen);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->renewLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(prefix, lease->addr_);
+ EXPECT_EQ(prefixlen, lease->prefixlen_);
+
+ // The lease was reused.
+ time_t age = lease->cltt_ - now;
+ EXPECT_GE(age, 100);
+ EXPECT_LE(age, 110);
+ EXPECT_EQ(400 - age, lease->reuseable_valid_lft_);
+ EXPECT_EQ(300 - age, lease->reuseable_preferred_lft_);
+
+ // Check other lease parameters.
+ EXPECT_TRUE(*lease->duid_ == *duid_);
+ EXPECT_TRUE(ctx.isAllocated(prefix, prefixlen));
+
+ // Check the lease was not updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(original_lease, from_mgr);
+}
+
+// Checks whether a lease can be reused (request) using both cache threshold
+// and max age.
+TEST_F(AllocEngine6Test, requestCacheBoth6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ // Set the max age to 150.
+ subnet_->setCacheMaxAge(150);
+
+ IOAddress addr("2001:db8:1::15");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid_,
+ 300, 400, subnet_->getID()));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ // Copy the lease, so as it can be compared with.
+ Lease6Ptr original_lease(new Lease6(*lease));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for request.
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(addr);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+ EXPECT_EQ(128, lease->prefixlen_);
+
+ // The lease was reused.
+ time_t age = lease->cltt_ - now;
+ EXPECT_GE(age, 100);
+ EXPECT_LE(age, 110);
+ EXPECT_EQ(400 - age, lease->reuseable_valid_lft_);
+ EXPECT_EQ(300 - age, lease->reuseable_preferred_lft_);
+
+ // Check other lease parameters.
+ EXPECT_TRUE(*lease->duid_ == *duid_);
+ EXPECT_TRUE(ctx.isAllocated(addr));
+
+ // Check the lease was not updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(original_lease, from_mgr);
+}
+
+// Checks whether a lease can be reused (renew) using both cache threshold
+// and max age.
+TEST_F(AllocEngine6Test, renewCacheBoth6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ // Set the max age to 150.
+ subnet_->setCacheMaxAge(150);
+
+ IOAddress prefix("2001:db8:1:2::");
+ uint8_t prefixlen = 80;
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_PD, prefix, duid_, iaid_,
+ 300, 400, subnet_->getID(),
+ HWAddrPtr(), prefixlen));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ // Copy the lease, so as it can be compared with.
+ Lease6Ptr original_lease(new Lease6(*lease));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for renew.
+ Pkt6Ptr query(new Pkt6(DHCPV6_RENEW, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().type_ = Lease::TYPE_PD;
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(prefix, prefixlen);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->renewLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(prefix, lease->addr_);
+ EXPECT_EQ(prefixlen, lease->prefixlen_);
+
+ // The lease was reused.
+ time_t age = lease->cltt_ - now;
+ EXPECT_GE(age, 100);
+ EXPECT_LE(age, 110);
+ EXPECT_EQ(400 - age, lease->reuseable_valid_lft_);
+ EXPECT_EQ(300 - age, lease->reuseable_preferred_lft_);
+
+ // Check other lease parameters.
+ EXPECT_TRUE(*lease->duid_ == *duid_);
+ EXPECT_TRUE(ctx.isAllocated(prefix, prefixlen));
+
+ // Check the lease was not updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(original_lease, from_mgr);
+}
+
+// Checks whether a lease can't be reused (request) using too small
+// cache threshold.
+TEST_F(AllocEngine6Test, requestCacheBadThreshold6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 10%.
+ subnet_->setCacheThreshold(.1);
+
+ // Set the max age to 150.
+ subnet_->setCacheMaxAge(150);
+
+ IOAddress addr("2001:db8:1::15");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid_,
+ 300, 400, subnet_->getID()));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for request.
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(addr);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+ EXPECT_EQ(128, lease->prefixlen_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+
+ // Check the lease was updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// Checks whether a lease can't be reused (renew) using too small
+// cache threshold.
+TEST_F(AllocEngine6Test, renewCacheBadThreshold6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 10%.
+ subnet_->setCacheThreshold(.1);
+
+ // Set the max age to 150.
+ subnet_->setCacheMaxAge(150);
+
+ IOAddress prefix("2001:db8:1:2::");
+ uint8_t prefixlen = 80;
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_PD, prefix, duid_, iaid_,
+ 300, 400, subnet_->getID(),
+ HWAddrPtr(), prefixlen));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for renew.
+ Pkt6Ptr query(new Pkt6(DHCPV6_RENEW, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().type_ = Lease::TYPE_PD;
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(prefix, prefixlen);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->renewLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(prefix, lease->addr_);
+ EXPECT_EQ(prefixlen, lease->prefixlen_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+
+ // Check the lease was updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// Checks whether a lease can't be reused (request) using too small
+// cache max age.
+TEST_F(AllocEngine6Test, requestCacheBadMaxAge6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ // Set the max age to 50.
+ subnet_->setCacheMaxAge(50);
+
+ IOAddress addr("2001:db8:1::15");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid_,
+ 300, 400, subnet_->getID()));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for request.
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(addr);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+ EXPECT_EQ(128, lease->prefixlen_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+
+ // Check the lease was updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// Checks whether a lease can't be reused (renew) using too small
+// cache max age.
+TEST_F(AllocEngine6Test, renewCacheBadMaxAge6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ // Set the max age to 50.
+ subnet_->setCacheMaxAge(50);
+
+ IOAddress prefix("2001:db8:1:2::");
+ uint8_t prefixlen = 80;
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_PD, prefix, duid_, iaid_,
+ 300, 400, subnet_->getID(),
+ HWAddrPtr(), prefixlen));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for renew.
+ Pkt6Ptr query(new Pkt6(DHCPV6_RENEW, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().type_ = Lease::TYPE_PD;
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(prefix, prefixlen);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->renewLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(prefix, lease->addr_);
+ EXPECT_EQ(prefixlen, lease->prefixlen_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+
+ // Check the lease was updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// Checks whether a lease can't be reused (renew) when the valid
+// lifetime was reduced.
+// This works only when the lifetime is recomputed.
+TEST_F(AllocEngine6Test, renewCacheReducedValid6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set valid lifetime to 200.
+ subnet_->setValid(200);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ IOAddress addr("2001:db8:1::15");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid_,
+ 300, 400, subnet_->getID()));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for renew.
+ Pkt6Ptr query(new Pkt6(DHCPV6_RENEW, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(addr);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->renewLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+ EXPECT_EQ(128, lease->prefixlen_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+
+ // Check the lease was updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// Checks whether a lease can't be reused (renew) when the preferred
+// lifetime was reduced.
+// This works only when the lifetime is recomputed.
+TEST_F(AllocEngine6Test, renewCacheReducedPreferred6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set preferred lifetime to 100.
+ subnet_->setPreferred(100);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ IOAddress prefix("2001:db8:1:2::");
+ uint8_t prefixlen = 80;
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_PD, prefix, duid_, iaid_,
+ 300, 400, subnet_->getID(),
+ HWAddrPtr(), prefixlen));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for renew.
+ Pkt6Ptr query(new Pkt6(DHCPV6_RENEW, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().type_ = Lease::TYPE_PD;
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(prefix, prefixlen);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->renewLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(prefix, lease->addr_);
+ EXPECT_EQ(prefixlen, lease->prefixlen_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+
+ // Check the lease was updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// Checks whether a lease can't be reused (request) when DDNS parameter changed.
+TEST_F(AllocEngine6Test, requestCacheFwdDDNS6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ IOAddress addr("2001:db8:1::15");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid_,
+ 300, 400, subnet_->getID()));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for request.
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, true, false, "", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(addr);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+ EXPECT_EQ(128, lease->prefixlen_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+
+ // Check the lease was updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// Checks whether a lease can't be reused (renew) when DDNS parameter changed.
+TEST_F(AllocEngine6Test, renewCacheFwdDDNS6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ IOAddress prefix("2001:db8:1:2::");
+ uint8_t prefixlen = 80;
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_PD, prefix, duid_, iaid_,
+ 300, 400, subnet_->getID(),
+ HWAddrPtr(), prefixlen));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for renew.
+ Pkt6Ptr query(new Pkt6(DHCPV6_RENEW, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, true, false, "", false,
+ query);
+ ctx.currentIA().type_ = Lease::TYPE_PD;
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(prefix, prefixlen);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->renewLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(prefix, lease->addr_);
+ EXPECT_EQ(prefixlen, lease->prefixlen_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+
+ // Check the lease was updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// Checks whether a lease can't be reused (request) when DDNS parameter changed.
+TEST_F(AllocEngine6Test, requestCacheRevDDNS6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ IOAddress addr("2001:db8:1::15");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid_,
+ 300, 400, subnet_->getID()));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for request.
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, true, "", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(addr);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+ EXPECT_EQ(128, lease->prefixlen_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+
+ // Check the lease was updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// Checks whether a lease can't be reused (renew) when DDNS parameter changed.
+TEST_F(AllocEngine6Test, renewCacheRevDDNS6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ IOAddress prefix("2001:db8:1:2::");
+ uint8_t prefixlen = 80;
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_PD, prefix, duid_, iaid_,
+ 300, 400, subnet_->getID(),
+ HWAddrPtr(), prefixlen));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for renew.
+ Pkt6Ptr query(new Pkt6(DHCPV6_RENEW, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, true, "", false,
+ query);
+ ctx.currentIA().type_ = Lease::TYPE_PD;
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(prefix, prefixlen);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->renewLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(prefix, lease->addr_);
+ EXPECT_EQ(prefixlen, lease->prefixlen_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+
+ // Check the lease was updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// Checks whether a lease can't be reused (request) when hostname changed.
+TEST_F(AllocEngine6Test, requestCacheHostname6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ IOAddress addr("2001:db8:1::15");
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid_,
+ 300, 400, subnet_->getID(),
+ false, false, "foo"));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for request.
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "bar", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(addr);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(addr, lease->addr_);
+ EXPECT_EQ(128, lease->prefixlen_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+ EXPECT_EQ("bar", lease->hostname_);
+
+ // Check the lease was updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// Checks whether a lease can't be reused (renew) when hostname changed.
+TEST_F(AllocEngine6Test, renewCacheHostname6) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Set the threshold to 25%.
+ subnet_->setCacheThreshold(.25);
+
+ IOAddress prefix("2001:db8:1:2::");
+ uint8_t prefixlen = 80;
+ time_t now = time(NULL) - 100; // Allocated 100 seconds ago.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_PD, prefix, duid_, iaid_,
+ 300, 400, subnet_->getID(),
+ false, false, "foo",
+ HWAddrPtr(), prefixlen));
+ lease->cltt_ = now;
+ ASSERT_FALSE(lease->expired());
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Create a context for renew.
+ Pkt6Ptr query(new Pkt6(DHCPV6_RENEW, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "bar", false,
+ query);
+ ctx.currentIA().type_ = Lease::TYPE_PD;
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(prefix, prefixlen);
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->renewLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(prefix, lease->addr_);
+ EXPECT_EQ(prefixlen, lease->prefixlen_);
+
+ // The lease was not reused.
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+ EXPECT_EQ("bar", lease->hostname_);
+
+ // Check the lease was updated in the database.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ detailCompareLease(lease, from_mgr);
+}
+
+// Verifies that AllocEngine::getLifetimes6() returns the appropriate
+// valid lifetime value based on the context content.
+TEST_F(AllocEngine6Test, getValidLifetime) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Let's make three classes, two with valid-lifetime and one without,
+ // and add them to the dictionary.
+ ClientClassDictionaryPtr dictionary =
+ CfgMgr::instance().getStagingCfg()->getClientClassDictionary();
+
+ ClientClassDefPtr class_def(new ClientClassDef("valid_one", ExpressionPtr()));
+ Triplet<uint32_t> valid_one(50, 100, 150);
+ class_def->setValid(valid_one);
+ dictionary->addClass(class_def);
+
+ class_def.reset(new ClientClassDef("valid_two", ExpressionPtr()));
+ Triplet<uint32_t>valid_two(200, 250, 300);
+ class_def->setValid(valid_two);
+ dictionary->addClass(class_def);
+
+ class_def.reset(new ClientClassDef("valid_unspec", ExpressionPtr()));
+ dictionary->addClass(class_def);
+
+ // Commit our class changes.
+ CfgMgr::instance().commit();
+
+ // Update the subnet's triplet to something more useful.
+ subnet_->setValid(Triplet<uint32_t>(500, 1000, 1500));
+
+ // Describes a test scenario.
+ struct Scenario {
+ std::string desc_; // descriptive text for logging
+ std::vector<std::string> classes_; // class list of assigned classes
+ uint32_t requested_lft_; // use as option 51 is > 0
+ uint32_t exp_valid_; // expected lifetime
+ };
+
+ // Scenarios to test.
+ std::vector<Scenario> scenarios = {
+ {
+ "no classes, no hint",
+ {},
+ 0,
+ subnet_->getValid()
+ },
+ {
+ "no classes, hint",
+ {},
+ subnet_->getValid().getMin() + 50,
+ subnet_->getValid().getMin() + 50
+ },
+ {
+ "no classes, hint too small",
+ {},
+ subnet_->getValid().getMin() - 50,
+ subnet_->getValid().getMin()
+ },
+ {
+ "no classes, hint too big",
+ {},
+ subnet_->getValid().getMax() + 50,
+ subnet_->getValid().getMax()
+ },
+ {
+ "class unspecified, no hint",
+ { "valid_unspec" },
+ 0,
+ subnet_->getValid()
+ },
+ {
+ "from last class, no hint",
+ { "valid_unspec", "valid_one" },
+ 0,
+ valid_one.get()
+ },
+ {
+ "from first class, no hint",
+ { "valid_two", "valid_one" },
+ 0,
+ valid_two.get()
+ },
+ {
+ "class plus hint",
+ { "valid_one" },
+ valid_one.getMin() + 25,
+ valid_one.getMin() + 25
+ },
+ {
+ "class plus hint too small",
+ { "valid_one" },
+ valid_one.getMin() - 25,
+ valid_one.getMin()
+ },
+ {
+ "class plus hint too big",
+ { "valid_one" },
+ valid_one.getMax() + 25,
+ valid_one.getMax()
+ }
+ };
+
+ // Iterate over the scenarios and verify the correct outcome.
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.desc_); {
+ // Create a context;
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", true,
+ Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)));
+ // Add client classes (if any)
+ for (auto class_name : scenario.classes_) {
+ ctx.query_->addClass(class_name);
+ }
+
+ // Add hint
+ ctx.currentIA().iaid_ = iaid_;
+
+ // prefix,prefixlen, preferred, valid
+ ctx.currentIA().addHint(IOAddress("::"), 128, 0, scenario.requested_lft_);
+
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(lease->valid_lft_, scenario.exp_valid_);
+ }
+ }
+}
+
+// Verifies that AllocEngine::getLifetimes6() returns the appropriate
+// preferred lifetime value based on the context content.
+TEST_F(AllocEngine6Test, getPreferredLifetime) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Let's make three classes, two with preferred-lifetime and one without,
+ // and add them to the dictionary.
+ ClientClassDictionaryPtr dictionary =
+ CfgMgr::instance().getStagingCfg()->getClientClassDictionary();
+
+ ClientClassDefPtr class_def(new ClientClassDef("preferred_one", ExpressionPtr()));
+ Triplet<uint32_t> preferred_one(50, 100, 150);
+ class_def->setPreferred(preferred_one);
+ dictionary->addClass(class_def);
+
+ class_def.reset(new ClientClassDef("preferred_two", ExpressionPtr()));
+ Triplet<uint32_t>preferred_two(200, 250, 300);
+ class_def->setPreferred(preferred_two);
+ dictionary->addClass(class_def);
+
+ class_def.reset(new ClientClassDef("preferred_unspec", ExpressionPtr()));
+ dictionary->addClass(class_def);
+
+ // Commit our class changes.
+ CfgMgr::instance().commit();
+
+ // Update the subnet's triplet to something more useful.
+ subnet_->setPreferred(Triplet<uint32_t>(500, 1000, 1500));
+
+ // Describes a test scenario.
+ struct Scenario {
+ std::string desc_; // descriptive text for logging
+ std::vector<std::string> classes_; // class list of assigned classes
+ uint32_t requested_lft_; // use as option 51 is > 0
+ uint32_t exp_preferred_; // expected lifetime
+ };
+
+ // Scenarios to test.
+ std::vector<Scenario> scenarios = {
+ {
+ "no classes, no hint",
+ {},
+ 0,
+ subnet_->getPreferred()
+ },
+ {
+ "no classes, hint",
+ {},
+ subnet_->getPreferred().getMin() + 50,
+ subnet_->getPreferred().getMin() + 50
+ },
+ {
+ "no classes, hint too small",
+ {},
+ subnet_->getPreferred().getMin() - 50,
+ subnet_->getPreferred().getMin()
+ },
+ {
+ "no classes, hint too big",
+ {},
+ subnet_->getPreferred().getMax() + 50,
+ subnet_->getPreferred().getMax()
+ },
+ {
+ "class unspecified, no hint",
+ { "preferred_unspec" },
+ 0,
+ subnet_->getPreferred()
+ },
+ {
+ "from last class, no hint",
+ { "preferred_unspec", "preferred_one" },
+ 0,
+ preferred_one.get()
+ },
+ {
+ "from first class, no hint",
+ { "preferred_two", "preferred_one" },
+ 0,
+ preferred_two.get()
+ },
+ {
+ "class plus hint",
+ { "preferred_one" },
+ preferred_one.getMin() + 25,
+ preferred_one.getMin() + 25
+ },
+ {
+ "class plus hint too small",
+ { "preferred_one" },
+ preferred_one.getMin() - 25,
+ preferred_one.getMin()
+ },
+ {
+ "class plus hint too big",
+ { "preferred_one" },
+ preferred_one.getMax() + 25,
+ preferred_one.getMax()
+ }
+ };
+
+ // Iterate over the scenarios and verify the correct outcome.
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.desc_); {
+ // Create a context;
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", true,
+ Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)));
+ // Add client classes (if any)
+ for (auto class_name : scenario.classes_) {
+ ctx.query_->addClass(class_name);
+ }
+
+ // Add hint
+ ctx.currentIA().iaid_ = iaid_;
+
+ // prefix,prefixlen, preferred, preferred
+ ctx.currentIA().addHint(IOAddress("::"), 128, scenario.requested_lft_, 0);
+
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(lease->preferred_lft_, scenario.exp_preferred_);
+ }
+ }
+}
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
diff --git a/src/lib/dhcpsrv/tests/alloc_engine_expiration_unittest.cc b/src/lib/dhcpsrv/tests/alloc_engine_expiration_unittest.cc
new file mode 100644
index 0000000..c758e47
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/alloc_engine_expiration_unittest.cc
@@ -0,0 +1,2323 @@
+// Copyright (C) 2015-2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcp/duid.h>
+#include <dhcp/option_data_types.h>
+#include <dhcp_ddns/ncr_msg.h>
+#include <dhcpsrv/tests/alloc_engine_utils.h>
+#include <dhcpsrv/testutils/test_utils.h>
+#include <hooks/hooks_manager.h>
+#include <stats/stats_mgr.h>
+#include <gtest/gtest.h>
+#include <boost/static_assert.hpp>
+#include <functional>
+#include <iomanip>
+#include <sstream>
+#include <time.h>
+#include <unistd.h>
+#include <vector>
+
+using namespace std;
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::dhcp_ddns;
+using namespace isc::hooks;
+using namespace isc::stats;
+namespace ph = std::placeholders;
+
+namespace {
+
+/// @brief Number of leases to be initialized for each test.
+///
+/// This value is expected by some of the tests to be multiple
+/// of 10.
+const unsigned int TEST_LEASES_NUM = 100;
+
+/// @brief Structure wrapping a lower limit within the collection
+/// of leases.
+///
+/// We're using this structure rather than a size_t value directly
+/// to make API of the test fixture class more readable, i.e. the
+/// struct name indicates the purpose of the value being held.
+struct LowerBound {
+ /// @brief Constructor.
+ ///
+ /// @param lower_bound Integer value wrapped by the structure.
+ explicit LowerBound(const size_t lower_bound)
+ : lower_bound_(lower_bound) { };
+
+ /// @brief Operator returning the size_t value wrapped.
+ operator size_t() const {
+ return (lower_bound_);
+ }
+
+ /// @brief Value wrapped in the structure.
+ size_t lower_bound_;
+};
+
+/// @brief Structure wrapping an upper limit within the collection
+/// of leases.
+///
+/// We're using this structure rather than a size_t value directly
+/// to make API of the test fixture class more readable, i.e. the
+/// struct name indicates the purpose of the value being held.
+struct UpperBound {
+ /// @brief Constructor.
+ ///
+ /// @param lower_bound Integer value wrapped by the structure.
+ explicit UpperBound(const size_t upper_bound)
+ : upper_bound_(upper_bound) { };
+
+ /// @brief Operator returning the size_t value wrapped.
+ operator size_t() const {
+ return (upper_bound_);
+ }
+
+ /// @brief Value wrapped in the structure.
+ size_t upper_bound_;
+};
+
+/// @brief List holding addresses for executed callouts.
+std::list<IOAddress> callouts_;
+
+/// @brief Callout argument name for expired lease.
+std::string callout_argument_name("lease4");
+
+/// @brief Base test fixture class for the lease reclamation routines in the
+/// @c AllocEngine.
+///
+/// This class implements infrastructure for testing leases reclamation
+/// routines. The lease reclamation routine has the following
+/// characteristic:
+/// - it processes multiple leases,
+/// - leases are processed in certain order,
+/// - number of processed leases may be limited by the parameters,
+/// - maximum duration of the lease reclamation routine may be limited,
+/// - reclaimed leases may be marked as reclaimed or deleted,
+/// - DNS records for some of the leases must be removed when the lease
+/// is reclaimed and DNS updates are enabled,
+/// - hooks must be invoked (if installed) for each reclaimed lease
+/// - statistics must be updated to increase the number of reclaimed
+/// leases and decrease the number of allocated leases
+///
+/// The typical test requires many leases to be initialized and stored
+/// in the lease database for the test. The test fixture class creates
+/// these leases upon construction. It is possible to modify these
+/// leases to test various properties of the lease reclamation routine
+/// as listed above. For example: some of the leases may be marked
+/// as expired or hostname may be cleared for some of the leases to
+/// check that DNS updates are not generated for them.
+///
+/// The tests are built around the
+/// @c ExpirationAllocEngineTest::testLeases methods. These methods
+/// verify that the certain operations have been performed by the
+/// lease reclamation routine on selected leases. The leases for which
+/// certain conditions should be met are selected using the "index
+/// algorithms". Various index algorithms are implemented in the
+/// test fixture class as static functions and the algorithm is
+/// selected by passing function pointer to the @c testLeases method.
+///
+/// Examples of index algorithms are:
+/// - evenLeaseIndex(index) - picks even index numbers,
+/// - oddLeaseIndex(index) - picks odd index numbers,
+/// - allLeasesIndexes(index) - picks all index number.
+///
+/// For example, the test may use the @c evenLeaseIndex algorithm
+/// to mark leases with even indexes as expired and then test whether
+/// leases with even indexes have been successfully reclaimed.
+///
+/// The "lease algorithm" verifies if the given lease fulfills the
+/// specific conditions after reclamation. These are the examples of
+/// the lease algorithms:
+/// - leaseExists - lease still exists in the database,
+/// - leaseDoesntExist - lease removed from the database,
+/// - leaseReclaimed - lease exists but has reclaimed status,
+/// - leaseNotReclaimed - lease exists and is not in the reclaimed status,
+/// - leaseDeclined - lease exists and is in declined state,
+/// - dnsUpdateGeneratedForLease - DNS updates generated for lease,
+/// - dnsUpdateNotGeneratedForLease - DNS updates not generated for lease
+///
+/// The combination of index algorithm and lease algorithm allows for
+/// verifying that the whole sets of leases in the lease database fulfill
+/// certain conditions. For example, it is possible to verify that
+/// after lease reclamation leases with even indexes have state set to
+/// "expired-reclaimed".
+///
+/// See @c ExpirationAllocEngineTest::testLeases for further details.
+///
+/// @todo These tests should be extended to cover the following cases:
+/// - declined leases - declined leases expire and should be removed
+/// from the lease database by the lease reclamation routine. See
+/// ticket #3976.
+template<typename LeasePtrType>
+class ExpirationAllocEngineTest : public ::testing::Test {
+public:
+
+ /// @brief Type definition for the lease algorithm.
+ typedef std::function<bool (const LeasePtrType)> LeaseAlgorithmFun;
+ /// @brief type definition for the lease index algorithm.
+ typedef std::function<bool (const size_t)> IndexAlgorithmFun;
+
+ /// @brief Constructor.
+ ///
+ /// Clears configuration, creates new lease manager and allocation engine
+ /// instances.
+ ExpirationAllocEngineTest(const std::string& lease_mgr_params) {
+ // Clear configuration.
+ CfgMgr::instance().clear();
+ D2ClientConfigPtr cfg(new D2ClientConfig());
+ CfgMgr::instance().setD2ClientConfig(cfg);
+
+ // Remove all statistics.
+ StatsMgr::instance().resetAll();
+
+ // Set the 'reclaimed-leases' statistics to '0'. This statistics
+ // is used by some tests to verify that the leases reclamation
+ // routine has been called.
+ StatsMgr::instance().setValue("reclaimed-leases",
+ static_cast<int64_t>(0));
+
+ // Create lease manager.
+ LeaseMgrFactory::create(lease_mgr_params);
+
+ // Create allocation engine instance.
+ engine_.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, true));
+ }
+
+ /// @brief Destructor
+ ///
+ /// Stops D2 client (if running), clears configuration and removes
+ /// an instance of the lease manager.
+ virtual ~ExpirationAllocEngineTest() {
+ // Stop D2 client if running and remove all queued name change
+ // requests.
+ D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+ if (mgr.amSending()) {
+ mgr.stopSender();
+ mgr.clearQueue();
+ }
+
+ // Clear configuration.
+ CfgMgr::instance().clear();
+ D2ClientConfigPtr cfg(new D2ClientConfig());
+ CfgMgr::instance().setD2ClientConfig(cfg);
+
+ // Remove all statistics.
+ StatsMgr::instance().resetAll();
+
+ // Kill lease manager.
+ LeaseMgrFactory::destroy();
+
+ // Remove callouts executed.
+ callouts_.clear();
+
+ // Unload libraries.
+ bool status = HooksManager::unloadLibraries();
+ if (!status) {
+ cerr << "(fixture dtor) unloadLibraries failed" << endl;
+ }
+ }
+
+ /// @brief Starts D2 client.
+ void enableDDNS() const {
+ // Start DDNS and assign no-op error handler.
+ D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+ D2ClientConfigPtr cfg(new D2ClientConfig());
+ cfg->enableUpdates(true);
+ mgr.setD2ClientConfig(cfg);
+ mgr.startSender(std::bind(&ExpirationAllocEngineTest::d2ErrorHandler, ph::_1, ph::_2));
+ }
+
+ /// @brief No-op error handler for the D2 client.
+ static void d2ErrorHandler(const dhcp_ddns::NameChangeSender::Result,
+ dhcp_ddns::NameChangeRequestPtr&) {
+ }
+
+ /// @brief Marks a lease as expired.
+ ///
+ /// @param lease_index Lease index. Must be between 0 and
+ /// @c TEST_LEASES_NUM.
+ /// @param secs Offset of the expiration time since now. For example
+ /// a value of 2 would set the lease expiration time to 2 seconds ago.
+ void expire(const uint16_t lease_index, const time_t secs) {
+ ASSERT_GT(leases_.size(), lease_index);
+ // We set the expiration time indirectly by modifying the cltt value.
+ leases_[lease_index]->cltt_ = time(NULL) - secs -
+ leases_[lease_index]->valid_lft_;
+ ASSERT_NO_THROW(updateLease(lease_index));
+ }
+
+ /// @brief Changes the owner of a lease.
+ ///
+ /// This method changes the owner of the lease. It must be implemented in
+ /// the derived classes to update the unique identifier(s) in the lease to
+ /// point to a different client.
+ ///
+ /// @param lease_index Lease index. Must be between 0 and
+ /// @c TEST_LEASES_NUM.
+ virtual void transferOwnership(const uint16_t lease_index) = 0;
+
+ /// @brief Marks lease as expired-reclaimed.
+ ///
+ /// @param lease_index Lease index. Must be between 0 and
+ /// @c TEST_LEASES_NUM.
+ /// @param secs Offset of the expiration time since now. For example
+ /// a value of 2 would set the lease expiration time to 2 seconds ago.
+ void reclaim(const uint16_t lease_index, const time_t secs) {
+ ASSERT_GT(leases_.size(), lease_index);
+ leases_[lease_index]->cltt_ = time(NULL) - secs -
+ leases_[lease_index]->valid_lft_;
+ leases_[lease_index]->state_ = Lease::STATE_EXPIRED_RECLAIMED;
+ ASSERT_NO_THROW(updateLease(lease_index));
+ }
+
+ /// @brief Declines specified lease
+ ///
+ /// Sets specified lease to declined state and sets its probation-period.
+ /// @param lease_index Index of the lease.
+ /// @param probation_time value of probation period to be set (in seconds)
+ void decline(const uint16_t lease_index, const time_t probation_time) {
+ ASSERT_GT(leases_.size(), lease_index);
+ leases_[lease_index]->decline(probation_time);
+ ASSERT_NO_THROW(updateLease(lease_index));
+ }
+
+ /// @brief Updates lease in the lease database.
+ ///
+ /// @param lease_index Index of the lease.
+ virtual void updateLease(const unsigned int lease_index) = 0;
+
+ /// @brief Retrieves lease from the database.
+ ///
+ /// @param lease_index Index of the lease.
+ virtual LeasePtrType getLease(const unsigned int lease_index) const = 0;
+
+ /// @brief Sets subnet id for a lease.
+ ///
+ /// It also updates statistics of assigned leases in the stats manager.
+ ///
+ /// @param lease_index Lease index.
+ /// @param id New subnet id.
+ virtual void setSubnetId(const uint16_t lease_index, const SubnetID& id) = 0;
+
+ /// @brief Wrapper method running lease reclamation routine.
+ ///
+ /// @param max_leases Maximum number of leases to be reclaimed.
+ /// @param timeout Maximum amount of time that the reclamation routine
+ /// may be processing expired leases, expressed in seconds.
+ /// @param remove_lease A boolean value indicating if the lease should
+ /// be removed when it is reclaimed (if true) or it should be left in the
+ /// database in the "expired-reclaimed" state (if false).
+ virtual void reclaimExpiredLeases(const size_t max_leases,
+ const uint16_t timeout,
+ const bool remove_lease) = 0;
+
+ /// @brief Wrapper method for removing expired-reclaimed leases.
+ ///
+ /// @param secs The minimum amount of time, expressed in seconds,
+ /// for the lease to be left in the "expired-reclaimed" state
+ /// before it can be removed.
+ virtual void deleteExpiredReclaimedLeases(const uint32_t secs) = 0;
+
+ /// @brief Test selected leases using the specified algorithms.
+ ///
+ /// This function picks leases from the range of 0 thru
+ /// @c TEST_LEASES_NUM and selects the ones to be verified using the
+ /// specified index algorithm. Selected leases are tested using
+ /// the specified lease algorithm.
+ ///
+ /// @param lease_algorithm Pointer to the lease algorithm function.
+ /// @param index_algorithm Pointer to the index algorithm function.
+ bool testLeases(const LeaseAlgorithmFun& lease_algorithm,
+ const IndexAlgorithmFun& index_algorithm) const {
+ // No limits are specified, so test all leases in the range of
+ // 0 .. TEST_LEASES_NUM.
+ return (testLeases(lease_algorithm, index_algorithm, LowerBound(0),
+ UpperBound(TEST_LEASES_NUM)));
+ }
+
+
+ /// @brief Test selected leases using the specified algorithms.
+ ///
+ /// This function picks leases from the range of @c lower_bound
+ /// thru @c upper_bound and selects the ones to be verified using the
+ /// specified index algorithm. Selected leases are tested using the
+ /// specified lease algorithm.
+ ///
+ /// @param lease_algorithm Pointer to the lease algorithm function.
+ /// @param index_algorithm Pointer to the index algorithm function.
+ /// @param lower_bound First index in the range.
+ /// @param upper_bound Last + 1 index in the range.
+ bool testLeases(const LeaseAlgorithmFun& lease_algorithm,
+ const IndexAlgorithmFun& index_algorithm,
+ const LowerBound& lower_bound,
+ const UpperBound& upper_bound) const {
+ // Select leases between the lower_bound and upper_bound.
+ for (size_t i = lower_bound; i < upper_bound; ++i) {
+ // Get the lease from the lease database.
+ LeasePtrType lease = getLease(i);
+ // index_algorithm(i) checks if the lease should be checked.
+ // If so, check if the lease_algorithm indicates that the
+ // lease fulfills a given condition, e.g. is present in the
+ // database. If not, return false.
+ if (index_algorithm(i) && !lease_algorithm(lease)) {
+ return (false);
+ }
+ }
+ // All leases checked, so return true.
+ return (true);
+ }
+
+ /// @brief Index algorithm selecting even indexes.
+ ///
+ /// @param index Lease index.
+ /// @return true if index is an even number.
+ static bool evenLeaseIndex(const size_t index) {
+ return (index % 2 == 0);
+ }
+
+ /// @brief Index algorithm selecting odd indexes.
+ ///
+ /// @param index Lease index.
+ /// @return true if index is an odd number.
+ static bool oddLeaseIndex(const size_t index) {
+ return (!evenLeaseIndex(index));
+ }
+
+ /// @brief Index algorithm selecting all indexes.
+ ///
+ /// @param index Lease index.
+ /// @return true if the index is in the range of [0 .. TEST_LEASES_NUM).
+ static bool allLeaseIndexes(const size_t index) {
+ return (index < TEST_LEASES_NUM);
+ }
+
+ /// @brief Lease algorithm checking if lease exists.
+ ///
+ /// @param lease Pointer to lease.
+ /// @return true if lease pointer is non-null.
+ static bool leaseExists(const LeasePtrType& lease) {
+ return (static_cast<bool>(lease));
+ }
+
+ /// @brief Lease algorithm checking if lease doesn't exist.
+ ///
+ /// @param lease Pointer to lease.
+ /// @return true if lease pointer is null.
+ static bool leaseDoesntExist(const LeasePtrType& lease) {
+ return (static_cast<bool>(!lease));
+ }
+
+ /// @brief Lease algorithm checking if lease state is expired-reclaimed.
+ ///
+ /// This algorithm also checks that the FQDN information has been removed
+ /// from the lease.
+ ///
+ /// @param lease Pointer to lease.
+ /// @return true if lease state is "expired-reclaimed" and the FQDN
+ /// information has been removed from the lease.
+ static bool leaseReclaimed(const LeasePtrType& lease) {
+ return (lease && lease->stateExpiredReclaimed() &&
+ lease->hostname_.empty() && !lease->fqdn_fwd_ &&
+ !lease->fqdn_rev_);
+ }
+
+ /// @brief Lease algorithm checking if lease state is Declined.
+ ///
+ /// @param lease Pointer to lease.
+ /// @return true if lease state is "declined".
+ static bool leaseDeclined(const LeasePtrType& lease) {
+ return (lease && lease->stateDeclined());
+ }
+
+ /// @brief Lease algorithm checking if lease state is not
+ /// expired-reclaimed.
+ ///
+ /// @param lease Pointer to lease.
+ /// @return true if lease state is not "expired-reclaimed".
+ static bool leaseNotReclaimed(const LeasePtrType& lease) {
+ return (lease && !lease->stateExpiredReclaimed());
+ }
+
+ /// @brief Lease algorithm checking if removal name change request
+ /// has been generated for lease.
+ ///
+ /// @param lease Pointer to lease.
+ /// @return true if NCR has been generated for the lease.
+ static bool dnsUpdateGeneratedForLease(const LeasePtrType& lease) {
+ try {
+ return (static_cast<bool>(getNCRForLease(lease)));
+
+ } catch (...) {
+ // If error occurred, treat it as no match.
+ return (false);
+ }
+ }
+
+ /// @brief Lease algorithm checking if removal name change request
+ /// hasn't been generated for lease.
+ ///
+ /// @param lease Pointer to lease.
+ /// @return true if NCR has not been generated for the lease.
+ static bool dnsUpdateNotGeneratedForLease(const LeasePtrType& lease) {
+ try {
+ // Iterate over the generated name change requests and try
+ // to find the match with our lease (using IP address). If
+ D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+ for (size_t i = 0; i < mgr.getQueueSize(); ++i) {
+ const NameChangeRequestPtr& ncr = mgr.peekAt(i);
+ // If match found, we treat it as if the test fails
+ // because we expected no NCR.
+ if (ncr->getIpAddress() == lease->addr_.toText()) {
+ return (false);
+ }
+ }
+ } catch (...) {
+ return (false);
+ }
+
+ // No match found, so we're good.
+ return (true);
+ }
+
+ /// @brief Lease algorithm checking if callout has been executed for
+ /// the expired lease.
+ ///
+ /// @param lease Pointer to lease.
+ /// @return true if callout has been executed for the lease.
+ static bool leaseCalloutExecuted(const LeasePtrType& lease) {
+ return (std::find(callouts_.begin(), callouts_.end(), lease->addr_) !=
+ callouts_.end());
+ }
+
+ /// @brief Lease algorithm checking if callout hasn't been executed for
+ /// the expired lease.
+ ///
+ /// @param lease Pointer to lease.
+ /// @return true if callout hasn't been executed for the lease.
+ static bool leaseCalloutNotExecuted(const LeasePtrType& lease) {
+ return (!leaseCalloutExecuted(lease));
+ }
+
+ /// @brief Implements "lease{4,6}_expire" callout.
+ ///
+ /// @param callout_handle Callout handle.
+ /// @return Zero.
+ static int leaseExpireCallout(CalloutHandle& callout_handle) {
+ LeasePtrType lease;
+ callout_handle.getArgument(callout_argument_name, lease);
+ bool remove_lease = true;
+ callout_handle.getArgument("remove_lease", remove_lease);
+
+ // Check if the remove_lease is set to false and assume that the callout
+ // has been successfully executed if it is. This is mainly to test
+ // that the lease reclamation routine sets this value at all.
+ if (!remove_lease) {
+ callouts_.push_back(lease->addr_);
+ }
+
+ return (0);
+ }
+
+ /// @brief Implements "lease{4,6}_expire callout returning skip flag.
+ ///
+ /// @param callout_handle Callout handle.
+ /// @return Zero.
+ static int leaseExpireWithSkipCallout(CalloutHandle& callout_handle) {
+ leaseExpireCallout(callout_handle);
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ return (0);
+ }
+
+ /// @brief Implements "lease{4,6}_expire callout, which lasts at least
+ /// 40ms.
+ ///
+ /// This callout is useful to test scenarios where the reclamation of the
+ /// lease needs to take a known amount of time. If the callout is installed
+ /// it will take at least 40ms for each lease. It is then possible to calculate
+ /// the approximate time that the reclamation of all leases would take and
+ /// test that the timeouts for the leases' reclamation work as expected.
+ ///
+ /// The value of 40ms is relatively high, but it has been selected to
+ /// mitigate the problems with usleep on some virtual machines. On those
+ /// machines the wakeup from usleep may take significant amount of time,
+ /// i.e. usually around 10ms. Thus, the sleep time should be considerably
+ /// higher than this delay.
+ ///
+ /// @param callout_handle Callout handle.
+ /// @return Zero.
+ static int leaseExpireWithDelayCallout(CalloutHandle& callout_handle) {
+ leaseExpireCallout(callout_handle);
+ // Delay the return from the callout by 40ms.
+ usleep(40000);
+
+ return (0);
+ }
+
+ /// @brief Returns removal name change request from the D2 client queue.
+ ///
+ /// @param lease Pointer to the lease to be matched with NCR.
+ ///
+ /// @return null pointer if no match found.
+ static NameChangeRequestPtr getNCRForLease(const LeasePtrType& lease) {
+ // Iterate over the generated name change requests and try
+ // to find the match with our lease (using IP address). If
+ D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr();
+ for (size_t i = 0; i < mgr.getQueueSize(); ++i) {
+ const NameChangeRequestPtr& ncr = mgr.peekAt(i);
+ // If match found, return true.
+ if ((ncr->getIpAddress() == lease->addr_.toText()) &&
+ (ncr->getChangeType() == CHG_REMOVE)) {
+ return (ncr);
+ }
+ }
+ return (NameChangeRequestPtr());
+ }
+
+ /// @brief Returns index of the lease from the address.
+ ///
+ /// This method assumes that leases are ordered from the smallest to
+ /// the highest address, e.g. 10.0.0.0, 10.0.0.1, 10.0.0.2 etc. The
+ /// last two bytes can be used to extract index.
+ ///
+ /// @param address Address.
+ ///
+ /// @return index
+ static uint16_t getLeaseIndexFromAddress(const IOAddress& address) {
+ std::vector<uint8_t> bytes = address.toBytes();
+ std::vector<uint8_t>::reverse_iterator bytes_it = bytes.rbegin();
+ uint16_t index = static_cast<uint16_t>(*bytes_it) |
+ (static_cast<uint16_t>(*(bytes_it + 1)) << 8);
+ return (index);
+ }
+
+ /// @brief Generates hostname for lease index.
+ ///
+ /// Generates hostname in the form of "hostXXXX.example.org", where
+ /// XXXX is a lease index.
+ ///
+ /// @param index Lease index.
+ ///
+ /// @return Generated hostname.
+ static std::string generateHostnameForLeaseIndex(const uint16_t index) {
+ std::ostringstream hostname_s;
+ hostname_s << "host" << std::setw(4) << std::setfill('0')
+ << index << ".example.org";
+ return (hostname_s.str());
+ }
+
+ /// @brief Test that leases can be reclaimed without being removed.
+ void testReclaimExpiredLeasesUpdateState() {
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ // Mark leases with even indexes as expired.
+ if (evenLeaseIndex(i)) {
+ // The higher the index, the more expired the lease.
+ expire(i, 10 + i);
+ }
+ }
+
+ // Run leases reclamation routine on all leases. This should result
+ // in setting "expired-reclaimed" state for all leases with even
+ // indexes.
+ ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, false));
+
+ // Leases with even indexes should be marked as reclaimed.
+ EXPECT_TRUE(testLeases(&leaseReclaimed, &evenLeaseIndex));
+ // Leases with odd indexes shouldn't be marked as reclaimed.
+ EXPECT_TRUE(testLeases(&leaseNotReclaimed, &oddLeaseIndex));
+ }
+
+ /// @brief Test that the leases may be reclaimed by being deleted.
+ void testReclaimExpiredLeasesDelete() {
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ // Mark leases with even indexes as expired.
+ if (evenLeaseIndex(i)) {
+ // The higher the index, the more expired the lease.
+ expire(i, 10 + i);
+ }
+ }
+
+ // Run leases reclamation routine on all leases. This should result
+ // in removal of all leases with even indexes.
+ ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, true));
+
+ // Leases with odd indexes should be retained and their state
+ // shouldn't be "expired-reclaimed".
+ EXPECT_TRUE(testLeases(&leaseNotReclaimed, &oddLeaseIndex));
+ // Leases with even indexes should have been removed.
+ EXPECT_TRUE(testLeases(&leaseDoesntExist, &evenLeaseIndex));
+ }
+
+ /// @brief Test that it is possible to specify the limit for the number
+ /// of reclaimed leases.
+ void testReclaimExpiredLeasesLimit() {
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ // Mark all leases as expired. The higher the index the less
+ // expired the lease.
+ expire(i, 1000 - i);
+ }
+
+ // We will be performing lease reclamation on lease groups of 10.
+ // Hence, it is convenient if the number of test leases is a
+ // multiple of 10.
+ const size_t reclamation_group_size = 10;
+ BOOST_STATIC_ASSERT(TEST_LEASES_NUM % reclamation_group_size == 0);
+
+ // Leases will be reclaimed in groups of 10.
+ for (unsigned int i = reclamation_group_size; i < TEST_LEASES_NUM;
+ i += reclamation_group_size) {
+
+ // Reclaim 10 most expired leases out of TEST_LEASES_NUM. Since
+ // leases are ordered from the most expired to the least expired
+ // this should reclaim leases between 0 and 9, then 10 and 19 etc.
+ ASSERT_NO_THROW(reclaimExpiredLeases(reclamation_group_size,
+ 0, false));
+
+ // Check that leases having all indexes between 0 and 9, 19, 29 etc.
+ // have been reclaimed.
+ EXPECT_TRUE(testLeases(&leaseReclaimed, &allLeaseIndexes,
+ LowerBound(0), UpperBound(i)))
+ << "check failed for i = " << i;
+
+ // Check that all remaining leases haven't been reclaimed.
+ EXPECT_TRUE(testLeases(&leaseNotReclaimed, &allLeaseIndexes,
+ LowerBound(i), UpperBound(TEST_LEASES_NUM)))
+ << "check failed for i = " << i;
+ }
+ }
+
+ /// @brief Test that DNS updates are generated for the leases for which
+ /// the DNS records exist.
+ void testReclaimExpiredLeasesWithDDNS() {
+ // DNS must be started for the D2 client to accept NCRs.
+ ASSERT_NO_THROW(enableDDNS());
+
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ // Expire all leases with even indexes.
+ if (evenLeaseIndex(i)) {
+ // The higher the index, the more expired the lease.
+ expire(i, 10 + i);
+ }
+ }
+
+ // Reclaim all expired leases.
+ ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, false));
+
+ // Leases with odd indexes shouldn't be reclaimed.
+ EXPECT_TRUE(testLeases(&leaseNotReclaimed, &oddLeaseIndex));
+ // Leases with even indexes should be reclaimed.
+ EXPECT_TRUE(testLeases(&leaseReclaimed, &evenLeaseIndex));
+ // DNS updates (removal NCRs) should be generated for leases with even
+ // indexes.
+ EXPECT_TRUE(testLeases(&dnsUpdateGeneratedForLease, &evenLeaseIndex));
+ // DNS updates (removal NCRs) shouldn't be generated for leases with
+ // odd indexes.
+ EXPECT_TRUE(testLeases(&dnsUpdateNotGeneratedForLease, &oddLeaseIndex));
+ }
+
+ /// @brief Test that DNS updates are only generated for the reclaimed
+ /// leases (not for all leases with hostname stored).
+ void testReclaimExpiredLeasesWithDDNSAndLimit() {
+ // DNS must be started for the D2 client to accept NCRs.
+ ASSERT_NO_THROW(enableDDNS());
+
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ // Expire only leases with even indexes.
+ if (evenLeaseIndex(i)) {
+ // The higher the index, the more expired the lease.
+ expire(i, 10 + i);
+ }
+ }
+
+ const size_t reclamation_group_size = 10;
+ BOOST_STATIC_ASSERT(TEST_LEASES_NUM % reclamation_group_size == 0);
+
+ // Leases will be reclaimed in groups of 10
+ for (unsigned int i = 10; i < TEST_LEASES_NUM; i += reclamation_group_size) {
+ // Reclaim 10 most expired leases. Note that the leases with the
+ // higher index are more expired. For example, if the
+ // TEST_LEASES_NUM is equal to 100, the most expired lease will
+ // be 98, then 96, 94 etc.
+ ASSERT_NO_THROW(reclaimExpiredLeases(reclamation_group_size, 0,
+ false));
+
+ // After the first iteration the lower bound is 80, because there
+ // will be 10 the most expired leases in this group: 80, 82, 84,
+ // 86, 88, 90, 92, 94, 96, 98. For subsequent iterations
+ // accordingly.
+ int reclaimed_lower_bound = TEST_LEASES_NUM - 2 * i;
+ // At some point the lower bound will hit the negative value, which
+ // must be corrected to 0.
+ if (reclaimed_lower_bound < 0) {
+ reclaimed_lower_bound = 0;
+ }
+
+ // Leases between the lower bound calculated above and the upper
+ // bound of all leases, and having even indexes should have been
+ // reclaimed.
+ EXPECT_TRUE(testLeases(&leaseReclaimed, &evenLeaseIndex,
+ LowerBound(reclaimed_lower_bound),
+ UpperBound(TEST_LEASES_NUM)))
+ << "check failed for i = " << i;
+
+ // For the same leases we should have generated DNS updates
+ // (removal NCRs).
+ EXPECT_TRUE(testLeases(&dnsUpdateGeneratedForLease, &evenLeaseIndex,
+ LowerBound(reclaimed_lower_bound),
+ UpperBound(TEST_LEASES_NUM)))
+ << "check failed for i = " << i;
+
+ // Leases with odd indexes (falling between the reclaimed ones)
+ // shouldn't have been reclaimed, because they are not expired.
+ EXPECT_TRUE(testLeases(&leaseNotReclaimed, &oddLeaseIndex,
+ LowerBound(reclaimed_lower_bound),
+ UpperBound(TEST_LEASES_NUM)))
+ << "check failed for i = " << i;
+
+ EXPECT_TRUE(testLeases(&dnsUpdateNotGeneratedForLease,
+ &oddLeaseIndex,
+ LowerBound(reclaimed_lower_bound),
+ UpperBound(TEST_LEASES_NUM)))
+ << "check failed for i = " << i;
+
+
+ // At early stages of iterations, there should be continuous
+ // group of leases (expired and not expired) which haven't been
+ // reclaimed.
+ if (reclaimed_lower_bound > 0) {
+ EXPECT_TRUE(testLeases(&leaseNotReclaimed, &allLeaseIndexes,
+ LowerBound(0),
+ UpperBound(reclaimed_lower_bound)))
+ << "check failed for i = " << i;
+
+ EXPECT_TRUE(testLeases(&dnsUpdateNotGeneratedForLease,
+ &oddLeaseIndex,
+ LowerBound(0),
+ UpperBound(reclaimed_lower_bound)));
+ }
+ }
+ }
+
+ /// @brief This test verifies that reclamation routine continues if the
+ /// DNS update has failed for some leases.
+ void testReclaimExpiredLeasesInvalidHostname() {
+ // DNS must be started for the D2 client to accept NCRs.
+ ASSERT_NO_THROW(enableDDNS());
+
+ for (size_t i = 0; i < TEST_LEASES_NUM; ++i) {
+ // Generate invalid hostname for every other lease.
+ if (evenLeaseIndex(i)) {
+ // Hostname with two consecutive dots is invalid and may result
+ // in exception if the reclamation routine doesn't protect
+ // against such exceptions.
+ std::ostringstream hostname_s;
+ hostname_s << "invalid-host" << i << "..example.com";
+ leases_[i]->hostname_ = hostname_s.str();
+ ASSERT_NO_THROW(updateLease(i));
+ }
+ // Every lease is expired.
+ expire(i, 10 + i);
+ }
+
+ // Although we know that some hostnames are broken we don't want the
+ // reclamation process to break when it finds a broken record.
+ // It should rather continue to process other leases.
+ ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, false));
+
+ // All leases should have been reclaimed. Broken DNS entry doesn't
+ // warrant that we don't reclaim the lease.
+ EXPECT_TRUE(testLeases(&leaseReclaimed, &allLeaseIndexes));
+ // The routine should not generate DNS updates for the leases with
+ // broken hostname.
+ EXPECT_TRUE(testLeases(&dnsUpdateNotGeneratedForLease,
+ &evenLeaseIndex));
+ // But it should generate DNS updates for the leases with the correct
+ // hostname.
+ EXPECT_TRUE(testLeases(&dnsUpdateGeneratedForLease, &oddLeaseIndex));
+ }
+
+ /// @brief This test verifies that callouts are executed for each expired
+ /// lease when installed.
+ void testReclaimExpiredLeasesHooks() {
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ if (evenLeaseIndex(i)) {
+ expire(i, 1000 - i);
+ }
+ }
+
+ HookLibsCollection libraries; // no libraries at this time
+ HooksManager::loadLibraries(libraries);
+
+ // Install a callout: lease4_expire or lease6_expire.
+ std::ostringstream callout_name;
+ callout_name << callout_argument_name << "_expire";
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ callout_name.str(), leaseExpireCallout));
+
+ ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, false));
+
+ // Callouts should be executed for leases with even indexes and these
+ // leases should be reclaimed.
+ EXPECT_TRUE(testLeases(&leaseCalloutExecuted, &evenLeaseIndex));
+ EXPECT_TRUE(testLeases(&leaseReclaimed, &evenLeaseIndex));
+ // Callouts should not be executed for leases with odd indexes and these
+ // leases should not be reclaimed.
+ EXPECT_TRUE(testLeases(&leaseCalloutNotExecuted, &oddLeaseIndex));
+ EXPECT_TRUE(testLeases(&leaseNotReclaimed, &oddLeaseIndex));
+ }
+
+ /// @brief This test verifies that callouts are executed for each expired
+ /// lease and that the lease is not reclaimed when skip flag is set.
+ void testReclaimExpiredLeasesHooksWithSkip() {
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ if (evenLeaseIndex(i)) {
+ expire(i, 1000 - i);
+ }
+ }
+
+ HookLibsCollection libraries; // no libraries at this time
+ HooksManager::loadLibraries(libraries);
+
+ // Install a callout: lease4_expire or lease6_expire.
+ std::ostringstream callout_name;
+ callout_name << callout_argument_name << "_expire";
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ callout_name.str(), leaseExpireWithSkipCallout));
+
+ ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, false));
+
+ // Callouts should have been executed for leases with even indexes.
+ EXPECT_TRUE(testLeases(&leaseCalloutExecuted, &evenLeaseIndex));
+ // Callouts should not be executed for leases with odd indexes.
+ EXPECT_TRUE(testLeases(&leaseCalloutNotExecuted, &oddLeaseIndex));
+ // Leases shouldn't be reclaimed because the callout sets the
+ // skip flag for each of them.
+ EXPECT_TRUE(testLeases(&leaseNotReclaimed, &allLeaseIndexes));
+ }
+
+ /// @brief This test verifies that it is possible to set the timeout for
+ /// the execution of the lease reclamation routine.
+ void testReclaimExpiredLeasesTimeout(const uint16_t timeout) {
+ // Leases are segregated from the most expired to the least expired.
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ expire(i, 2000 - i);
+ }
+
+ HookLibsCollection libraries;
+ HooksManager::loadLibraries(libraries);
+
+ // Install a callout: lease4_expire or lease6_expire. Each callout
+ // takes at least 40ms to run (it uses usleep).
+ std::ostringstream callout_name;
+ callout_name << callout_argument_name << "_expire";
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ callout_name.str(), leaseExpireWithDelayCallout));
+
+ // Reclaim leases with timeout.
+ ASSERT_NO_THROW(reclaimExpiredLeases(0, timeout, false));
+
+ // We reclaimed at most (timeout / 40ms) leases.
+ const uint16_t theoretical_reclaimed = static_cast<uint16_t>(timeout / 40);
+
+ // The actual number of leases reclaimed is likely to be lower than
+ // the theoretical number. For low theoretical number the adjusted
+ // number is always 1. For higher number, it will be 10 less than the
+ // theoretical number.
+ const uint16_t adjusted_reclaimed = (theoretical_reclaimed > 10 ?
+ theoretical_reclaimed - 10 : 1);
+
+ EXPECT_TRUE(testLeases(&leaseCalloutExecuted, &allLeaseIndexes,
+ LowerBound(0), UpperBound(adjusted_reclaimed)));
+ EXPECT_TRUE(testLeases(&leaseReclaimed, &allLeaseIndexes,
+ LowerBound(0), UpperBound(adjusted_reclaimed)));
+ EXPECT_TRUE(testLeases(&leaseCalloutNotExecuted, &allLeaseIndexes,
+ LowerBound(theoretical_reclaimed + 1),
+ UpperBound(TEST_LEASES_NUM)));
+ EXPECT_TRUE(testLeases(&leaseNotReclaimed, &allLeaseIndexes,
+ LowerBound(theoretical_reclaimed + 1),
+ UpperBound(TEST_LEASES_NUM)));
+ }
+
+ /// @brief This test verifies that expired-reclaimed leases are removed
+ /// from the lease database.
+ void testDeleteExpiredReclaimedLeases() {
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ // Mark leases with even indexes as expired.
+ if (evenLeaseIndex(i)) {
+ // The higher the index, the more expired the lease.
+ reclaim(i, 10 + i);
+ }
+ }
+
+ // Run leases reclamation routine on all leases. This should result
+ // in removal of all leases with even indexes.
+ ASSERT_NO_THROW(deleteExpiredReclaimedLeases(10));
+
+ // Leases with odd indexes shouldn't be removed from the database.
+ EXPECT_TRUE(testLeases(&leaseExists, &oddLeaseIndex));
+ // Leases with even indexes should have been removed.
+ EXPECT_TRUE(testLeases(&leaseDoesntExist, &evenLeaseIndex));
+ }
+
+ /// @brief Test that declined expired leases can be removed.
+ ///
+ /// This method allows controlling remove_leases parameter when calling
+ /// @ref AllocEngine::reclaimExpiredLeases4 or
+ /// @ref AllocEngine::reclaimExpiredLeases6. This should not matter, as
+ /// the address affinity doesn't make sense for declined leases (they don't
+ /// have any useful information in them anymore), so AllocEngine should
+ /// remove them all the time.
+ ///
+ /// @param remove see description above
+ void testReclaimDeclined(bool remove) {
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+
+ // Mark leases with even indexes as expired.
+ if (evenLeaseIndex(i)) {
+
+ // Mark lease as declined with 100 seconds of probation-period
+ // (i.e. lease is supposed to be off limits for 100 seconds)
+ decline(i, 100);
+
+ // The higher the index, the more expired the lease.
+ expire(i, 10 + i);
+ }
+ }
+
+ // Run leases reclamation routine on all leases. This should result
+ // in removing all leases with status = declined, i.e. all
+ // even leases should be gone.
+ ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, remove));
+
+ // Leases with even indexes should not exist in the DB
+ EXPECT_TRUE(testLeases(&leaseDoesntExist, &evenLeaseIndex));
+ }
+
+ /// @brief Test that appropriate statistics are updated when
+ /// declined expired leases are processed by AllocEngine.
+ ///
+ /// This method works for both v4 and v6. Just make sure the correct
+ /// statistic name is passed. This is the name of the assigned addresses,
+ /// that is expected to be decreased once the reclamation procedure
+ /// is complete.
+ ///
+ /// @param stat_name name of the statistic for assigned addresses statistic
+ /// ("assigned-addresses" for both v4 and "assigned-nas" for v6)
+ void testReclaimDeclinedStats(const std::string& stat_name) {
+
+ // Leases by default all belong to subnet_id_ = 1. Let's count the
+ // number of declined leases.
+ int subnet1_cnt = 0;
+ int subnet2_cnt = 0;
+
+ // Let's move all leases to declined,expired state.
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+
+ // Move the lease to declined state
+ decline(i, 100);
+
+ // And expire it, so it will be reclaimed
+ expire(i, 10 + 1);
+
+ // Move every other lease to subnet-id = 2.
+ if (evenLeaseIndex(i)) {
+ subnet1_cnt++;
+ } else {
+ subnet2_cnt++;
+ setSubnetId(i, 2);
+ }
+ }
+
+ StatsMgr& stats_mgr = StatsMgr::instance();
+
+ // Let's set the global statistic. Values are arbitrary and can
+ // be used to easily detect whether a given stat was decreased or
+ // increased. They are sufficiently high compared to number of leases
+ // to avoid any chances of going into negative.
+ stats_mgr.setValue("declined-addresses", static_cast<int64_t>(1000));
+
+ // Let's set global the counter for reclaimed declined addresses.
+ stats_mgr.setValue("reclaimed-declined-addresses",
+ static_cast<int64_t>(2000));
+
+ // And those subnet specific as well
+ stats_mgr.setValue(stats_mgr.generateName("subnet", 1,
+ stat_name), int64_t(1000));
+ stats_mgr.setValue(stats_mgr.generateName("subnet", 2,
+ stat_name), int64_t(2000));
+
+ stats_mgr.setValue(stats_mgr.generateName("subnet", 1,
+ "reclaimed-declined-addresses"), int64_t(10000));
+ stats_mgr.setValue(stats_mgr.generateName("subnet", 2,
+ "reclaimed-declined-addresses"), int64_t(20000));
+
+ stats_mgr.setValue(stats_mgr.generateName("subnet", 1,
+ "declined-addresses"), int64_t(100));
+ stats_mgr.setValue(stats_mgr.generateName("subnet", 2,
+ "declined-addresses"), int64_t(200));
+
+ // Run leases reclamation routine on all leases. This should result
+ // in removal of all leases with even indexes.
+ ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, true));
+
+ // Declined-addresses should be decreased from its initial value (1000)
+ // for both recovered addresses from subnet1 and subnet2.
+ testStatistics("declined-addresses", 1000 - subnet1_cnt - subnet2_cnt);
+
+ // The code should bump up global counter for reclaimed declined
+ // addresses.
+ testStatistics("reclaimed-declined-addresses", 2000 + subnet1_cnt + subnet2_cnt);
+
+ // subnet[X].assigned-addresses should go down. Between the time
+ // of DHCPDECLINE(v4)/DECLINE(v6) reception and declined expired lease
+ // reclamation, we count this address as assigned-addresses. We decrease
+ // assigned-addresses(v4)/assigned-nas(v6) when we reclaim the lease,
+ // not when the packet is received. For explanation, see Duplicate
+ // Addresses (DHCPDECLINE support) (v4) or Duplicate Addresses (DECLINE
+ // support) sections in the User's Guide or a comment in
+ // Dhcpv4Srv::declineLease or Dhcpv6Srv::declineLease.
+ testStatistics("subnet[1]." + stat_name, 1000 - subnet1_cnt);
+ testStatistics("subnet[2]." + stat_name, 2000 - subnet2_cnt);
+
+ testStatistics("subnet[1].declined-addresses", 100 - subnet1_cnt);
+ testStatistics("subnet[2].declined-addresses", 200 - subnet2_cnt);
+
+ // subnet[X].reclaimed-declined-addresses should go up in each subnet
+ testStatistics("subnet[1].reclaimed-declined-addresses", 10000 + subnet1_cnt);
+ testStatistics("subnet[2].reclaimed-declined-addresses", 20000 + subnet1_cnt);
+ }
+
+ /// @brief Collection of leases created at construction time.
+ std::vector<LeasePtrType> leases_;
+
+ /// @brief Allocation engine instance used for tests.
+ AllocEnginePtr engine_;
+};
+
+
+
+/// @brief Specialization of the @c ExpirationAllocEngineTest class to test
+/// reclamation of the IPv6 leases.
+class ExpirationAllocEngine6Test : public ExpirationAllocEngineTest<Lease6Ptr> {
+public:
+
+ /// @brief Class constructor.
+ ///
+ /// This constructor initializes @c TEST_LEASES_NUM leases and
+ /// stores them in the lease manager.
+ ExpirationAllocEngine6Test();
+
+ /// @brief Virtual destructor.
+ ///
+ /// Clears up static fields that may be modified by hooks.
+ virtual ~ExpirationAllocEngine6Test() {
+ callout_lease_.reset();
+ callout_name_ = string("");
+ }
+
+ /// @brief Creates collection of leases for a test.
+ ///
+ /// It is called internally at the construction time.
+ void createLeases();
+
+ /// @brief Updates lease in the lease database.
+ ///
+ /// @param lease_index Index of the lease.
+ virtual void updateLease(const unsigned int lease_index) {
+ LeaseMgrFactory::instance().updateLease6(leases_[lease_index]);
+ }
+
+ /// @brief Changes the owner of a lease.
+ ///
+ /// This method changes the owner of the lease by modifying the DUID.
+ ///
+ /// @param lease_index Lease index. Must be between 0 and
+ /// @c TEST_LEASES_NUM.
+ virtual void transferOwnership(const uint16_t lease_index);
+
+ /// @brief Sets subnet id for a lease.
+ ///
+ /// It also updates statistics of assigned leases in the stats manager.
+ ///
+ /// @param lease_index Lease index.
+ /// @param id New subnet id.
+ virtual void setSubnetId(const uint16_t lease_index, const SubnetID& id);
+
+ /// @brief Sets type of a lease.
+ ///
+ /// It also updates statistics of assigned leases in the stats manager.
+ ///
+ /// @param lease_index Lease index.
+ /// @param lease_type Lease type.
+ void setLeaseType(const uint16_t lease_index, const Lease6::Type& lease_type);
+
+ /// @brief Retrieves lease from the database.
+ ///
+ /// @param lease_index Index of the lease.
+ virtual Lease6Ptr getLease(const unsigned int lease_index) const {
+ return (LeaseMgrFactory::instance().getLease6(leases_[lease_index]->type_,
+ leases_[lease_index]->addr_));
+ }
+
+ /// @brief Wrapper method running lease reclamation routine.
+ ///
+ /// @param max_leases Maximum number of leases to be reclaimed.
+ /// @param timeout Maximum amount of time that the reclamation routine
+ /// may be processing expired leases, expressed in seconds.
+ /// @param remove_lease A boolean value indicating if the lease should
+ /// be removed when it is reclaimed (if true) or it should be left in the
+ /// database in the "expired-reclaimed" state (if false).
+ virtual void reclaimExpiredLeases(const size_t max_leases,
+ const uint16_t timeout,
+ const bool remove_lease) {
+ engine_->reclaimExpiredLeases6(max_leases, timeout, remove_lease);
+ }
+
+ /// @brief Wrapper method for removing expired-reclaimed leases.
+ ///
+ /// @param secs The minimum amount of time, expressed in seconds,
+ /// for the lease to be left in the "expired-reclaimed" state
+ /// before it can be removed.
+ virtual void deleteExpiredReclaimedLeases(const uint32_t secs) {
+ engine_->deleteExpiredReclaimedLeases6(secs);
+ }
+
+ /// @brief Test that statistics is updated when leases are reclaimed.
+ void testReclaimExpiredLeasesStats();
+
+ /// @brief Test that expired leases are reclaimed before they are allocated.
+ ///
+ /// @param msg_type DHCPv6 message type.
+ /// @param use_reclaimed Boolean parameter indicating if the leases
+ /// stored in the lease database should be marked as 'expired-reclaimed'
+ /// or 'expired'. This allows to test whether the allocation engine can
+ /// determine that the lease has been reclaimed already and not reclaim
+ /// it the second time.
+ void testReclaimReusedLeases(const uint16_t msg_type, const bool use_reclaimed);
+
+ /// @brief Callout for lease6_recover
+ ///
+ /// This callout stores passed parameter into static fields.
+ ///
+ /// @param callout_handle will be provided by hooks framework
+ /// @return always 0
+ static int lease6RecoverCallout(CalloutHandle& callout_handle) {
+ callout_name_ = "lease6_recover";
+
+ callout_handle.getArgument("lease6", callout_lease_);
+
+ return (0);
+ }
+
+ /// @brief Callout for lease6_recover that sets status to SKIP
+ ///
+ /// This callout stores passed parameter into static fields.
+ ///
+ /// @param callout_handle will be provided by hooks framework
+ /// @return always 0
+ static int lease6RecoverSkipCallout(CalloutHandle& callout_handle) {
+ // Set the next step status to SKIP
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ return (lease6RecoverCallout(callout_handle));
+ }
+
+ /// @brief Test install a hook callout, recovers declined leases
+ ///
+ /// This test: declines, then expires half of the leases, then
+ /// installs a callout on lease6_recover hook, then reclaims
+ /// expired leases and checks that:
+ /// - the callout was indeed called
+ /// - the parameter (lease6) was indeed passed as expected
+ /// - checks that the leases are removed (skip=false) or
+ /// - checks that the leases are still there (skip=true)
+ /// @param skip should the callout set the next step status to skip?
+ void
+ testReclaimDeclinedHook(bool skip);
+
+ /// The following parameters will be written by a callout
+ static std::string callout_name_; ///< Stores callout name
+ static Lease6Ptr callout_lease_; ///< Stores callout parameter
+};
+
+std::string ExpirationAllocEngine6Test::callout_name_;
+Lease6Ptr ExpirationAllocEngine6Test::callout_lease_;
+
+ExpirationAllocEngine6Test::ExpirationAllocEngine6Test()
+ : ExpirationAllocEngineTest<Lease6Ptr>("type=memfile universe=6 persist=false") {
+ createLeases();
+ callout_argument_name = "lease6";
+
+ // Let's clear any garbage previous test may have left in static fields.
+ callout_name_ = string("");
+ callout_lease_.reset();
+}
+
+void
+ExpirationAllocEngine6Test::createLeases() {
+ // Create TEST_LEASES_NUM leases.
+ for (uint16_t i = 0; i < TEST_LEASES_NUM; ++i) {
+ // DUID
+ std::ostringstream duid_s;
+ duid_s << "01020304050607" << std::setw(4) << std::setfill('0') << i;
+ DuidPtr duid(new DUID(DUID::fromText(duid_s.str()).getDuid()));
+
+ // Address.
+ std::ostringstream address_s;
+ address_s << "2001:db8:1::" << std::setw(4) << std::setfill('0') << i;
+ IOAddress address(address_s.str());
+
+ // Create lease.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, address, duid, 1, 50, 60,
+ SubnetID(1), true, true,
+ generateHostnameForLeaseIndex(i)));
+ leases_.push_back(lease);
+ // Copy the lease before adding it to the lease manager. We want to
+ // make sure that modifications to the leases held in the leases_
+ // container doesn't affect the leases in the lease manager.
+ Lease6Ptr tmp(new Lease6(*lease));
+ LeaseMgrFactory::instance().addLease(tmp);
+
+ // Note in the statistics that this lease has been added.
+ StatsMgr& stats_mgr = StatsMgr::instance();
+ std::string stat_name =
+ lease->type_ == Lease::TYPE_NA ? "assigned-nas" : "assigned-pds";
+ stats_mgr.addValue(stats_mgr.generateName("subnet", lease->subnet_id_, stat_name),
+ int64_t(1));
+ }
+}
+
+void
+ExpirationAllocEngine6Test::transferOwnership(const uint16_t lease_index) {
+ ASSERT_GT(leases_.size(), lease_index);
+ std::vector<uint8_t> bytes = leases_[lease_index]->duid_->getDuid();
+ if (bytes.size() > 1) {
+ if (++bytes[0] == 0) {
+ ++bytes[1];
+ }
+ }
+
+ leases_[lease_index]->duid_.reset(new DUID(bytes));
+}
+
+void
+ExpirationAllocEngine6Test::setSubnetId(const uint16_t lease_index, const SubnetID& id) {
+ ASSERT_GT(leases_.size(), lease_index);
+ if (leases_[lease_index]->subnet_id_ != id) {
+ StatsMgr& stats_mgr = StatsMgr::instance();
+ std::string stats_name = (leases_[lease_index]->type_ == Lease::TYPE_NA ?
+ "assigned-nas" : "assigned-pds");
+ stats_mgr.addValue(stats_mgr.generateName("subnet", id, stats_name),
+ int64_t(1));
+ stats_mgr.addValue(stats_mgr.generateName("subnet",
+ leases_[lease_index]->subnet_id_,
+ stats_name),
+ int64_t(-1));
+ leases_[lease_index]->subnet_id_ = id;
+ ASSERT_NO_THROW(updateLease(lease_index));
+ }
+}
+
+void
+ExpirationAllocEngine6Test::setLeaseType(const uint16_t lease_index,
+ const Lease6::Type& lease_type) {
+ ASSERT_GT(leases_.size(), lease_index);
+ if (leases_[lease_index]->type_ != lease_type) {
+ StatsMgr& stats_mgr = StatsMgr::instance();
+ std::string stats_name = (lease_type == Lease::TYPE_NA ?
+ "assigned-nas" : "assigned-pds");
+ stats_mgr.addValue(stats_mgr.generateName("subnet",
+ leases_[lease_index]->subnet_id_,
+ stats_name),
+ int64_t(1));
+ stats_name = (leases_[lease_index]->type_ == Lease::TYPE_NA ?
+ "assigned-nas" : "assigned-pds");
+ stats_mgr.addValue(stats_mgr.generateName("subnet",
+ leases_[lease_index]->subnet_id_,
+ stats_name),
+ int64_t(-1));
+ leases_[lease_index]->type_ = lease_type;
+ ASSERT_NO_THROW(updateLease(lease_index));
+ }
+}
+
+
+void
+ExpirationAllocEngine6Test::testReclaimExpiredLeasesStats() {
+ // This test requires that the number of leases is an even number.
+ BOOST_STATIC_ASSERT(TEST_LEASES_NUM % 2 == 0);
+
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ // Mark all leases as expired. The higher the index the less
+ // expired the lease.
+ expire(i, 1000 - i);
+ // Modify subnet ids and lease types for some leases.
+ if (evenLeaseIndex(i)) {
+ setSubnetId(i, SubnetID(2));
+ setLeaseType(i, Lease::TYPE_PD);
+ }
+ }
+
+ // Leases will be reclaimed in groups of 8.
+ const size_t reclamation_group_size = 8;
+ for (unsigned int i = reclamation_group_size; i < TEST_LEASES_NUM;
+ i += reclamation_group_size) {
+
+ // Reclaim 8 most expired leases out of TEST_LEASES_NUM.
+ ASSERT_NO_THROW(reclaimExpiredLeases(reclamation_group_size,
+ 0, false));
+
+ // Number of reclaimed leases should increase as we loop.
+ EXPECT_TRUE(testStatistics("reclaimed-leases", i));
+ // Make sure that the number of reclaimed leases is also distributed
+ // across two subnets.
+ EXPECT_TRUE(testStatistics("subnet[1].reclaimed-leases", i / 2));
+ EXPECT_TRUE(testStatistics("subnet[2].reclaimed-leases", i / 2));
+ // Number of assigned leases should decrease as we reclaim them.
+ EXPECT_TRUE(testStatistics("subnet[1].assigned-nas",
+ (TEST_LEASES_NUM - i) / 2));
+ EXPECT_TRUE(testStatistics("subnet[2].assigned-pds",
+ (TEST_LEASES_NUM - i) / 2));
+ }
+}
+
+void
+ExpirationAllocEngine6Test::testReclaimReusedLeases(const uint16_t msg_type,
+ const bool use_reclaimed) {
+ BOOST_STATIC_ASSERT(TEST_LEASES_NUM < 1000);
+
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ // Depending on the parameter, mark leases 'expired-reclaimed' or
+ // simply 'expired'.
+ if (use_reclaimed) {
+ reclaim(i, 1000 - i);
+
+ } else {
+ // Mark all leases as expired.
+ expire(i, 1000 - i);
+ }
+
+ // For the Renew case, we don't change the ownership of leases. We
+ // will let the lease owners renew them. For other cases, we modify
+ // the DUIDs to simulate reuse of expired leases.
+ if (msg_type != DHCPV6_RENEW) {
+ transferOwnership(i);
+ }
+ }
+
+ // Create subnet and the pool. This is required by the allocation process.
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 64, 10, 20, 50, 60,
+ SubnetID(1)));
+ ASSERT_NO_THROW(subnet->addPool(Pool6Ptr(new Pool6(Lease::TYPE_NA,
+ IOAddress("2001:db8:1::"),
+ IOAddress("2001:db8:1::FFFF")))));
+
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ // Build the context.
+ AllocEngine::ClientContext6 ctx(subnet, leases_[i]->duid_,
+ false, false,
+ leases_[i]->hostname_,
+ msg_type == DHCPV6_SOLICIT,
+ Pkt6Ptr(new Pkt6(msg_type, 0x1234)));
+ ctx.currentIA().iaid_ = 1;
+ ctx.currentIA().addHint(leases_[i]->addr_);
+
+ // Depending on the message type, we will call a different function.
+ if (msg_type == DHCPV6_RENEW) {
+ ASSERT_NO_THROW(engine_->renewLeases6(ctx));
+
+ } else {
+ ASSERT_NO_THROW(engine_->allocateLeases6(ctx));
+ }
+ }
+
+ // The Solicit should not trigger leases reclamation. The Renew and
+ // Request must trigger leases reclamation unless the lease is
+ // initially reclaimed.
+ if (use_reclaimed || (msg_type == DHCPV6_SOLICIT)) {
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 0));
+ } else {
+ EXPECT_TRUE(testStatistics("reclaimed-leases", TEST_LEASES_NUM));
+ EXPECT_TRUE(testStatistics("assigned-nas", TEST_LEASES_NUM, subnet->getID()));
+ // Leases should have been updated in the lease database and their
+ // state should not be 'expired-reclaimed' anymore.
+ EXPECT_TRUE(testLeases(&leaseNotReclaimed, &allLeaseIndexes));
+ }
+
+}
+
+void
+ExpirationAllocEngine6Test::testReclaimDeclinedHook(bool skip) {
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+
+ // Mark leases with even indexes as expired.
+ if (evenLeaseIndex(i)) {
+
+ // Mark lease as declined with 100 seconds of probation-period
+ // (i.e. lease is supposed to be off limits for 100 seconds)
+ decline(i, 100);
+
+ // The higher the index, the more expired the lease.
+ expire(i, 10 + i);
+ }
+ }
+
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease6_recover",
+ skip ? lease6RecoverSkipCallout : lease6RecoverCallout));
+
+ // Run leases reclamation routine on all leases.
+ ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, true));
+
+ // Make sure that the callout really was called. It was supposed to modify
+ // the callout_name_ and store the lease in callout_lease_
+ EXPECT_EQ("lease6_recover", callout_name_);
+ EXPECT_TRUE(callout_lease_);
+
+ // Leases with even indexes should not exist in the DB
+ if (skip) {
+ // Skip status should have prevented removing the lease.
+ EXPECT_TRUE(testLeases(&leaseExists, &evenLeaseIndex));
+ } else {
+ // The hook hasn't modified next step status. The lease should be gone.
+ EXPECT_TRUE(testLeases(&leaseDoesntExist, &evenLeaseIndex));
+ }
+};
+
+// This test verifies that the leases can be reclaimed without being removed
+// from the database. In such case, the leases' state is set to
+// "expired-reclaimed".
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeases6UpdateState) {
+ testReclaimExpiredLeasesUpdateState();
+}
+
+// This test verifies that the reclaimed leases are deleted when requested.
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesDelete) {
+ testReclaimExpiredLeasesDelete();
+}
+
+// This test verifies that it is possible to specify the limit for the
+// number of reclaimed leases.
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesLimit) {
+ testReclaimExpiredLeasesLimit();
+}
+
+// This test verifies that DNS updates are generated for the leases
+// for which the DNS records exist.
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesWithDDNS) {
+ testReclaimExpiredLeasesWithDDNS();
+}
+
+// This test verifies that it is DNS updates are generated only for the
+// reclaimed expired leases. In this case we limit the number of leases
+// reclaimed during a single call to reclamation routine.
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesWithDDNSAndLimit) {
+ testReclaimExpiredLeasesWithDDNSAndLimit();
+}
+
+// This test verifies that if some leases have invalid hostnames, the
+// lease reclamation routine continues with reclamation of leases anyway.
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesInvalidHostname) {
+ testReclaimExpiredLeasesInvalidHostname();
+}
+
+// This test verifies that statistics is correctly updated when the leases
+// are reclaimed.
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesStats) {
+ testReclaimExpiredLeasesStats();
+}
+
+// This test verifies that callouts are executed for each expired lease.
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesHooks) {
+ testReclaimExpiredLeasesHooks();
+}
+
+// This test verifies that callouts are executed for each expired lease
+// and that the lease is not reclaimed when the skip flag is set.
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesHooksWithSkip) {
+ testReclaimExpiredLeasesHooksWithSkip();
+}
+
+// This test verifies that it is possible to set the timeout for the
+// execution of the lease reclamation routine.
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesTimeout) {
+ // This test needs at least 40 leases to make sense.
+ BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 40);
+ // Run with timeout of 1.2s.
+ testReclaimExpiredLeasesTimeout(1200);
+}
+
+// This test verifies that at least one lease is reclaimed if the timeout
+// for the lease reclamation routine is shorter than the time needed for
+// the reclamation of a single lease. This prevents the situation when
+// very short timeout (perhaps misconfigured) effectively precludes leases
+// reclamation.
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesShortTimeout) {
+ // We will most likely reclaim just one lease, so 5 is more than enough.
+ BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 5);
+ // Reclaim leases with the 1ms timeout.
+ testReclaimExpiredLeasesTimeout(1);
+}
+
+// This test verifies that expired-reclaimed leases are removed from the
+// lease database.
+TEST_F(ExpirationAllocEngine6Test, deleteExpiredReclaimedLeases) {
+ BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 10);
+ testDeleteExpiredReclaimedLeases();
+}
+
+/// This test verifies that @ref AllocEngine::reclaimExpiredLeases6 properly
+/// handles declined leases that have expired in case when it is told to
+/// remove leases.}
+TEST_F(ExpirationAllocEngine6Test, reclaimDeclined1) {
+ testReclaimDeclined(true);
+}
+
+/// This test verifies that @ref AllocEngine::reclaimExpiredLeases6 properly
+/// handles declined leases that have expired in case when it is told to
+/// not remove leases. This flag should not matter and declined expired
+/// leases should always be removed.
+TEST_F(ExpirationAllocEngine6Test, reclaimDeclined2) {
+ testReclaimDeclined(false);
+}
+
+/// This test verifies that statistics are modified correctly after
+/// reclaim expired leases is called.
+TEST_F(ExpirationAllocEngine6Test, reclaimDeclinedStats) {
+ testReclaimDeclinedStats("assigned-nas");
+}
+
+// This test verifies that expired leases are reclaimed before they are
+// allocated to another client sending a Request message.
+TEST_F(ExpirationAllocEngine6Test, reclaimReusedLeases) {
+ testReclaimReusedLeases(DHCPV6_REQUEST, false);
+}
+
+// This test verifies that allocation engine detects that the expired
+// lease has been reclaimed already when it reuses this lease.
+TEST_F(ExpirationAllocEngine6Test, reclaimReusedLeasesAlreadyReclaimed) {
+ testReclaimReusedLeases(DHCPV6_REQUEST, true);
+}
+
+// This test verifies that expired leases are reclaimed before they
+// are renewed.
+TEST_F(ExpirationAllocEngine6Test, reclaimRenewedLeases) {
+ testReclaimReusedLeases(DHCPV6_RENEW, false);
+}
+
+// This test verifies that allocation engine detects that the expired
+// lease has been reclaimed already when it renews the lease.
+TEST_F(ExpirationAllocEngine6Test, reclaimRenewedLeasesAlreadyReclaimed) {
+ testReclaimReusedLeases(DHCPV6_RENEW, true);
+}
+
+// This test verifies that the expired leases are not reclaimed when the
+// Solicit message is being processed.
+TEST_F(ExpirationAllocEngine6Test, reclaimReusedLeasesSolicit) {
+ testReclaimReusedLeases(DHCPV6_SOLICIT, false);
+}
+
+// This test verifies that the 'expired-reclaimed' leases are not reclaimed
+// again when the Solicit message is being processed.
+TEST_F(ExpirationAllocEngine6Test, reclaimReusedLeasesSolicitAlreadyReclaimed) {
+ testReclaimReusedLeases(DHCPV6_SOLICIT, true);
+}
+
+// This test verifies if the hooks installed on lease6_recover are called
+// when the lease expires.
+TEST_F(ExpirationAllocEngine6Test, reclaimDeclinedHook1) {
+ testReclaimDeclinedHook(false); // false = don't use skip callout
+}
+
+// This test verifies if the hooks installed on lease6_recover are called
+// when the lease expires and that the next step status set to SKIP
+// causes the recovery to not be conducted.
+TEST_F(ExpirationAllocEngine6Test, reclaimDeclinedHook2) {
+ testReclaimDeclinedHook(true); // true = use skip callout
+}
+
+// *******************************************************
+//
+// DHCPv4 lease reclamation routine tests start here!
+//
+// *******************************************************
+
+/// @brief Specialization of the @c ExpirationAllocEngineTest class to test
+/// reclamation of the IPv4 leases.
+class ExpirationAllocEngine4Test : public ExpirationAllocEngineTest<Lease4Ptr> {
+public:
+
+ /// @brief Class constructor.
+ ///
+ /// This constructor initializes @c TEST_LEASES_NUM leases and
+ /// stores them in the lease manager.
+ ExpirationAllocEngine4Test();
+
+ /// @brief Virtual destructor.
+ ///
+ /// Clears up static fields that may be modified by hooks.
+ virtual ~ExpirationAllocEngine4Test() {
+ callout_lease_.reset();
+ callout_name_ = string("");
+ }
+
+ /// @brief Creates collection of leases for a test.
+ ///
+ /// It is called internally at the construction time.
+ void createLeases();
+
+ /// @brief Generates unique client identifier from lease index.
+ ///
+ /// @param index lease index.
+ void setUniqueClientId(const uint16_t index);
+
+ /// @brief Updates lease in the lease database.
+ ///
+ /// @param lease_index Index of the lease.
+ virtual void updateLease(const unsigned int lease_index) {
+ LeaseMgrFactory::instance().updateLease4(leases_[lease_index]);
+ }
+
+ /// @brief Changes the owner of a lease.
+ ///
+ /// This method changes the owner of the lease by updating the client
+ /// identifier (if present) or HW address.
+ ///
+ /// @param lease_index Lease index. Must be between 0 and
+ /// @c TEST_LEASES_NUM.
+ virtual void transferOwnership(const uint16_t lease_index);
+
+ /// @brief Retrieves lease from the database.
+ ///
+ /// @param lease_index Index of the lease.
+ virtual Lease4Ptr getLease(const unsigned int lease_index) const {
+ return (LeaseMgrFactory::instance().getLease4(leases_[lease_index]->addr_));
+ }
+
+ /// @brief Sets subnet id for a lease.
+ ///
+ /// It also updates statistics of assigned leases in the stats manager.
+ ///
+ /// @param lease_index Lease index.
+ /// @param id New subnet id.
+ virtual void setSubnetId(const uint16_t lease_index, const SubnetID& id);
+
+ /// @brief Wrapper method running lease reclamation routine.
+ ///
+ /// @param max_leases Maximum number of leases to be reclaimed.
+ /// @param timeout Maximum amount of time that the reclamation routine
+ /// may be processing expired leases, expressed in seconds.
+ /// @param remove_lease A boolean value indicating if the lease should
+ /// be removed when it is reclaimed (if true) or it should be left in the
+ /// database in the "expired-reclaimed" state (if false).
+ virtual void reclaimExpiredLeases(const size_t max_leases,
+ const uint16_t timeout,
+ const bool remove_lease) {
+ engine_->reclaimExpiredLeases4(max_leases, timeout, remove_lease);
+ }
+
+ /// @brief Wrapper method for removing expired-reclaimed leases.
+ ///
+ /// @param secs The minimum amount of time, expressed in seconds,
+ /// for the lease to be left in the "expired-reclaimed" state
+ /// before it can be removed.
+ virtual void deleteExpiredReclaimedLeases(const uint32_t secs) {
+ engine_->deleteExpiredReclaimedLeases4(secs);
+ }
+
+ /// @brief Lease algorithm checking if NCR has been generated from client
+ /// identifier.
+ ///
+ /// @param lease Pointer to the lease for which the NCR needs to be checked.
+ static bool dnsUpdateGeneratedFromClientId(const Lease4Ptr& lease);
+
+ /// @brief Lease algorithm checking if NCR has been generated from
+ /// HW address.
+ static bool dnsUpdateGeneratedFromHWAddress(const Lease4Ptr& lease);
+
+ /// @brief Test that DNS updates are properly generated when the
+ /// reclaimed leases contain client identifier.
+ void testReclaimExpiredLeasesWithDDNSAndClientId();
+
+ /// @brief Test that statistics is updated when leases are reclaimed..
+ void testReclaimExpiredLeasesStats();
+
+ /// @brief Test that the lease is reclaimed before it is renewed or
+ /// reused.
+ ///
+ /// @param msg_type DHCPv4 message type, i.e. DHCPDISCOVER or DHCPREQUEST.
+ /// @param client_renews A boolean value which indicates if the test should
+ /// simulate renewals of leases (if true) or reusing expired leases which
+ /// belong to different clients (if false).
+ /// @param use_reclaimed Boolean parameter indicating if the leases being
+ /// reused should initially be reclaimed.
+ void testReclaimReusedLeases(const uint8_t msg_type, const bool client_renews,
+ const bool use_reclaimed);
+
+ /// @brief Callout for lease4_recover
+ ///
+ /// This callout stores passed parameter into static fields.
+ ///
+ /// @param callout_handle will be provided by hooks framework
+ /// @return always 0
+ static int lease4RecoverCallout(CalloutHandle& callout_handle) {
+ callout_name_ = "lease4_recover";
+
+ callout_handle.getArgument("lease4", callout_lease_);
+
+ return (0);
+ }
+
+ /// @brief Callout for lease4_recover that sets status to SKIP
+ ///
+ /// This callout stores passed parameter into static fields.
+ ///
+ /// @param callout_handle will be provided by hooks framework
+ /// @return always 0
+ static int lease4RecoverSkipCallout(CalloutHandle& callout_handle) {
+ // Set the next step status to SKIP
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ return (lease4RecoverCallout(callout_handle));
+ }
+
+ /// @brief Test install a hook callout, recovers declined leases
+ ///
+ /// This test: declines, then expires half of the leases, then
+ /// installs a callout on lease4_recover hook, then reclaims
+ /// expired leases and checks that:
+ /// - the callout was indeed called
+ /// - the parameter (lease4) was indeed passed as expected
+ /// - checks that the leases are removed (skip=false) or
+ /// - checks that the leases are still there (skip=true)
+ /// @param skip should the callout set the next step status to skip?
+ void
+ testReclaimDeclinedHook(bool skip);
+
+ /// The following parameters will be written by a callout
+ static std::string callout_name_; ///< Stores callout name
+ static Lease4Ptr callout_lease_; ///< Stores callout parameter
+};
+
+std::string ExpirationAllocEngine4Test::callout_name_;
+Lease4Ptr ExpirationAllocEngine4Test::callout_lease_;
+
+ExpirationAllocEngine4Test::ExpirationAllocEngine4Test()
+ : ExpirationAllocEngineTest<Lease4Ptr>("type=memfile universe=4 persist=false") {
+ createLeases();
+ callout_argument_name = "lease4";
+
+ // Let's clear any garbage previous test may have left in static fields.
+ callout_name_ = string("");
+ callout_lease_.reset();
+}
+
+void
+ExpirationAllocEngine4Test::createLeases() {
+ // Create TEST_LEASES_NUM leases.
+ for (uint16_t i = 0; i < TEST_LEASES_NUM; ++i) {
+ // HW address
+ std::ostringstream hwaddr_s;
+ hwaddr_s << "01:02:03:04:" << std::setw(2) << std::setfill('0')
+ << (i >> 8) << ":" << std::setw(2) << std::setfill('0')
+ << (i & 0x00FF);
+ HWAddrPtr hwaddr(new HWAddr(HWAddr::fromText(hwaddr_s.str(),
+ HTYPE_ETHER)));
+
+ // Address.
+ std::ostringstream address_s;
+ address_s << "10.0." << (i >> 8) << "." << (i & 0x00FF);
+ IOAddress address(address_s.str());
+
+ // Create lease.
+ Lease4Ptr lease(new Lease4(address, hwaddr, ClientIdPtr(), 60,
+ time(NULL), SubnetID(1), true, true,
+ generateHostnameForLeaseIndex(i)));
+ leases_.push_back(lease);
+ // Copy the lease before adding it to the lease manager. We want to
+ // make sure that modifications to the leases held in the leases_
+ // container doesn't affect the leases in the lease manager.
+ Lease4Ptr tmp(new Lease4(*lease));
+ LeaseMgrFactory::instance().addLease(tmp);
+
+ // Note in the statistics that this lease has been added.
+ StatsMgr& stats_mgr = StatsMgr::instance();
+ std::string stat_name = "assigned-addresses";
+ stats_mgr.addValue(stats_mgr.generateName("subnet", lease->subnet_id_, stat_name),
+ int64_t(1));
+ }
+}
+
+void
+ExpirationAllocEngine4Test::setUniqueClientId(const uint16_t index) {
+ std::ostringstream clientid_s;
+ clientid_s << "AA:BB:" << std::setw(2) << std::setfill('0')
+ << (index >> 8) << ":" << std::setw(2) << std::setfill('0')
+ << (index & 0x00FF);
+ ClientIdPtr client_id(ClientId::fromText(clientid_s.str()));
+ leases_[index]->client_id_ = client_id;
+ LeaseMgrFactory::instance().updateLease4(leases_[index]);
+}
+
+void
+ExpirationAllocEngine4Test::setSubnetId(const uint16_t lease_index, const SubnetID& id) {
+ ASSERT_GT(leases_.size(), lease_index);
+ if (leases_[lease_index]->subnet_id_ != id) {
+ StatsMgr& stats_mgr = StatsMgr::instance();
+ stats_mgr.addValue(stats_mgr.generateName("subnet", id, "assigned-addresses"),
+ int64_t(1));
+ stats_mgr.addValue(stats_mgr.generateName("subnet",
+ leases_[lease_index]->subnet_id_,
+ "assigned-addresses"),
+ int64_t(-1));
+ leases_[lease_index]->subnet_id_ = id;
+ ASSERT_NO_THROW(updateLease(lease_index));
+ }
+}
+
+void
+ExpirationAllocEngine4Test::transferOwnership(const uint16_t lease_index) {
+ ASSERT_GT(leases_.size(), lease_index);
+ std::vector<uint8_t> bytes;
+ if (leases_[lease_index]->client_id_) {
+ bytes = leases_[lease_index]->client_id_->getClientId();
+ } else {
+ bytes = leases_[lease_index]->hwaddr_->hwaddr_;
+ }
+
+ if (!bytes.empty()) {
+ if (++bytes[0] == 0) {
+ ++bytes[1];
+ }
+ }
+
+ if (leases_[lease_index]->client_id_) {
+ leases_[lease_index]->client_id_.reset(new ClientId(bytes));
+ } else {
+ leases_[lease_index]->hwaddr_.reset(new HWAddr(bytes, HTYPE_ETHER));
+ }
+}
+
+
+bool
+ExpirationAllocEngine4Test::dnsUpdateGeneratedFromClientId(const Lease4Ptr& lease) {
+ try {
+ NameChangeRequestPtr ncr = getNCRForLease(lease);
+ if (ncr) {
+ if (lease->client_id_) {
+ // Generate hostname for this lease. Note that the lease
+ // in the database doesn't have the hostname because it
+ // has been removed by the lease reclamation routine.
+ std::string hostname = generateHostnameForLeaseIndex(
+ getLeaseIndexFromAddress(lease->addr_));
+
+ // Get DHCID from NCR.
+ const D2Dhcid& dhcid = ncr->getDhcid();
+ // Generate reference DHCID to compare with the one from
+ // the NCR.
+ std::vector<uint8_t> fqdn_wire;
+ OptionDataTypeUtil::writeFqdn(hostname, fqdn_wire, true);
+ D2Dhcid clientid_dhcid(lease->client_id_->getClientId(),
+ fqdn_wire);
+ // Return true if they match.
+ return (dhcid == clientid_dhcid);
+ }
+ }
+
+ } catch (...) {
+ // If error occurred, treat it as no match.
+ return (false);
+ }
+
+ // All leases checked - no match.
+ return (false);
+}
+
+bool
+ExpirationAllocEngine4Test::dnsUpdateGeneratedFromHWAddress(const Lease4Ptr& lease) {
+ try {
+ NameChangeRequestPtr ncr = getNCRForLease(lease);
+ if (ncr) {
+ if (lease->hwaddr_) {
+ // Generate hostname for this lease. Note that the lease
+ // in the database doesn't have the hostname because it
+ // has been removed by the lease reclamation routine.
+ std::string hostname = generateHostnameForLeaseIndex(
+ getLeaseIndexFromAddress(lease->addr_));
+
+ // Get DHCID from NCR.
+ const D2Dhcid& dhcid = ncr->getDhcid();
+ // Generate reference DHCID to compare with the one from
+ // the NCR.
+ std::vector<uint8_t> fqdn_wire;
+ OptionDataTypeUtil::writeFqdn(hostname, fqdn_wire, true);
+ D2Dhcid hwaddr_dhcid(lease->hwaddr_, fqdn_wire);
+ // Return true if they match.
+ return (dhcid == hwaddr_dhcid);
+ }
+ }
+
+ } catch (...) {
+ // If error occurred, treat it as no match.
+ return (false);
+ }
+
+ // All leases checked - no match.
+ return (false);
+}
+
+void
+ExpirationAllocEngine4Test::testReclaimExpiredLeasesWithDDNSAndClientId() {
+ // DNS must be started for the D2 client to accept NCRs.
+ ASSERT_NO_THROW(enableDDNS());
+
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ // Set client identifiers for leases with even indexes only.
+ if (evenLeaseIndex(i)) {
+ setUniqueClientId(i);
+ }
+ // Expire all leases. The higher the index, the more expired the lease.
+ expire(i, 10 + i);
+ }
+
+ // Reclaim all expired leases.
+ ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, false));
+
+ // Leases with even indexes should be reclaimed.
+ EXPECT_TRUE(testLeases(&leaseReclaimed, &evenLeaseIndex));
+ // DNS updates (removal NCRs) should be generated for all leases.
+ EXPECT_TRUE(testLeases(&dnsUpdateGeneratedForLease, &allLeaseIndexes));
+ // Leases with even indexes include client identifiers so the DHCID should
+ // be generated from the client identifiers.
+ EXPECT_TRUE(testLeases(&dnsUpdateGeneratedFromClientId, &evenLeaseIndex));
+ // Leases with odd indexes do not include client identifiers so their
+ // DHCID should be generated from the HW address.
+ EXPECT_TRUE(testLeases(&dnsUpdateGeneratedFromHWAddress, &oddLeaseIndex));
+}
+
+void
+ExpirationAllocEngine4Test::testReclaimExpiredLeasesStats() {
+ // This test requires that the number of leases is an even number.
+ BOOST_STATIC_ASSERT(TEST_LEASES_NUM % 2 == 0);
+
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ // Mark all leases as expired. The higher the index the less
+ // expired the lease.
+ expire(i, 1000 - i);
+ // Modify subnet ids of some leases.
+ if (evenLeaseIndex(i)) {
+ setSubnetId(i, 2);
+ }
+ }
+
+ // Leases will be reclaimed in groups of 8.
+ const size_t reclamation_group_size = 8;
+ for (unsigned int i = reclamation_group_size; i < TEST_LEASES_NUM;
+ i += reclamation_group_size) {
+
+ // Reclaim 8 most expired leases out of TEST_LEASES_NUM.
+ ASSERT_NO_THROW(reclaimExpiredLeases(reclamation_group_size,
+ 0, false));
+
+ // Number of reclaimed leases should increase as we loop.
+ EXPECT_TRUE(testStatistics("reclaimed-leases", i));
+ // Make sure that the number of reclaimed leases is also distributed
+ // across two subnets.
+ EXPECT_TRUE(testStatistics("subnet[1].reclaimed-leases", i / 2));
+ EXPECT_TRUE(testStatistics("subnet[2].reclaimed-leases", i / 2));
+ // Number of assigned leases should decrease as we reclaim them.
+ EXPECT_TRUE(testStatistics("subnet[1].assigned-addresses",
+ (TEST_LEASES_NUM - i) / 2));
+ EXPECT_TRUE(testStatistics("subnet[2].assigned-addresses",
+ (TEST_LEASES_NUM - i) / 2));
+ }
+}
+
+void
+ExpirationAllocEngine4Test::testReclaimReusedLeases(const uint8_t msg_type,
+ const bool client_renews,
+ const bool use_reclaimed) {
+ // Let's restrict the number of leases.
+ BOOST_STATIC_ASSERT(TEST_LEASES_NUM < 1000);
+
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ // Depending on the parameter, mark leases 'expired-reclaimed' or
+ // simply 'expired'.
+ if (use_reclaimed) {
+ reclaim(i, 1000 - i);
+
+ } else {
+ // Mark all leases as expired.
+ expire(i, 1000 - i);
+ }
+
+ // Check if we're simulating renewals or reusing leases. If this is
+ // about reusing leases, we should be using different MAC addresses
+ // or client identifiers for the leases than those stored presently
+ // in the database.
+ if (!client_renews) {
+ // This function modifies the MAC address or the client identifier
+ // of the test lease to make sure it doesn't match the one we
+ // have in the database.
+ transferOwnership(i);
+ }
+ }
+
+ // The call to AllocEngine::allocateLease4 requires the subnet selection.
+ // The pool must be present within a subnet for the allocation engine to
+ // hand out address from.
+ Subnet4Ptr subnet(new Subnet4(IOAddress("10.0.0.0"), 16, 10, 20, 60, SubnetID(1)));
+ ASSERT_NO_THROW(subnet->addPool(Pool4Ptr(new Pool4(IOAddress("10.0.0.0"),
+ IOAddress("10.0.255.255")))));
+
+ // Re-allocate leases (reuse or renew).
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+ // Build the context.
+ AllocEngine::ClientContext4 ctx(subnet, leases_[i]->client_id_,
+ leases_[i]->hwaddr_,
+ leases_[i]->addr_, false, false,
+ leases_[i]->hostname_,
+ msg_type == DHCPDISCOVER);
+ // Query is needed for logging purposes.
+ ctx.query_.reset(new Pkt4(msg_type, 0x1234));
+
+ // Re-allocate a lease. Note that the iterative will pick addresses
+ // starting from the beginning of the pool. This matches exactly
+ // the set of addresses we have allocated and stored in the database.
+ // Since all leases are marked expired the allocation engine will
+ // reuse them or renew them as appropriate.
+ ASSERT_NO_THROW(engine_->allocateLease4(ctx));
+ }
+
+ // If DHCPDISCOVER is being processed, the leases should not be reclaimed.
+ // Also, the leases should not be reclaimed if they are already in the
+ // 'expired-reclaimed' state.
+ if (use_reclaimed || (msg_type == DHCPDISCOVER)) {
+ EXPECT_TRUE(testStatistics("reclaimed-leases", 0));
+
+ } else if (msg_type == DHCPREQUEST) {
+ // Re-allocation of expired leases should result in reclamations.
+ EXPECT_TRUE(testStatistics("reclaimed-leases", TEST_LEASES_NUM));
+ EXPECT_TRUE(testStatistics("assigned-addresses", TEST_LEASES_NUM, subnet->getID()));
+ // Leases should have been updated in the lease database and their
+ // state should not be 'expired-reclaimed' anymore.
+ EXPECT_TRUE(testLeases(&leaseNotReclaimed, &allLeaseIndexes));
+ }
+}
+
+void
+ExpirationAllocEngine4Test::testReclaimDeclinedHook(bool skip) {
+ for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) {
+
+ // Mark leases with even indexes as expired.
+ if (evenLeaseIndex(i)) {
+
+ // Mark lease as declined with 100 seconds of probation-period
+ // (i.e. lease is supposed to be off limits for 100 seconds)
+ decline(i, 100);
+
+ // The higher the index, the more expired the lease.
+ expire(i, 10 + i);
+ }
+ }
+
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_recover",
+ skip ? lease4RecoverSkipCallout : lease4RecoverCallout));
+
+ // Run leases reclamation routine on all leases.
+ ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, true));
+
+ // Make sure that the callout really was called. It was supposed to modify
+ // the callout_name_ and store the lease in callout_lease_
+ EXPECT_EQ("lease4_recover", callout_name_);
+ EXPECT_TRUE(callout_lease_);
+
+ // Leases with even indexes should not exist in the DB
+ if (skip) {
+ // Skip status should have prevented removing the lease.
+ EXPECT_TRUE(testLeases(&leaseExists, &evenLeaseIndex));
+ } else {
+ // The hook hasn't modified next step status. The lease should be gone.
+ EXPECT_TRUE(testLeases(&leaseDoesntExist, &evenLeaseIndex));
+ }
+};
+
+// This test verifies that the leases can be reclaimed without being removed
+// from the database. In such case, the leases' state is set to
+// "expired-reclaimed".
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesUpdateState) {
+ testReclaimExpiredLeasesUpdateState();
+}
+
+// This test verifies that the reclaimed leases are deleted when requested.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesDelete) {
+ testReclaimExpiredLeasesDelete();
+}
+
+// This test verifies that it is possible to specify the limit for the
+// number of reclaimed leases.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesLimit) {
+ testReclaimExpiredLeasesLimit();
+}
+
+// This test verifies that DNS updates are generated for the leases
+// for which the DNS records exist.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesWithDDNS) {
+ testReclaimExpiredLeasesWithDDNS();
+}
+
+// This test verifies that it is DNS updates are generated only for the
+// reclaimed expired leases. In this case we limit the number of leases
+// reclaimed during a single call to reclamation routine.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesWithDDNSAndLimit) {
+ testReclaimExpiredLeasesWithDDNSAndLimit();
+}
+
+// This test verifies that if some leases have invalid hostnames, the
+// lease reclamation routine continues with reclamation of leases anyway.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesInvalidHostname) {
+ testReclaimExpiredLeasesInvalidHostname();
+}
+
+// This test verifies that DNS updates are properly generated when the
+// client id is used as a primary identifier in the lease.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesWithDDNSAndClientId) {
+ testReclaimExpiredLeasesWithDDNSAndClientId();
+}
+
+// This test verifies that statistics is correctly updated when the leases
+// are reclaimed.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesStats) {
+ testReclaimExpiredLeasesStats();
+}
+
+// This test verifies that callouts are executed for each expired lease.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesHooks) {
+ testReclaimExpiredLeasesHooks();
+}
+
+// This test verifies that callouts are executed for each expired lease
+// and that the lease is not reclaimed when the skip flag is set.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesHooksWithSkip) {
+ testReclaimExpiredLeasesHooksWithSkip();
+}
+
+// This test verifies that it is possible to set the timeout for the
+// execution of the lease reclamation routine.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesTimeout) {
+ // This test needs at least 40 leases to make sense.
+ BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 40);
+ // Run with timeout of 1.2s.
+ testReclaimExpiredLeasesTimeout(1200);
+}
+
+// This test verifies that at least one lease is reclaimed if the timeout
+// for the lease reclamation routine is shorter than the time needed for
+// the reclamation of a single lease. This prevents the situation when
+// very short timeout (perhaps misconfigured) effectively precludes leases
+// reclamation.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesShortTimeout) {
+ // We will most likely reclaim just one lease, so 5 is more than enough.
+ BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 5);
+ // Reclaim leases with the 1ms timeout.
+ testReclaimExpiredLeasesTimeout(1);
+}
+
+// This test verifies that expired-reclaimed leases are removed from the
+// lease database.
+TEST_F(ExpirationAllocEngine4Test, deleteExpiredReclaimedLeases) {
+ BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 10);
+ testDeleteExpiredReclaimedLeases();
+}
+
+/// This test verifies that @ref AllocEngine::reclaimExpiredLeases4 properly
+/// handles declined leases that have expired in case when it is told to
+/// remove leases.
+TEST_F(ExpirationAllocEngine4Test, reclaimDeclined1) {
+ testReclaimDeclined(true);
+}
+
+/// This test verifies that @ref AllocEngine::reclaimExpiredLeases4 properly
+/// handles declined leases that have expired in case when it is told to
+/// not remove leases. This flag should not matter and declined expired
+/// leases should always be removed.
+TEST_F(ExpirationAllocEngine4Test, reclaimDeclined2) {
+ testReclaimDeclined(false);
+}
+
+/// This test verifies that statistics are modified correctly after
+/// reclaim expired leases is called.
+TEST_F(ExpirationAllocEngine4Test, reclaimDeclinedStats) {
+ testReclaimDeclinedStats("assigned-addresses");
+}
+
+// This test verifies that the lease is reclaimed before it is reused.
+TEST_F(ExpirationAllocEngine4Test, reclaimReusedLeases) {
+ // First false value indicates that the leases will be reused.
+ // Second false value indicates that the lease will not be
+ // initially reclaimed.
+ testReclaimReusedLeases(DHCPREQUEST, false, false);
+}
+
+// This test verifies that the lease is not reclaimed when it is
+// reused and if its state indicates that it has been already reclaimed.
+TEST_F(ExpirationAllocEngine4Test, reclaimReusedLeasesAlreadyReclaimed) {
+ // false value indicates that the leases will be reused
+ // true value indicates that the lease will be initially reclaimed.
+ testReclaimReusedLeases(DHCPREQUEST, false, true);
+}
+
+// This test verifies that the expired lease is reclaimed before it
+// is renewed.
+TEST_F(ExpirationAllocEngine4Test, reclaimRenewedLeases) {
+ // true value indicates that the leases will be renewed.
+ // false value indicates that the lease will not be initially
+ // reclaimed.
+ testReclaimReusedLeases(DHCPREQUEST, true, false);
+}
+
+// This test verifies that the lease is not reclaimed upon renewal
+// if its state indicates that it has been already reclaimed.
+TEST_F(ExpirationAllocEngine4Test, reclaimRenewedLeasesAlreadyReclaimed) {
+ // First true value indicates that the leases will be renewed.
+ // Second true value indicates that the lease will be initially
+ // reclaimed.
+ testReclaimReusedLeases(DHCPREQUEST, true, true);
+}
+
+// This test verifies that the reused lease is not reclaimed when the
+// processed message is a DHCPDISCOVER.
+TEST_F(ExpirationAllocEngine4Test, reclaimReusedLeasesDiscover) {
+ testReclaimReusedLeases(DHCPDISCOVER, false, false);
+}
+
+// This test verifies that the lease being in the 'expired-reclaimed'
+// state is not reclaimed again when processing the DHCPDISCOVER
+// message.
+TEST_F(ExpirationAllocEngine4Test, reclaimRenewedLeasesDiscoverAlreadyReclaimed) {
+ testReclaimReusedLeases(DHCPDISCOVER, false, true);
+}
+
+// This test verifies if the hooks installed on lease4_recover are called
+// when the lease expires.
+TEST_F(ExpirationAllocEngine4Test, reclaimDeclinedHook1) {
+ testReclaimDeclinedHook(false); // false = don't use skip callout
+}
+
+// This test verifies if the hooks installed on lease4_recover are called
+// when the lease expires and that the next step status set to SKIP
+// causes the recovery to not be conducted.
+TEST_F(ExpirationAllocEngine4Test, reclaimDeclinedHook2) {
+ testReclaimDeclinedHook(true); // true = use skip callout
+}
+
+}; // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/alloc_engine_hooks_unittest.cc b/src/lib/dhcpsrv/tests/alloc_engine_hooks_unittest.cc
new file mode 100644
index 0000000..935296d
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/alloc_engine_hooks_unittest.cc
@@ -0,0 +1,660 @@
+// Copyright (C) 2015-2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcpsrv/tests/alloc_engine_utils.h>
+#include <dhcpsrv/testutils/test_utils.h>
+
+#include <hooks/server_hooks.h>
+#include <hooks/callout_manager.h>
+#include <hooks/hooks_manager.h>
+
+#include <iostream>
+
+using namespace std;
+using namespace isc::hooks;
+using namespace isc::asiolink;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief helper class used in Hooks testing in AllocEngine6
+///
+/// It features a couple of callout functions and buffers to store
+/// the data that is accessible via callouts.
+class HookAllocEngine6Test : public AllocEngine6Test {
+public:
+ HookAllocEngine6Test() {
+ resetCalloutBuffers();
+ }
+
+ virtual ~HookAllocEngine6Test() {
+ resetCalloutBuffers();
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+ "lease6_select");
+ bool status = HooksManager::unloadLibraries();
+ if (!status) {
+ cerr << "(fixture dtor) unloadLibraries failed" << endl;
+ }
+ }
+
+ /// @brief clears out buffers, so callouts can store received arguments
+ void resetCalloutBuffers() {
+ callback_name_ = string("");
+ callback_subnet6_.reset();
+ callback_fake_allocation_ = false;
+ callback_lease6_.reset();
+ callback_argument_names_.clear();
+ callback_addr_original_ = IOAddress("::");
+ callback_addr_updated_ = IOAddress("::");
+ callback_qry_pkt6_.reset();
+ callback_qry_options_copy_ = false;
+ callback_skip_ = 0;
+ }
+
+ /// @brief Checks if the state of the callout handle associated with a query
+ /// was reset after the callout invocation.
+ ///
+ /// The check includes verification if the status was set to 'continue' and
+ /// that all arguments were deleted.
+ ///
+ /// @param query pointer to the query which callout handle is associated
+ /// with.
+ void checkCalloutHandleReset(const Pkt6Ptr& query) {
+ CalloutHandlePtr callout_handle = query->getCalloutHandle();
+ ASSERT_TRUE(callout_handle);
+ EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, callout_handle->getStatus());
+ EXPECT_TRUE(callout_handle->getArgumentNames().empty());
+ }
+
+ /// callback that stores received callout name and received values
+ static int
+ lease6_select_callout(CalloutHandle& callout_handle) {
+
+ callback_name_ = string("lease6_select");
+
+ callout_handle.getArgument("query6", callback_qry_pkt6_);
+ callout_handle.getArgument("subnet6", callback_subnet6_);
+ callout_handle.getArgument("fake_allocation", callback_fake_allocation_);
+ callout_handle.getArgument("lease6", callback_lease6_);
+
+ callback_addr_original_ = callback_lease6_->addr_;
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+
+ if (callback_qry_pkt6_) {
+ callback_qry_options_copy_ =
+ callback_qry_pkt6_->isCopyRetrievedOptions();
+ }
+
+ return (0);
+ }
+
+ /// callback that overrides the lease with different values
+ static int
+ lease6_select_different_callout(CalloutHandle& callout_handle) {
+
+ // Let's call the basic callout, so it can record all parameters
+ lease6_select_callout(callout_handle);
+
+ // Now we need to tweak the least a bit
+ Lease6Ptr lease;
+ callout_handle.getArgument("lease6", lease);
+ callback_addr_updated_ = addr_override_;
+ lease->addr_ = callback_addr_updated_;
+ lease->preferred_lft_ = pref_override_;
+ lease->valid_lft_ = valid_override_;
+
+ return (0);
+ }
+
+ /// callback that return next step skip status
+ static int
+ lease6_select_skip_callout(CalloutHandle& callout_handle) {
+
+ // Let's call the basic callout, so it can record all parameters
+ lease6_select_callout(callout_handle);
+
+ // Count the call
+ callback_skip_++;
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ return (0);
+ }
+
+ // Values to be used in callout to override lease6 content
+ static const IOAddress addr_override_;
+ static const uint32_t pref_override_;
+ static const uint32_t valid_override_;
+
+ // Callback will store original and overridden values here
+ static IOAddress callback_addr_original_;
+ static IOAddress callback_addr_updated_;
+
+ // Buffers (callback will store received values here)
+ static string callback_name_;
+ static Subnet6Ptr callback_subnet6_;
+ static Lease6Ptr callback_lease6_;
+ static bool callback_fake_allocation_;
+ static vector<string> callback_argument_names_;
+ static Pkt6Ptr callback_qry_pkt6_;
+ static bool callback_qry_options_copy_;
+
+ // Counter for next step skip (should be not retried)
+ static unsigned callback_skip_;
+};
+
+// For some reason initialization within a class makes the linker confused.
+// linker complains about undefined references if they are defined within
+// the class declaration.
+const IOAddress HookAllocEngine6Test::addr_override_("2001:db8::abcd");
+const uint32_t HookAllocEngine6Test::pref_override_ = 8000;
+const uint32_t HookAllocEngine6Test::valid_override_ = 9000;
+
+IOAddress HookAllocEngine6Test::callback_addr_original_("::");
+IOAddress HookAllocEngine6Test::callback_addr_updated_("::");
+
+string HookAllocEngine6Test::callback_name_;
+Subnet6Ptr HookAllocEngine6Test::callback_subnet6_;
+Lease6Ptr HookAllocEngine6Test::callback_lease6_;
+bool HookAllocEngine6Test::callback_fake_allocation_;
+vector<string> HookAllocEngine6Test::callback_argument_names_;
+Pkt6Ptr HookAllocEngine6Test::callback_qry_pkt6_;
+bool HookAllocEngine6Test::callback_qry_options_copy_;
+
+unsigned HookAllocEngine6Test::callback_skip_;
+
+// This test checks if the lease6_select callout is executed and expected
+// parameters as passed.
+TEST_F(HookAllocEngine6Test, lease6_select) {
+
+ // Note: The following order is working as expected:
+ // 1. create AllocEngine (that register hook points)
+ // 2. call loadLibraries()
+ //
+ // This order, however, causes segfault in HooksManager
+ // 1. call loadLibraries()
+ // 2. create AllocEngine (that register hook points)
+
+ // Create allocation engine (hook names are registered in its ctor)
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Initialize Hooks Manager
+ HookLibsCollection libraries; // no libraries at this time
+ ASSERT_NO_THROW(HooksManager::loadLibraries(libraries));
+
+ // Install lease6_select
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease6_select", lease6_select_callout));
+
+ Lease6Ptr lease;
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)),
+ HooksManager::createCalloutHandle());
+ ctx.currentIA().iaid_ = iaid_;
+
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Do all checks on the lease
+ checkLease6(duid_, lease, Lease::TYPE_NA, 128);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Check that callouts were indeed called
+ EXPECT_EQ("lease6_select", callback_name_);
+
+ // Check that query6 argument was set correctly
+ ASSERT_TRUE(callback_qry_pkt6_);
+ EXPECT_EQ(callback_qry_pkt6_.get(), ctx.query_.get());
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ ASSERT_TRUE(callback_lease6_);
+ detailCompareLease(callback_lease6_, from_mgr);
+
+ ASSERT_TRUE(callback_subnet6_);
+ EXPECT_EQ(subnet_->toText(), callback_subnet6_->toText());
+
+ EXPECT_FALSE(callback_fake_allocation_);
+
+ // Check if all expected parameters are reported. The order needs to be
+ // alphabetical to match the order returned by
+ // CallbackHandle::getArgumentNames()
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("fake_allocation");
+ expected_argument_names.push_back("lease6");
+ expected_argument_names.push_back("query6");
+ expected_argument_names.push_back("subnet6");
+
+ sort(callback_argument_names_.begin(), callback_argument_names_.end());
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(ctx.query_);
+}
+
+// This test checks if lease6_select callout is able to override the values
+// in a lease6.
+TEST_F(HookAllocEngine6Test, change_lease6_select) {
+
+ // Make sure that the overridden values are different than the ones from
+ // subnet originally used to create the lease
+ ASSERT_NE(pref_override_, subnet_->getPreferred());
+ ASSERT_NE(valid_override_, subnet_->getValid());
+ ASSERT_FALSE(subnet_->inRange(addr_override_));
+
+ // Create allocation engine (hook names are registered in its ctor)
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Initialize Hooks Manager
+ HookLibsCollection libraries; // no libraries at this time
+ ASSERT_NO_THROW(HooksManager::loadLibraries(libraries));
+
+ // Install a callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease6_select", lease6_select_different_callout));
+
+ // Call allocateLeases6. Callouts should be triggered here.
+ Lease6Ptr lease;
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)),
+ HooksManager::createCalloutHandle());
+ ctx.currentIA().iaid_ = iaid_;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // See if the values overridden by callout are there
+ EXPECT_TRUE(lease->addr_.equals(addr_override_));
+ EXPECT_EQ(pref_override_, lease->preferred_lft_);
+ EXPECT_EQ(valid_override_, lease->valid_lft_);
+
+ // Now check if the lease is in the database
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Check if values in the database are overridden
+ EXPECT_TRUE(from_mgr->addr_.equals(addr_override_));
+ EXPECT_EQ(pref_override_, from_mgr->preferred_lft_);
+ EXPECT_EQ(valid_override_, from_mgr->valid_lft_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(ctx.query_);
+}
+
+// This test checks if lease6_select callout can set the status to next
+// step skip without the engine to retry.
+TEST_F(HookAllocEngine6Test, skip_lease6_select) {
+
+ // Create allocation engine (hook names are registered in its ctor)
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Initialize Hooks Manager
+ HookLibsCollection libraries; // no libraries at this time
+ ASSERT_NO_THROW(HooksManager::loadLibraries(libraries));
+
+ // Install a callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease6_select", lease6_select_skip_callout));
+
+ // Call allocateLeases6. Callouts should be triggered here.
+ Lease6Ptr lease;
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)),
+ HooksManager::createCalloutHandle());
+ ctx.currentIA().iaid_ = iaid_;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+ // Check that we got no lease
+ EXPECT_FALSE(lease);
+
+ // Check no retry was attempted
+ EXPECT_EQ(1, callback_skip_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(ctx.query_);
+}
+
+/// @brief helper class used in Hooks testing in AllocEngine4
+///
+/// It features a couple of callout functions and buffers to store
+/// the data that is accessible via callouts.
+///
+/// Note: lease4_renew callout is tested from DHCPv4 server.
+/// See HooksDhcpv4SrvTest.basic_lease4_renew in
+/// src/bin/dhcp4/tests/dhcp4_srv_unittest.cc
+class HookAllocEngine4Test : public AllocEngine4Test {
+public:
+ HookAllocEngine4Test() {
+ // The default context is not used in these tests.
+ ctx_.callout_handle_.reset();
+ resetCalloutBuffers();
+ }
+
+ virtual ~HookAllocEngine4Test() {
+ resetCalloutBuffers();
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+ "lease4_select");
+ bool status = HooksManager::unloadLibraries();
+ if (!status) {
+ cerr << "(fixture dtor) unloadLibraries failed" << endl;
+ }
+ }
+
+ /// @brief clears out buffers, so callouts can store received arguments
+ void resetCalloutBuffers() {
+ callback_name_ = string("");
+ callback_subnet4_.reset();
+ callback_fake_allocation_ = false;
+ callback_lease4_.reset();
+ callback_argument_names_.clear();
+ callback_addr_original_ = IOAddress("::");
+ callback_addr_updated_ = IOAddress("::");
+ callback_qry_pkt4_.reset();
+ callback_qry_options_copy_ = false;
+ callback_skip_ = 0;
+ }
+
+ /// @brief Checks if the state of the callout handle associated with a query
+ /// was reset after the callout invocation.
+ ///
+ /// The check includes verification if the status was set to 'continue' and
+ /// that all arguments were deleted.
+ ///
+ /// @param query pointer to the query which callout handle is associated
+ /// with.
+ void checkCalloutHandleReset(const Pkt4Ptr& query) {
+ CalloutHandlePtr callout_handle = query->getCalloutHandle();
+ ASSERT_TRUE(callout_handle);
+ EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, callout_handle->getStatus());
+ EXPECT_TRUE(callout_handle->getArgumentNames().empty());
+ }
+
+ /// callback that stores received callout name and received values
+ static int
+ lease4_select_callout(CalloutHandle& callout_handle) {
+
+ callback_name_ = string("lease4_select");
+
+ callout_handle.getArgument("query4", callback_qry_pkt4_);
+ callout_handle.getArgument("subnet4", callback_subnet4_);
+ callout_handle.getArgument("fake_allocation", callback_fake_allocation_);
+ callout_handle.getArgument("lease4", callback_lease4_);
+
+ callback_addr_original_ = callback_lease4_->addr_;
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+
+ if (callback_qry_pkt4_) {
+ callback_qry_options_copy_ =
+ callback_qry_pkt4_->isCopyRetrievedOptions();
+ }
+
+ return (0);
+ }
+
+ /// callback that overrides the lease with different values
+ static int
+ lease4_select_different_callout(CalloutHandle& callout_handle) {
+
+ // Let's call the basic callout, so it can record all parameters
+ lease4_select_callout(callout_handle);
+
+ // Now we need to tweak the least a bit
+ Lease4Ptr lease;
+ callout_handle.getArgument("lease4", lease);
+ callback_addr_updated_ = addr_override_;
+ lease->addr_ = callback_addr_updated_;
+ lease->valid_lft_ = valid_override_;
+
+ return (0);
+ }
+
+ /// callback that return next step skip status
+ static int
+ lease4_select_skip_callout(CalloutHandle& callout_handle) {
+
+ // Let's call the basic callout, so it can record all parameters
+ lease4_select_callout(callout_handle);
+
+ // Count the call
+ callback_skip_++;
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ return (0);
+ }
+
+ // Values to be used in callout to override lease4 content
+ static const IOAddress addr_override_;
+ static const uint32_t valid_override_;
+
+ // Callback will store original and overridden values here
+ static IOAddress callback_addr_original_;
+ static IOAddress callback_addr_updated_;
+
+ // Buffers (callback will store received values here)
+ static string callback_name_;
+ static Subnet4Ptr callback_subnet4_;
+ static Lease4Ptr callback_lease4_;
+ static bool callback_fake_allocation_;
+ static vector<string> callback_argument_names_;
+ static Pkt4Ptr callback_qry_pkt4_;
+ static bool callback_qry_options_copy_;
+
+ // Counter for next step skip (should be not retried)
+ static unsigned callback_skip_;
+};
+
+// For some reason initialization within a class makes the linker confused.
+// linker complains about undefined references if they are defined within
+// the class declaration.
+const IOAddress HookAllocEngine4Test::addr_override_("192.0.3.1");
+const uint32_t HookAllocEngine4Test::valid_override_ = 9000;
+
+IOAddress HookAllocEngine4Test::callback_addr_original_("::");
+IOAddress HookAllocEngine4Test::callback_addr_updated_("::");
+
+string HookAllocEngine4Test::callback_name_;
+Subnet4Ptr HookAllocEngine4Test::callback_subnet4_;
+Lease4Ptr HookAllocEngine4Test::callback_lease4_;
+bool HookAllocEngine4Test::callback_fake_allocation_;
+vector<string> HookAllocEngine4Test::callback_argument_names_;
+Pkt4Ptr HookAllocEngine4Test::callback_qry_pkt4_;
+bool HookAllocEngine4Test::callback_qry_options_copy_;
+
+unsigned HookAllocEngine4Test::callback_skip_;
+
+// This test checks if the lease4_select callout is executed and expected
+// parameters as passed.
+TEST_F(HookAllocEngine4Test, lease4_select) {
+
+ // Note: The following order is working as expected:
+ // 1. create AllocEngine (that register hook points)
+ // 2. call loadLibraries()
+ //
+ // This order, however, causes segfault in HooksManager
+ // 1. call loadLibraries()
+ // 2. create AllocEngine (that register hook points)
+
+ // Create allocation engine (hook names are registered in its ctor)
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
+ ASSERT_TRUE(engine);
+
+ // Initialize Hooks Manager
+ HookLibsCollection libraries; // no libraries at this time
+ ASSERT_NO_THROW(HooksManager::loadLibraries(libraries));
+
+ // Install lease4_select
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_select", lease4_select_callout));
+
+ CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"),
+ false, false, "", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ ctx.callout_handle_ = callout_handle;
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Do all checks on the lease
+ checkLease4(lease);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Check that callouts were indeed called
+ EXPECT_EQ("lease4_select", callback_name_);
+
+ // Check that query4 argument was set correctly
+ ASSERT_TRUE(callback_qry_pkt4_);
+ EXPECT_EQ(callback_qry_pkt4_.get(), ctx.query_.get());
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ ASSERT_TRUE(callback_lease4_);
+ detailCompareLease(callback_lease4_, from_mgr);
+
+ ASSERT_TRUE(callback_subnet4_);
+ EXPECT_EQ(subnet_->toText(), callback_subnet4_->toText());
+
+ EXPECT_EQ(callback_fake_allocation_, false);
+
+ // Check if all expected parameters are reported. The order needs to be
+ // alphabetical to match the order returned by
+ // CallbackHandle::getArgumentNames()
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("fake_allocation");
+ expected_argument_names.push_back("lease4");
+ expected_argument_names.push_back("query4");
+ expected_argument_names.push_back("subnet4");
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+
+ EXPECT_TRUE(callback_qry_options_copy_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(ctx.query_);
+}
+
+// This test checks if lease4_select callout is able to override the values
+// in a lease4.
+TEST_F(HookAllocEngine4Test, change_lease4_select) {
+
+ // Make sure that the overridden values are different than the ones from
+ // subnet originally used to create the lease
+ ASSERT_NE(valid_override_, subnet_->getValid());
+ ASSERT_FALSE(subnet_->inRange(addr_override_));
+
+ // Create allocation engine (hook names are registered in its ctor)
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
+ ASSERT_TRUE(engine);
+
+ // Initialize Hooks Manager
+ HookLibsCollection libraries; // no libraries at this time
+ ASSERT_NO_THROW(HooksManager::loadLibraries(libraries));
+
+ // Install a callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_select", lease4_select_different_callout));
+
+ // Normally, dhcpv4_srv would passed the handle when calling allocateLease4,
+ // but in tests we need to create it on our own.
+ CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+ false, true, "somehost.example.com.", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ ctx.callout_handle_ = callout_handle;
+
+ // Call allocateLease4. Callouts should be triggered here.
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // See if the values overridden by callout are there
+ EXPECT_TRUE(lease->addr_.equals(addr_override_));
+ EXPECT_EQ(valid_override_, lease->valid_lft_);
+
+ // Now check if the lease is in the database
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Check if values in the database are overridden
+ EXPECT_TRUE(from_mgr->addr_.equals(addr_override_));
+ EXPECT_EQ(valid_override_, from_mgr->valid_lft_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(ctx.query_);
+}
+
+// This test checks if lease4_select callout can set the status to next
+// step skip without the engine to retry.
+TEST_F(HookAllocEngine4Test, skip_lease4_select) {
+
+ // Create allocation engine (hook names are registered in its ctor)
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
+ ASSERT_TRUE(engine);
+
+ // Initialize Hooks Manager
+ HookLibsCollection libraries; // no libraries at this time
+ ASSERT_NO_THROW(HooksManager::loadLibraries(libraries));
+
+ // Install a callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_select", lease4_select_skip_callout));
+
+ CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"),
+ false, false, "", false);
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ ctx.callout_handle_ = callout_handle;
+
+ Lease4Ptr lease = engine->allocateLease4(ctx);
+
+ // Check that we got no lease
+ EXPECT_FALSE(lease);
+
+ // Check no retry was attempted
+ EXPECT_EQ(1, callback_skip_);
+
+ // Check if the callout handle state was reset after the callout.
+ checkCalloutHandleReset(ctx.query_);
+}
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
diff --git a/src/lib/dhcpsrv/tests/alloc_engine_utils.cc b/src/lib/dhcpsrv/tests/alloc_engine_utils.cc
new file mode 100644
index 0000000..75270d3
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/alloc_engine_utils.cc
@@ -0,0 +1,655 @@
+// Copyright (C) 2012-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcp/duid.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt6.h>
+#include <dhcpsrv/host_mgr.h>
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/memfile_lease_mgr.h>
+#include <hooks/hooks_manager.h>
+#include <hooks/callout_handle.h>
+#include <stats/stats_mgr.h>
+
+#include <dhcpsrv/testutils/test_utils.h>
+#include <dhcpsrv/tests/alloc_engine_utils.h>
+
+#include <boost/shared_ptr.hpp>
+#include <boost/scoped_ptr.hpp>
+
+#include <iostream>
+#include <sstream>
+#include <algorithm>
+#include <set>
+#include <time.h>
+
+using namespace std;
+using namespace isc::hooks;
+using namespace isc::asiolink;
+using namespace isc::stats;
+using namespace isc::util;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+bool testStatistics(const std::string& stat_name, const int64_t exp_value,
+ const SubnetID subnet_id) {
+ try {
+ std::string name = (subnet_id == SUBNET_ID_UNUSED ? stat_name :
+ StatsMgr::generateName("subnet", subnet_id, stat_name));
+ ObservationPtr observation = StatsMgr::instance().getObservation(name);
+ if (observation) {
+ if (observation->getInteger().first != exp_value) {
+ ADD_FAILURE()
+ << "value of the observed statistics '"
+ << name << "' ("
+ << observation->getInteger().first << ") "
+ << "doesn't match expected value (" << exp_value << ")";
+ }
+ return (observation->getInteger().first == exp_value);
+ } else {
+ ADD_FAILURE() << "Expected statistic " << name
+ << " not found.";
+ }
+
+ } catch (...) {
+ ;
+ }
+ return (false);
+}
+
+int64_t getStatistics(const std::string& stat_name, const SubnetID subnet_id) {
+ try {
+ std::string name = (subnet_id == SUBNET_ID_UNUSED ? stat_name :
+ StatsMgr::generateName("subnet", subnet_id, stat_name));
+ ObservationPtr observation = StatsMgr::instance().getObservation(name);
+ if (observation) {
+ return (observation->getInteger().first);
+ }
+ } catch (...) {
+ ;
+ }
+ return (0);
+}
+
+void
+AllocEngine4Test::testReuseLease4(const AllocEnginePtr& engine,
+ Lease4Ptr& existing_lease,
+ const std::string& addr,
+ const bool fake_allocation,
+ ExpectedResult exp_result,
+ Lease4Ptr& result) {
+ ASSERT_TRUE(engine);
+
+ if (existing_lease) {
+ // If an existing lease was specified, we need to add it to the
+ // database. Let's wipe any leases for that address (if any). We
+ // ignore any errors (previous lease may not exist)
+ (void) LeaseMgrFactory::instance().deleteLease(existing_lease);
+
+ // Let's add it.
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(existing_lease));
+ }
+
+ // A client comes along, asking specifically for a given address
+ AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
+ IOAddress(addr), false, false,
+ "", fake_allocation);
+ if (fake_allocation) {
+ ctx.query_.reset(new Pkt4(DHCPDISCOVER, 1234));
+ } else {
+ ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+ }
+ result = engine->allocateLease4(ctx);
+
+ switch (exp_result) {
+ case SHOULD_PASS:
+ ASSERT_TRUE(result);
+
+ checkLease4(result);
+ break;
+
+ case SHOULD_FAIL:
+ ASSERT_FALSE(result);
+ break;
+ }
+}
+
+Lease4Ptr
+AllocEngine4Test::generateDeclinedLease(const std::string& addr,
+ time_t probation_period,
+ int32_t expired) {
+ // There's an assumption that hardware address is always present for IPv4
+ // packet (always non-null). Client-id is optional (may be null).
+ HWAddrPtr hwaddr(new HWAddr());
+ time_t now = time(NULL);
+ Lease4Ptr declined(new Lease4(addr, hwaddr, ClientIdPtr(), 495,
+ now, subnet_->getID()));
+ declined->decline(probation_period);
+ declined->cltt_ = now - probation_period + expired;
+ return (declined);
+}
+
+AllocEngine6Test::AllocEngine6Test() {
+ Subnet::resetSubnetID();
+ CfgMgr::instance().clear();
+
+ // This lease mgr needs to exist to before configuration commits.
+ factory_.create("type=memfile universe=6 persist=false");
+
+ duid_ = DuidPtr(new DUID(std::vector<uint8_t>(8, 0x42)));
+ iaid_ = 42;
+
+ // Create fresh instance of the HostMgr, and drop any previous HostMgr state.
+ HostMgr::instance().create();
+
+ // Let's use odd hardware type to check if there is no Ethernet
+ // hardcoded anywhere.
+ const uint8_t mac[] = { 0, 1, 22, 33, 44, 55};
+ hwaddr_ = HWAddrPtr(new HWAddr(mac, sizeof(mac), HTYPE_FDDI));
+ // Initialize a subnet and short address pool.
+ initSubnet(IOAddress("2001:db8:1::"),
+ IOAddress("2001:db8:1::10"),
+ IOAddress("2001:db8:1::20"),
+ IOAddress("2001:db8:1:2::"),
+ 64, 80);
+
+ initFqdn("", false, false);
+
+ StatsMgr::instance().resetAll();
+}
+
+void
+AllocEngine6Test::initSubnet(const asiolink::IOAddress& subnet,
+ const asiolink::IOAddress& pool_start,
+ const asiolink::IOAddress& pool_end,
+ const asiolink::IOAddress& pd_pool_prefix,
+ const uint8_t pd_pool_length,
+ const uint8_t pd_delegated_length) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+
+ subnet_ = Subnet6Ptr(new Subnet6(subnet, 56, 100, 200, 300, 400));
+ pool_ = Pool6Ptr(new Pool6(Lease::TYPE_NA, pool_start, pool_end));
+
+ subnet_->addPool(pool_);
+
+ if (!pd_pool_prefix.isV6Zero()) {
+ pd_pool_ = Pool6Ptr(new Pool6(Lease::TYPE_PD, pd_pool_prefix,
+ pd_pool_length, pd_delegated_length));
+ }
+ subnet_->addPool(pd_pool_);
+
+ cfg_mgr.getStagingCfg()->getCfgSubnets6()->add(subnet_);
+ cfg_mgr.commit();
+}
+
+void
+AllocEngine6Test::findReservation(AllocEngine& engine,
+ AllocEngine::ClientContext6& ctx) {
+ engine.findReservation(ctx);
+ // Let's check whether there's a hostname specified in the reservation
+ if (ctx.currentHost()) {
+ std::string hostname = ctx.currentHost()->getHostname();
+ // If there is, let's use it
+ if (!hostname.empty()) {
+ ctx.hostname_ = hostname;
+ }
+ }
+}
+
+HostPtr
+AllocEngine6Test::createHost6HWAddr(bool add_to_host_mgr, IPv6Resrv::Type type,
+ HWAddrPtr& hwaddr, const asiolink::IOAddress& addr,
+ uint8_t prefix_len) {
+ HostPtr host(new Host(&hwaddr->hwaddr_[0], hwaddr->hwaddr_.size(),
+ Host::IDENT_HWADDR, SUBNET_ID_UNUSED, subnet_->getID(),
+ asiolink::IOAddress("0.0.0.0")));
+ IPv6Resrv resv(type, addr, prefix_len);
+ host->addReservation(resv);
+
+ if (add_to_host_mgr) {
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ CfgMgr::instance().commit();
+ }
+ return (host);
+}
+
+Lease6Collection
+AllocEngine6Test::allocateTest(AllocEngine& engine, const Pool6Ptr& pool,
+ const asiolink::IOAddress& hint, bool fake,
+ bool in_pool) {
+ Lease::Type type = pool->getType();
+ uint8_t expected_len = pool->getLength();
+
+ Pkt6Ptr query(new Pkt6(fake ? DHCPV6_SOLICIT : DHCPV6_REQUEST, 1234));
+
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "",
+ fake, query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().type_ = type;
+ ctx.currentIA().addHint(hint);
+
+ Lease6Collection leases;
+
+ findReservation(engine, ctx);
+ EXPECT_NO_THROW(leases = engine.allocateLeases6(ctx));
+
+ for (Lease6Collection::iterator it = leases.begin(); it != leases.end(); ++it) {
+
+ // Do all checks on the lease
+ checkLease6(duid_, *it, type, expected_len, in_pool, in_pool);
+
+ // Check that context has been updated with allocated addresses or
+ // prefixes.
+ checkAllocatedResources(*it, ctx);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(type,
+ (*it)->addr_);
+ if (!fake) {
+ // This is a real (REQUEST) allocation, the lease must be in the DB
+ EXPECT_TRUE(from_mgr) << "Lease " << from_mgr->addr_.toText()
+ << " returned by allocateLeases6(), "
+ << "but was not present in LeaseMgr";
+ if (!from_mgr) {
+ return (leases);
+ }
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(*it, from_mgr);
+ } else {
+ // This is a fake (SOLICIT) allocation, the lease must not be in DB
+ EXPECT_FALSE(from_mgr) << "Lease " << from_mgr->addr_.toText()
+ << " returned by allocateLeases6(), "
+ << "was present in LeaseMgr (expected to be"
+ << " not present)";
+ if (from_mgr) {
+ return (leases);
+ }
+ }
+ }
+
+ return (leases);
+}
+
+Lease6Ptr
+AllocEngine6Test::simpleAlloc6Test(const Pool6Ptr& pool, const IOAddress& hint,
+ bool fake, bool in_pool) {
+ return (simpleAlloc6Test(pool, duid_, hint, fake, in_pool));
+}
+
+Lease6Ptr
+AllocEngine6Test::simpleAlloc6Test(const Pool6Ptr& pool, const IOAddress& hint,
+ uint32_t preferred, uint32_t valid,
+ uint32_t exp_preferred, uint32_t exp_valid) {
+ Lease::Type type = pool->getType();
+ uint8_t expected_len = pool->getLength();
+
+ boost::scoped_ptr<AllocEngine> engine;
+ EXPECT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100)));
+ // We can't use ASSERT macros in non-void methods
+ EXPECT_TRUE(engine);
+ if (!engine) {
+ return (Lease6Ptr());
+ }
+
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false, query);
+ ctx.hwaddr_ = hwaddr_;
+ ctx.addHostIdentifier(Host::IDENT_HWADDR, hwaddr_->hwaddr_);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().type_ = type;
+ ctx.currentIA().addHint(hint, expected_len, preferred, valid);
+ subnet_->setPreferred(Triplet<uint32_t>(200, 300, 400));
+ subnet_->setValid(Triplet<uint32_t>(300, 400, 500));
+
+ // Set some non-standard callout status to make sure it doesn't affect the
+ // allocation.
+ ctx.callout_handle_ = HooksManager::createCalloutHandle();
+ ctx.callout_handle_->setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ findReservation(*engine, ctx);
+ Lease6Ptr lease;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+
+ // Check that we got a lease
+ EXPECT_TRUE(lease);
+ if (!lease) {
+ return (Lease6Ptr());
+ }
+
+ // Do all checks on the lease
+ checkLease6(duid_, lease, type, expected_len, true, true);
+
+ // Check expected preferred and valid lifetimes.
+ EXPECT_EQ(exp_preferred, lease->preferred_lft_);
+ EXPECT_EQ(exp_valid, lease->valid_lft_);
+
+ return (lease);
+}
+
+Lease6Ptr
+AllocEngine6Test::simpleAlloc6Test(const Pool6Ptr& pool, const DuidPtr& duid,
+ const IOAddress& hint, bool fake, bool in_pool) {
+ Lease::Type type = pool->getType();
+ uint8_t expected_len = pool->getLength();
+
+ boost::scoped_ptr<AllocEngine> engine;
+ EXPECT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100)));
+ // We can't use ASSERT macros in non-void methods
+ EXPECT_TRUE(engine);
+ if (!engine) {
+ return (Lease6Ptr());
+ }
+
+ Pkt6Ptr query(new Pkt6(fake ? DHCPV6_SOLICIT : DHCPV6_REQUEST, 1234));
+
+ AllocEngine::ClientContext6 ctx(subnet_, duid, false, false, "", fake, query);
+ ctx.hwaddr_ = hwaddr_;
+ ctx.addHostIdentifier(Host::IDENT_HWADDR, hwaddr_->hwaddr_);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().type_ = type;
+ ctx.currentIA().addHint(hint);
+
+ // Set some non-standard callout status to make sure it doesn't affect the
+ // allocation.
+ ctx.callout_handle_ = HooksManager::createCalloutHandle();
+ ctx.callout_handle_->setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+ findReservation(*engine, ctx);
+ Lease6Ptr lease;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+
+ // Check that we got a lease
+ EXPECT_TRUE(lease);
+ if (!lease) {
+ return (Lease6Ptr());
+ }
+
+ // Do all checks on the lease
+ checkLease6(duid, lease, type, expected_len, in_pool, in_pool);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(type, lease->addr_);
+ if (!fake) {
+ // This is a real (REQUEST) allocation, the lease must be in the DB
+ EXPECT_TRUE(from_mgr);
+ if (!from_mgr) {
+ return (Lease6Ptr());
+ }
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+ } else {
+ // This is a fake (SOLICIT) allocation, the lease must not be in DB
+ EXPECT_FALSE(from_mgr);
+ if (from_mgr) {
+ return (Lease6Ptr());
+ }
+ }
+
+ return (lease);
+}
+
+Lease6Collection
+AllocEngine6Test::renewTest(AllocEngine& engine, const Pool6Ptr& pool,
+ AllocEngine::HintContainer& hints,
+ bool in_pool) {
+
+ Lease::Type type = pool->getType();
+ uint8_t expected_len = pool->getLength();
+
+ Pkt6Ptr query(new Pkt6(DHCPV6_RENEW, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "",
+ false, query);
+ ctx.currentIA().hints_ = hints;
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().type_ = type;
+
+ findReservation(engine, ctx);
+ Lease6Collection leases = engine.renewLeases6(ctx);
+
+ for (Lease6Collection::iterator it = leases.begin(); it != leases.end(); ++it) {
+
+ // Do all checks on the lease
+ checkLease6(duid_, *it, type, expected_len, in_pool, in_pool);
+
+ // Check that context has been updated with allocated addresses or
+ // prefixes.
+ checkAllocatedResources(*it, ctx);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(type,
+ (*it)->addr_);
+
+ // This is a real (REQUEST) allocation, the lease must be in the DB
+ EXPECT_TRUE(from_mgr) << "Lease " << from_mgr->addr_.toText()
+ << " returned by allocateLeases6(), "
+ << "but was not present in LeaseMgr";
+ if (!from_mgr) {
+ return (leases);
+ }
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(*it, from_mgr);
+ }
+
+ return (leases);
+}
+
+void
+AllocEngine6Test::allocWithUsedHintTest(Lease::Type type, IOAddress used_addr,
+ IOAddress requested,
+ uint8_t expected_pd_len) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Let's create a lease and put it in the LeaseMgr
+ DuidPtr duid2 = boost::shared_ptr<DUID>(new DUID(vector<uint8_t>(8, 0xff)));
+ time_t now = time(NULL);
+ Lease6Ptr used(new Lease6(type, used_addr,
+ duid2, 1, 2, now, subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Another client comes in and request an address that is in pool, but
+ // unfortunately it is used already. The same address must not be allocated
+ // twice.
+
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().type_ = type;
+ ctx.currentIA().addHint(requested);
+
+ Lease6Ptr lease;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Allocated address must be different
+ EXPECT_NE(used_addr, lease->addr_);
+
+ // We should NOT get what we asked for, because it is used already
+ EXPECT_NE(requested, lease->addr_);
+
+ // Do all checks on the lease
+ checkLease6(duid_, lease, type, expected_pd_len);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+}
+
+void
+AllocEngine6Test::allocBogusHint6(Lease::Type type, asiolink::IOAddress hint,
+ uint8_t expected_pd_len) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Client would like to get a 3000::abc lease, which does not belong to any
+ // supported lease. Allocation engine should ignore it and carry on
+ // with the normal allocation
+
+ Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
+ query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().type_ = type;
+ ctx.currentIA().addHint(hint);
+
+ Lease6Ptr lease;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // We should NOT get what we asked for, because it is used already
+ EXPECT_NE(hint, lease->addr_);
+
+ // Do all checks on the lease
+ checkLease6(duid_, lease, type, expected_pd_len);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+}
+
+void
+AllocEngine6Test::testReuseLease6(const AllocEnginePtr& engine,
+ Lease6Ptr& existing_lease,
+ const std::string& addr,
+ const bool fake_allocation,
+ ExpectedResult exp_result,
+ Lease6Ptr& result) {
+ ASSERT_TRUE(engine);
+
+ if (existing_lease) {
+ // If an existing lease was specified, we need to add it to the
+ // database. Let's wipe any leases for that address (if any). We
+ // ignore any errors (previous lease may not exist)
+ (void) LeaseMgrFactory::instance().deleteLease(existing_lease);
+
+ // Let's add it.
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(existing_lease));
+ }
+
+ // A client comes along, asking specifically for a given address
+
+ Pkt6Ptr query(new Pkt6(fake_allocation ? DHCPV6_SOLICIT : DHCPV6_REQUEST, 1234));
+ AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "",
+ fake_allocation, query);
+ ctx.currentIA().iaid_ = iaid_;
+ ctx.currentIA().addHint(IOAddress(addr));
+
+ Lease6Collection leases;
+
+ leases = engine->allocateLeases6(ctx);
+
+ switch (exp_result) {
+ case SHOULD_PASS:
+ ASSERT_FALSE(leases.empty());
+ ASSERT_EQ(1, leases.size());
+ result = leases[0];
+
+ checkLease6(duid_, result, Lease::TYPE_NA, 128);
+ break;
+
+ case SHOULD_FAIL:
+ ASSERT_TRUE(leases.empty());
+ break;
+ }
+}
+
+Lease6Ptr
+AllocEngine6Test::generateDeclinedLease(const std::string& addr,
+ time_t probation_period,
+ int32_t expired) {
+ Lease6Ptr declined(new Lease6(Lease::TYPE_NA, IOAddress(addr),
+ duid_, iaid_, 100, 100, subnet_->getID()));
+
+ time_t now = time(NULL);
+ declined->decline(probation_period);
+ declined->cltt_ = now - probation_period + expired;
+ return (declined);
+}
+
+void
+AllocEngine4Test::initSubnet(const asiolink::IOAddress& pool_start,
+ const asiolink::IOAddress& pool_end) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+
+ subnet_ = Subnet4Ptr(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3));
+ pool_ = Pool4Ptr(new Pool4(pool_start, pool_end));
+ subnet_->addPool(pool_);
+
+ cfg_mgr.getStagingCfg()->getCfgSubnets4()->add(subnet_);
+}
+
+AllocEngine4Test::AllocEngine4Test() {
+ Subnet::resetSubnetID();
+ CfgMgr::instance().clear();
+
+ // This lease mgr needs to exist to before configuration commits.
+ factory_.create("type=memfile universe=4 persist=false");
+
+ // Create fresh instance of the HostMgr, and drop any previous HostMgr state.
+ HostMgr::instance().create();
+
+ clientid_ = ClientIdPtr(new ClientId(vector<uint8_t>(8, 0x44)));
+ clientid2_ = ClientIdPtr(new ClientId(vector<uint8_t>(8, 0x56)));
+
+ uint8_t mac[] = { 0, 1, 22, 33, 44, 55};
+
+ // Let's use odd hardware type to check if there is no Ethernet
+ // hardcoded anywhere.
+ hwaddr_ = HWAddrPtr(new HWAddr(mac, sizeof(mac), HTYPE_FDDI));
+
+ // Allocate different MAC address for the tests that require two
+ // different MAC addresses.
+ ++mac[sizeof(mac) - 1];
+ hwaddr2_ = HWAddrPtr(new HWAddr(mac, sizeof (mac), HTYPE_FDDI));
+
+ // instantiate cfg_mgr
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+
+ initSubnet(IOAddress("192.0.2.100"), IOAddress("192.0.2.109"));
+ cfg_mgr.commit();
+
+
+ // Create a default context. Note that remaining parameters must be
+ // assigned when needed.
+ ctx_.subnet_ = subnet_;
+ ctx_.clientid_ = clientid_;
+ ctx_.hwaddr_ = hwaddr_;
+ ctx_.callout_handle_ = HooksManager::createCalloutHandle();
+ ctx_.query_.reset(new Pkt4(DHCPREQUEST, 1234));
+
+ StatsMgr::instance().resetAll();
+}
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
diff --git a/src/lib/dhcpsrv/tests/alloc_engine_utils.h b/src/lib/dhcpsrv/tests/alloc_engine_utils.h
new file mode 100644
index 0000000..2e4ffbe
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/alloc_engine_utils.h
@@ -0,0 +1,631 @@
+// Copyright (C) 2015-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef LIBDHCPSRV_ALLOC_ENGINE_UTILS_H
+#define LIBDHCPSRV_ALLOC_ENGINE_UTILS_H
+
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/alloc_engine.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <asiolink/io_address.h>
+#include <gtest/gtest.h>
+#include <vector>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @file alloc_engine_utils.h
+///
+/// @brief This is a header file for all Allocation Engine tests.
+///
+/// There used to be one, huge (over 3kloc) alloc_engine_unittest.cc. It is now
+/// split into serveral smaller files:
+/// alloc_engine_utils.h - contains test class definitions (this file)
+/// alloc_engine_utils.cc - contains test class implementation
+/// alloc_engine4_unittest.cc - all unit-tests dedicated to IPv4
+/// alloc_engine6_unittest.cc - all unit-tests dedicated to IPv6
+/// alloc_engine_hooks_unittest.cc - all unit-tests dedicated to hooks
+
+
+/// @brief Test that statistic manager holds a given value.
+///
+/// This function may be used in many allocation tests and there's no
+/// single base class for it. @todo consider moving it src/lib/util.
+///
+/// @param stat_name Statistic name.
+/// @param exp_value Expected value.
+/// @param subnet_id subnet_id of the desired subnet, if not zero
+///
+/// @return true if the statistic manager holds a particular value,
+/// false otherwise.
+bool testStatistics(const std::string& stat_name, const int64_t exp_value,
+ const SubnetID subnet_id = SUBNET_ID_UNUSED);
+
+/// @brief Get a value held by statistic manager.
+///
+/// This function may be used in some allocation tests and there's no
+/// single base class for it. @todo consider moving it src/lib/util.
+///
+/// @param stat_name Statistic name.
+/// @param subnet_id subnet_id of the desired subnet, if not zero.
+/// @return the value held by the statistic manager or zero.
+int64_t getStatistics(const std::string& stat_name,
+ const SubnetID subnet_id = SUBNET_ID_UNUSED);
+
+/// @brief Allocation engine with some internal methods exposed
+class NakedAllocEngine : public AllocEngine {
+public:
+
+ /// @brief the sole constructor
+ /// @param engine_type specifies engine type (e.g. iterative)
+ /// @param attempts number of lease selection attempts before giving up
+ /// @param ipv6 specifies if the engine is IPv6 or IPv4
+ NakedAllocEngine(AllocEngine::AllocType engine_type,
+ unsigned int attempts, bool ipv6 = true)
+ :AllocEngine(engine_type, attempts, ipv6) {
+ }
+
+ // Expose internal classes for testing purposes
+ using AllocEngine::Allocator;
+ using AllocEngine::IterativeAllocator;
+ using AllocEngine::getAllocator;
+ using AllocEngine::updateLease4ExtendedInfo;
+
+ /// @brief IterativeAllocator with internal methods exposed
+ class NakedIterativeAllocator: public AllocEngine::IterativeAllocator {
+ public:
+
+ /// @brief constructor
+ /// @param type pool types that will be iterated through
+ NakedIterativeAllocator(Lease::Type type)
+ :IterativeAllocator(type) {
+ }
+
+ using AllocEngine::IterativeAllocator::increaseAddress;
+ using AllocEngine::IterativeAllocator::increasePrefix;
+ };
+
+ /// @brief Wrapper method for invoking AllocEngine4::updateLease4ExtendedInfo().
+ /// @param lease lease to update
+ /// @param ctx current packet processing context
+ /// @return true if extended information was changed
+ bool callUpdateLease4ExtendedInfo(const Lease4Ptr& lease,
+ AllocEngine::ClientContext4& ctx) const {
+ return (updateLease4ExtendedInfo(lease, ctx));
+ }
+
+ /// @brief Wrapper method for invoking AllocEngine6::updateLease6ExtendedInfo().
+ /// @param lease lease to update
+ /// @param ctx current packet processing context
+ /// @return true if extended information was changed
+ bool callUpdateLease6ExtendedInfo(const Lease6Ptr& lease,
+ AllocEngine::ClientContext6& ctx) const {
+ return (updateLease6ExtendedInfo(lease, ctx));
+ }
+
+};
+
+/// @brief Used in Allocation Engine tests for IPv6
+class AllocEngine6Test : public ::testing::Test {
+public:
+
+ /// @brief Specified expected result of a given operation
+ enum ExpectedResult {
+ SHOULD_PASS,
+ SHOULD_FAIL
+ };
+
+ /// @brief Default constructor
+ ///
+ /// Sets duid_, iaid_, subnet_, pool_ fields to example values used
+ /// in many tests, initializes cfg_mgr configuration and creates
+ /// lease database.
+ AllocEngine6Test();
+
+ /// @brief Configures a subnet and adds one pool to it.
+ ///
+ /// This function removes existing v6 subnets before configuring
+ /// a new one.
+ ///
+ /// @param subnet Address of a subnet to be configured.
+ /// @param pool_start First address in the address pool.
+ /// @param pool_end Last address in the address pool.
+ /// @param pd_pool_prefix Prefix for the prefix delegation pool. It
+ /// defaults to 0 which means that PD pool is not specified.
+ /// @param pd_pool_length Length of the PD pool prefix.
+ /// @param pd_delegated_length Delegated prefix length.
+ void initSubnet(const asiolink::IOAddress& subnet,
+ const asiolink::IOAddress& pool_start,
+ const asiolink::IOAddress& pool_end,
+ const asiolink::IOAddress& pd_pool_prefix =
+ asiolink::IOAddress::IPV6_ZERO_ADDRESS(),
+ const uint8_t pd_pool_length = 0,
+ const uint8_t pd_delegated_length = 0);
+
+
+ /// @brief Initializes FQDN data for a test.
+ ///
+ /// The initialized values are used by the test fixture class members to
+ /// verify the correctness of a lease.
+ ///
+ /// @param hostname Hostname to be assigned to a lease.
+ /// @param fqdn_fwd Indicates whether or not to perform forward DNS update
+ /// for a lease.
+ /// @param fqdn_fwd Indicates whether or not to perform reverse DNS update
+ /// for a lease.
+ void initFqdn(const std::string& hostname, const bool fqdn_fwd,
+ const bool fqdn_rev) {
+ hostname_ = hostname;
+ fqdn_fwd_ = fqdn_fwd;
+ fqdn_rev_ = fqdn_rev;
+ }
+
+ /// @brief Wrapper around call to AllocEngine6::findReservation
+ ///
+ /// If a reservation is found by the engine, the function sets
+ /// ctx.hostname_ accordingly.
+ ///
+ /// @param engine allocation engine to use
+ /// @param ctx client context to pass into engine's findReservation method
+ void findReservation(AllocEngine& engine, AllocEngine::ClientContext6& ctx);
+
+ /// @brief attempts to convert leases collection to a single lease
+ ///
+ /// This operation makes sense if there is at most one lease in the
+ /// collection. Otherwise it will throw.
+ ///
+ /// @param col collection of leases (zero or one leases allowed)
+ /// @throw MultipleRecords if there is more than one lease
+ /// @return Lease6 pointer (or NULL if collection was empty)
+ Lease6Ptr expectOneLease(const Lease6Collection& col) {
+ if (col.size() > 1) {
+ isc_throw(db::MultipleRecords, "More than one lease found in collection");
+ }
+ if (col.empty()) {
+ return (Lease6Ptr());
+ }
+ return (*col.begin());
+ }
+
+ /// @brief checks if Lease6 matches expected configuration
+ ///
+ /// @param duid pointer to the client's DUID.
+ /// @param lease lease to be checked
+ /// @param exp_type expected lease type
+ /// @param exp_pd_len expected prefix length
+ /// @param expected_in_subnet whether the lease is expected to be in subnet
+ /// @param expected_in_pool whether the lease is expected to be in dynamic
+ void checkLease6(const DuidPtr& duid, const Lease6Ptr& lease,
+ Lease::Type exp_type, uint8_t exp_pd_len = 128,
+ bool expected_in_subnet = true,
+ bool expected_in_pool = true) {
+
+ // that is belongs to the right subnet
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+
+ if (expected_in_subnet) {
+ EXPECT_TRUE(subnet_->inRange(lease->addr_))
+ << " address: " << lease->addr_.toText();
+ } else {
+ EXPECT_FALSE(subnet_->inRange(lease->addr_))
+ << " address: " << lease->addr_.toText();
+ }
+
+ if (expected_in_pool) {
+ EXPECT_TRUE(subnet_->inPool(exp_type, lease->addr_));
+ } else {
+ EXPECT_FALSE(subnet_->inPool(exp_type, lease->addr_));
+ }
+
+ // that it have proper parameters
+ EXPECT_EQ(exp_type, lease->type_);
+ EXPECT_EQ(iaid_, lease->iaid_);
+ if (subnet_->getValid().getMin() == subnet_->getValid().getMax()) {
+ EXPECT_EQ(subnet_->getValid(), lease->valid_lft_);
+ } else {
+ EXPECT_LE(subnet_->getValid().getMin(), lease->valid_lft_);
+ EXPECT_GE(subnet_->getValid().getMax(), lease->valid_lft_);
+ }
+ if (subnet_->getPreferred().getMin() == subnet_->getPreferred().getMax()) {
+ EXPECT_EQ(subnet_->getPreferred(), lease->preferred_lft_);
+ } else {
+ EXPECT_LE(subnet_->getPreferred().getMin(), lease->preferred_lft_);
+ EXPECT_GE(subnet_->getPreferred().getMax(), lease->preferred_lft_);
+ }
+ EXPECT_EQ(exp_pd_len, lease->prefixlen_);
+ EXPECT_EQ(fqdn_fwd_, lease->fqdn_fwd_);
+ EXPECT_EQ(fqdn_rev_, lease->fqdn_rev_);
+ EXPECT_EQ(hostname_, lease->hostname_);
+ EXPECT_TRUE(*lease->duid_ == *duid);
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+ /// @todo: check cltt
+ }
+
+ /// @brief Checks if specified address or prefix has been recorded as
+ /// allocated to the client.
+ ///
+ /// @param lease Allocated lease.
+ /// @param ctx Context structure in which this function should check if
+ /// leased address is stored as allocated resource.
+ void checkAllocatedResources(const Lease6Ptr& lease,
+ AllocEngine::ClientContext6& ctx) {
+ EXPECT_TRUE(ctx.isAllocated(lease->addr_, lease->prefixlen_));
+ }
+
+ /// @brief Checks if specified address is increased properly
+ ///
+ /// Method uses gtest macros to mark check failure. This is a proxy
+ /// method, since increaseAddress was moved to IOAddress class.
+ ///
+ /// @param alloc IterativeAllocator that is tested
+ /// @param input address to be increased
+ /// @param exp_output expected address after increase
+ void
+ checkAddrIncrease(NakedAllocEngine::NakedIterativeAllocator& alloc,
+ std::string input, std::string exp_output) {
+ EXPECT_EQ(exp_output, alloc.increaseAddress(asiolink::IOAddress(input),
+ false, 0).toText());
+ }
+
+ /// @brief Checks if increasePrefix() works as expected
+ ///
+ /// Method uses gtest macros to mark check failure.
+ ///
+ /// @param alloc allocator to be tested
+ /// @param input IPv6 prefix (as a string)
+ /// @param prefix_len prefix len
+ /// @param exp_output expected output (string)
+ void
+ checkPrefixIncrease(NakedAllocEngine::NakedIterativeAllocator& alloc,
+ std::string input, uint8_t prefix_len,
+ std::string exp_output) {
+ EXPECT_EQ(exp_output, alloc.increasePrefix(asiolink::IOAddress(input),
+ prefix_len).toText());
+ }
+
+ /// @brief Checks if the simple allocation can succeed
+ ///
+ /// The type of lease is determined by pool type (pool->getType())
+ ///
+ /// @param pool pool from which the lease will be allocated from
+ /// @param hint address to be used as a hint
+ /// @param fake true - this is fake allocation (SOLICIT)
+ /// @param in_pool specifies whether the lease is expected to be in pool
+ /// @return allocated lease (or NULL)
+ Lease6Ptr simpleAlloc6Test(const Pool6Ptr& pool,
+ const asiolink::IOAddress& hint,
+ bool fake, bool in_pool = true);
+
+ /// @brief Checks if the simple allocation can succeed with lifetimes.
+ ///
+ /// The type of lease is determined by pool type (pool->getType())
+ ///
+ /// @param pool pool from which the lease will be allocated from
+ /// @param hint address to be used as a hint
+ /// @param preferred preferred lifetime to be used as a hint
+ /// @param valid valid lifetime to be used as a hint
+ /// @param exp_preferred expected lease preferred lifetime
+ /// @param exp_valid expected lease valid lifetime
+ /// @return allocated lease (or NULL)
+ Lease6Ptr simpleAlloc6Test(const Pool6Ptr& pool,
+ const asiolink::IOAddress& hint,
+ uint32_t preferred, uint32_t valid,
+ uint32_t exp_preferred, uint32_t exp_valid);
+
+ /// @brief Checks if the simple allocation can succeed for custom DUID.
+ ///
+ /// The type of lease is determined by pool type (pool->getType())
+ ///
+ /// @param pool pool from which the lease will be allocated from
+ /// @param duid pointer to the DUID used for allocation.
+ /// @param hint address to be used as a hint
+ /// @param fake true - this is fake allocation (SOLICIT)
+ /// @param in_pool specifies whether the lease is expected to be in pool
+ /// @return allocated lease (or NULL)
+ Lease6Ptr simpleAlloc6Test(const Pool6Ptr& pool, const DuidPtr& duid,
+ const asiolink::IOAddress& hint,
+ bool fake, bool in_pool = true);
+
+
+ /// @brief Checks if the allocation can succeed.
+ ///
+ /// The type of lease is determined by pool type (pool->getType()).
+ /// This test is particularly useful in connection with @ref renewTest.
+ ///
+ /// @param engine a reference to Allocation Engine
+ /// @param pool pool from which the lease will be allocated from
+ /// @param hint address to be used as a hint
+ /// @param fake true - this is fake allocation (SOLICIT)
+ /// @param in_pool specifies whether the lease is expected to be in pool
+ /// @return allocated lease(s) (may be empty)
+ Lease6Collection allocateTest(AllocEngine& engine, const Pool6Ptr& pool,
+ const asiolink::IOAddress& hint, bool fake,
+ bool in_pool = true);
+
+ /// @brief Checks if the allocation can be renewed.
+ ///
+ /// The type of lease is determined by pool type (pool->getType()).
+ /// This test is particularly useful as a follow up to @ref allocateTest.
+ ///
+ /// @param engine a reference to Allocation Engine
+ /// @param pool pool from which the lease will be allocated from
+ /// @param hints address to be used as a hint
+ /// @param in_pool specifies whether the lease is expected to be in pool
+ /// @return allocated lease(s) (may be empty)
+ Lease6Collection renewTest(AllocEngine& engine, const Pool6Ptr& pool,
+ AllocEngine::HintContainer& hints,
+ bool in_pool = true);
+
+ /// @brief Checks if the address allocation with a hint that is in range,
+ /// in pool, but is currently used, can succeed
+ ///
+ /// Method uses gtest macros to mark check failure.
+ ///
+ /// @param type lease type
+ /// @param used_addr address should be preallocated (simulates prior
+ /// allocation by some other user)
+ /// @param requested address requested by the client
+ /// @param expected_pd_len expected PD len (128 for addresses)
+ void allocWithUsedHintTest(Lease::Type type, asiolink::IOAddress used_addr,
+ asiolink::IOAddress requested,
+ uint8_t expected_pd_len);
+
+ /// @brief Generic test used for IPv6 lease allocation and reuse
+ ///
+ /// This test inserts existing_lease (if specified, may be null) into the
+ /// LeaseMgr, then conducts lease allocation (pretends that client
+ /// sent either Solicit or Request, depending on fake_allocation).
+ /// Allocated lease is then returned (using result) for further inspection.
+ ///
+ /// @param alloc_engine allocation engine
+ /// @param existing_lease optional lease to be inserted in the database
+ /// @param addr address to be requested by client
+ /// @param fake_allocation true = SOLICIT, false = REQUEST
+ /// @param exp_result expected result
+ /// @param result [out] allocated lease
+ void testReuseLease6(const AllocEnginePtr& alloc_engine,
+ Lease6Ptr& existing_lease,
+ const std::string& addr,
+ const bool fake_allocation,
+ ExpectedResult exp_result,
+ Lease6Ptr& result);
+
+ /// @brief Creates a declined IPv6 lease with specified expiration time
+ ///
+ /// expired parameter controls probation period. Positive value
+ /// means that the lease will expire in X seconds. Negative means
+ /// that the lease expired X seconds ago. 0 means it expires now.
+ /// Probation period is a parameter that specifies for how long
+ /// a lease will stay unavailable after decline.
+ ///
+ /// @param addr address of the lease
+ /// @param probation_period expressed in seconds
+ /// @param expired number of seconds when the lease will expire
+ Lease6Ptr generateDeclinedLease(const std::string& addr,
+ time_t probation_period,
+ int32_t expired);
+
+ /// @brief checks if bogus hint can be ignored and the allocation succeeds
+ ///
+ /// This test checks if the allocation with a hing that is out of the blue
+ /// can succeed. The invalid hint should be ignored completely.
+ ///
+ /// @param type Lease type
+ /// @param hint hint (as sent by a client)
+ /// @param expected_pd_len (used in validation)
+ void allocBogusHint6(Lease::Type type, asiolink::IOAddress hint,
+ uint8_t expected_pd_len);
+
+ /// @brief Utility function that creates a host reservation (duid)
+ ///
+ /// @param add_to_host_mgr true if the reservation should be added
+ /// @param type specifies reservation type
+ /// @param addr specifies reserved address or prefix
+ /// @param prefix_len prefix length (should be 128 for addresses)
+ /// @return created Host object.
+ HostPtr
+ createHost6(bool add_to_host_mgr, IPv6Resrv::Type type,
+ const asiolink::IOAddress& addr, uint8_t prefix_len) {
+ HostPtr host(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
+ Host::IDENT_DUID, SUBNET_ID_UNUSED, subnet_->getID(),
+ asiolink::IOAddress("0.0.0.0")));
+ IPv6Resrv resv(type, addr, prefix_len);
+ host->addReservation(resv);
+
+ if (add_to_host_mgr) {
+ addHost(host);
+ }
+
+ return (host);
+ }
+
+ /// @brief Add a host reservation to the current configuration
+ ///
+ /// Adds the given host reservation to the current configuration by
+ /// casting it to non-const. We do it this way rather than adding it to
+ /// staging and then committing as that wipes out the current configuration
+ /// such as subnets.
+ ///
+ /// @param host host reservation to add
+ void
+ addHost(HostPtr& host) {
+ SrvConfigPtr cfg = boost::const_pointer_cast<SrvConfig>(CfgMgr::instance().getCurrentCfg());
+ cfg->getCfgHosts()->add(host);
+ }
+
+ /// @brief Utility function that creates a host reservation (hwaddr)
+ ///
+ /// @param add_to_host_mgr true if the reservation should be added
+ /// @param type specifies reservation type
+ /// @param hwaddr hardware address to be reserved to
+ /// @param addr specifies reserved address or prefix
+ /// @param prefix_len prefix length (should be 128 for addresses)
+ /// @return created Host object.
+ HostPtr
+ createHost6HWAddr(bool add_to_host_mgr, IPv6Resrv::Type type,
+ HWAddrPtr& hwaddr, const asiolink::IOAddress& addr,
+ uint8_t prefix_len);
+
+ /// @brief Utility function that decrements cltt of a persisted lease
+ ///
+ /// This function is used to simulate the passage of time by decrementing
+ /// the lease's cltt, currently by 1. It fetches the desired lease from the
+ /// lease manager, decrements the cltt, then updates the lease in the lease
+ /// manager. Next, it refetches the lease and verifies the update took place.
+ ///
+ /// @param[in][out] lease pointer reference to the lease to modify. Upon
+ /// return it will point to the newly updated lease.
+ void
+ rollbackPersistedCltt(Lease6Ptr& lease) {
+ ASSERT_TRUE(lease) << "rollbackPersistedCltt lease is empty";
+
+ // Fetch it, so we can update it.
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr) << "rollbackPersistedCltt: lease not found?";
+
+ // Decrement cltt then update it in the manager.
+ --from_mgr->cltt_;
+ ASSERT_NO_THROW(LeaseMgrFactory::instance().updateLease6(from_mgr));
+
+ // Fetch it fresh.
+ lease = LeaseMgrFactory::instance().getLease6(lease->type_, lease->addr_);
+
+ // Make sure it stuck.
+ ASSERT_EQ(lease->cltt_, from_mgr->cltt_);
+ }
+
+ virtual ~AllocEngine6Test() {
+ factory_.destroy();
+ }
+
+ DuidPtr duid_; ///< client-identifier (value used in tests)
+ HWAddrPtr hwaddr_; ///< client's hardware address
+ uint32_t iaid_; ///< IA identifier (value used in tests)
+ Subnet6Ptr subnet_; ///< subnet6 (used in tests)
+ Pool6Ptr pool_; ///< NA pool belonging to subnet_
+ Pool6Ptr pd_pool_; ///< PD pool belonging to subnet_
+ std::string hostname_; ///< Hostname
+ bool fqdn_fwd_; ///< Perform forward update for a lease.
+ bool fqdn_rev_; ///< Perform reverse update for a lease.
+ LeaseMgrFactory factory_; ///< pointer to LeaseMgr factory
+ ClientClasses cc_; ///< client classes
+};
+
+/// @brief Used in Allocation Engine tests for IPv4
+class AllocEngine4Test : public ::testing::Test {
+public:
+
+ /// @brief Specified expected result of a given operation
+ enum ExpectedResult {
+ SHOULD_PASS,
+ SHOULD_FAIL
+ };
+
+ /// @brief Default constructor
+ ///
+ /// Sets clientid_, hwaddr_, subnet_, pool_ fields to example values
+ /// used in many tests, initializes cfg_mgr configuration and creates
+ /// lease database.
+ ///
+ /// It also re-initializes the Host Manager.
+ AllocEngine4Test();
+
+ /// @brief checks if Lease4 matches expected configuration
+ ///
+ /// @param lease lease to be checked
+ void checkLease4(const Lease4Ptr& lease) {
+ // Check that is belongs to the right subnet
+ EXPECT_EQ(lease->subnet_id_, subnet_->getID());
+ EXPECT_TRUE(subnet_->inRange(lease->addr_));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, lease->addr_));
+
+ // Check that it has proper parameters
+ if (subnet_->getValid().getMin() == subnet_->getValid().getMax()) {
+ EXPECT_EQ(subnet_->getValid(), lease->valid_lft_);
+ } else {
+ EXPECT_LE(subnet_->getValid().getMin(), lease->valid_lft_);
+ EXPECT_GE(subnet_->getValid().getMax(), lease->valid_lft_);
+ }
+ if (lease->client_id_ && !clientid_) {
+ ADD_FAILURE() << "Lease4 has a client-id, while it should have none.";
+ } else
+ if (!lease->client_id_ && clientid_) {
+ ADD_FAILURE() << "Lease4 has no client-id, but it was expected to have one.";
+ } else
+ if (lease->client_id_ && clientid_) {
+ EXPECT_TRUE(*lease->client_id_ == *clientid_);
+ }
+ EXPECT_TRUE(*lease->hwaddr_ == *hwaddr_);
+ EXPECT_EQ(0, lease->reuseable_valid_lft_);
+ /// @todo: check cltt
+ }
+
+ /// @brief Generic test used for IPv4 lease allocation and reuse
+ ///
+ /// This test inserts existing_lease (if specified, may be null) into the
+ /// LeaseMgr, then conducts lease allocation (pretends that client
+ /// sent either Discover or Request, depending on fake_allocation).
+ /// Allocated lease is then returned (using result) for further inspection.
+ ///
+ /// @param alloc_engine allocation engine
+ /// @param existing_lease optional lease to be inserted in the database
+ /// @param addr address to be requested by client
+ /// @param fake_allocation true = DISCOVER, false = REQUEST
+ /// @param exp_result expected result
+ /// @param result [out] allocated lease
+ void testReuseLease4(const AllocEnginePtr& alloc_engine,
+ Lease4Ptr& existing_lease,
+ const std::string& addr,
+ const bool fake_allocation,
+ ExpectedResult exp_result,
+ Lease4Ptr& result);
+
+ /// @brief Creates a declined IPv4 lease with specified expiration time
+ ///
+ /// expired parameter controls probation period. Positive value
+ /// means that the lease will expire in X seconds. Negative means
+ /// that the lease expired X seconds ago. 0 means it expires now.
+ /// Probation period is a parameter that specifies for how long
+ /// a lease will stay unavailable after decline.
+ ///
+ /// @param addr address of the lease
+ /// @param probation_period expressed in seconds
+ /// @param expired number of seconds when the lease will expire
+ Lease4Ptr generateDeclinedLease(const std::string& addr,
+ time_t probation_period,
+ int32_t expired);
+
+ /// @brief Create a subnet with a specified pool of addresses.
+ ///
+ /// @param pool_start First address in the pool.
+ /// @param pool_end Last address in the pool.
+ void initSubnet(const asiolink::IOAddress& pool_start,
+ const asiolink::IOAddress& pool_end);
+
+ virtual ~AllocEngine4Test() {
+ factory_.destroy();
+ }
+
+ ClientIdPtr clientid_; ///< Client-identifier (value used in tests)
+ ClientIdPtr clientid2_; ///< Alternative client-identifier.
+ HWAddrPtr hwaddr_; ///< Hardware address (value used in tests)
+ HWAddrPtr hwaddr2_; ///< Alternative hardware address.
+ Subnet4Ptr subnet_; ///< Subnet4 (used in tests)
+ Pool4Ptr pool_; ///< Pool belonging to subnet_
+ LeaseMgrFactory factory_; ///< Pointer to LeaseMgr factory
+ AllocEngine::ClientContext4 ctx_; ///< Context information passed to various
+ ClientClasses cc_; ///< Client classes
+ ///< allocation engine functions.
+};
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
+
+#endif
diff --git a/src/lib/dhcpsrv/tests/callout_handle_store_unittest.cc b/src/lib/dhcpsrv/tests/callout_handle_store_unittest.cc
new file mode 100644
index 0000000..380ed58
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/callout_handle_store_unittest.cc
@@ -0,0 +1,74 @@
+// Copyright (C) 2012-2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/pkt4.h>
+#include <dhcpsrv/callout_handle_store.h>
+#include "test_get_callout_handle.h"
+
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::hooks;
+
+namespace {
+
+TEST(CalloutHandleStoreTest, StoreRetrieve) {
+
+ // Create two DHCP4 packets during tests. The constructor arguments are
+ // arbitrary.
+ Pkt4Ptr pktptr_1(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr pktptr_2(new Pkt4(DHCPDISCOVER, 5678));
+
+ // Check that the pointers point to objects that are different, and that
+ // the pointers are the only pointers pointing to the packets.
+ ASSERT_TRUE(pktptr_1);
+ ASSERT_TRUE(pktptr_2);
+
+ ASSERT_TRUE(pktptr_1 != pktptr_2);
+
+ // Get the CalloutHandle for the first packet.
+ CalloutHandlePtr chptr_1 = getCalloutHandle(pktptr_1);
+ ASSERT_TRUE(chptr_1);
+
+ // Try getting another pointer for the same packet. Use a different
+ // pointer object to check that the function returns a handle based on the
+ // pointed-to data and not the pointer.
+ Pkt4Ptr pktptr_temp = pktptr_1;
+ CalloutHandlePtr chptr_2 = getCalloutHandle(pktptr_temp);
+ pktptr_temp.reset();
+
+ ASSERT_TRUE(chptr_2);
+ EXPECT_TRUE(chptr_1 == chptr_2);
+
+ // Now ask for a CalloutHandle for a different object. This should return
+ // a different CalloutHandle.
+ chptr_2 = getCalloutHandle(pktptr_2);
+ EXPECT_FALSE(chptr_1 == chptr_2);
+}
+
+// The followings is a trivial test to check that if the template function
+// is referred to in a separate compilation unit, only one copy of the static
+// objects stored in it are returned. (For a change, we'll use a Pkt6 as the
+// packet object.)
+
+TEST(CalloutHandleStoreTest, SeparateCompilationUnit) {
+
+ // Access the template function here.
+ Pkt6Ptr pktptr_1(new Pkt6(DHCPV6_ADVERTISE, 4321));
+ CalloutHandlePtr chptr_1 = getCalloutHandle(pktptr_1);
+ ASSERT_TRUE(chptr_1);
+
+ // Access it from within another compilation unit.
+ CalloutHandlePtr chptr_2 = isc::dhcp::test::testGetCalloutHandle(pktptr_1);
+ EXPECT_TRUE(chptr_1 == chptr_2);
+}
+
+} // Anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/callout_library.cc b/src/lib/dhcpsrv/tests/callout_library.cc
new file mode 100644
index 0000000..bfc2f9e
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/callout_library.cc
@@ -0,0 +1,25 @@
+// Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+/// @file
+/// @brief Callout Library
+///
+/// This is the source of a test library for the basic DHCP parser
+/// tests. It just has to load - nothing else.
+
+#include <config.h>
+
+#include <hooks/hooks.h>
+
+extern "C" {
+
+// Framework functions
+int
+version() {
+ return (KEA_HOOKS_VERSION);
+}
+
+};
diff --git a/src/lib/dhcpsrv/tests/callout_params_library.cc b/src/lib/dhcpsrv/tests/callout_params_library.cc
new file mode 100644
index 0000000..4e19ed1
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/callout_params_library.cc
@@ -0,0 +1,25 @@
+// Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+/// @file
+/// @brief Callout Library
+///
+/// This is the source of a test library for the DHCP parser tests that
+/// specify parameters. It will attempt to obtain its own parameters.
+
+#include <config.h>
+
+#include <hooks/hooks.h>
+
+extern "C" {
+
+// Framework functions
+int
+version() {
+ return (KEA_HOOKS_VERSION);
+}
+
+};
diff --git a/src/lib/dhcpsrv/tests/cb_ctl_dhcp_unittest.cc b/src/lib/dhcpsrv/tests/cb_ctl_dhcp_unittest.cc
new file mode 100644
index 0000000..43a2f79
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cb_ctl_dhcp_unittest.cc
@@ -0,0 +1,1884 @@
+// Copyright (C) 2019-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <cc/stamped_value.h>
+#include <dhcp/option_string.h>
+#include <dhcpsrv/cb_ctl_dhcp4.h>
+#include <dhcpsrv/cb_ctl_dhcp6.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/client_class_def.h>
+#include <dhcpsrv/host_mgr.h>
+#include <dhcpsrv/testutils/memory_host_data_source.h>
+#include <dhcpsrv/testutils/generic_backend_unittest.h>
+#include <dhcpsrv/testutils/test_config_backend_dhcp4.h>
+#include <dhcpsrv/testutils/test_config_backend_dhcp6.h>
+#include <hooks/server_hooks.h>
+#include <hooks/callout_manager.h>
+#include <hooks/hooks_manager.h>
+#include <testutils/gtest_utils.h>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <boost/make_shared.hpp>
+#include <gtest/gtest.h>
+#include <iostream>
+#include <map>
+#include <string>
+
+using namespace isc::asiolink;
+using namespace isc::db;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::process;
+using namespace isc::hooks;
+
+namespace {
+
+/// @brief Derivation of the @c MemHostDataSource which always returns
+/// @c false when setting IP reservations unique/non-unique mode.
+class NonUniqueHostDataSource : public MemHostDataSource {
+public:
+
+ /// @brief Virtual destructor.
+ virtual ~NonUniqueHostDataSource() {}
+
+ /// @brief Configure unique/non-unique IP reservations.
+ ///
+ /// @return Always false.
+ virtual bool setIPReservationsUnique(const bool) {
+ return (false);
+ }
+};
+
+/// @brief Pointer to the @c NonUniqueHostDataSource instance.
+typedef boost::shared_ptr<NonUniqueHostDataSource> NonUniqueHostDataSourcePtr;
+
+/// @brief Base class for testing derivations of the CBControlDHCP.
+class CBControlDHCPTest : public GenericBackendTest {
+public:
+
+ /// @brief Constructor.
+ CBControlDHCPTest()
+ : timestamp_(), object_timestamp_(), audit_entries_(),
+ modification_id_(2345) {
+ CfgMgr::instance().clear();
+ initTimestamps();
+ callback_name_ = std::string("");
+ callback_audit_entries_.reset();
+ HostMgr::create();
+ }
+
+ /// @brief Destructor.
+ virtual ~CBControlDHCPTest() {
+ // Unregister the factory to be tidy.
+ ConfigBackendDHCPv4Mgr::instance().unregisterBackendFactory("memfile");
+ CfgMgr::instance().clear();
+ // Unregister hooks.
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("cb4_updated");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("cb6_updated");
+ bool status = HooksManager::unloadLibraries();
+ if (!status) {
+ std::cerr << "(fixture dtor) unloadLibraries failed" << std::endl;
+ }
+ HostDataSourceFactory::deregisterFactory("test");
+ }
+
+ /// @brief Creates new CREATE audit entry.
+ ///
+ /// The audit entry is added to the @c audit_entries_ collection.
+ ///
+ /// @param object_type Object type to be associated with the audit
+ /// entry.
+ /// @param object_id Identifier of the object to be associated with
+ /// the audit entry.
+ void addCreateAuditEntry(const std::string& object_type,
+ const uint64_t object_id) {
+ AuditEntryPtr entry(new AuditEntry(object_type, object_id,
+ AuditEntry::ModificationType::CREATE,
+ ++modification_id_,
+ "some log message"));
+ audit_entries_.insert(entry);
+ }
+
+ /// @brief Creates new DELETE audit entry.
+ ///
+ /// The audit entry is added to the @c audit_entries_ collection.
+ ///
+ /// @param object_type Object type to be associated with the audit
+ /// entry.
+ /// @param object_id Identifier of the object to be associated with
+ /// the audit entry.
+ void addDeleteAuditEntry(const std::string& object_type,
+ const uint64_t object_id) {
+ AuditEntryPtr entry(new AuditEntry(object_type, object_id,
+ AuditEntry::ModificationType::DELETE,
+ ++modification_id_,
+ "some log message"));
+ audit_entries_.insert(entry);
+ }
+
+ /// @brief Initializes timestamps used in tests.
+ void initTimestamps() {
+ // Get the current timestamp and move it 30 seconds backwards.
+ auto now = boost::posix_time::second_clock::local_time() -
+ boost::posix_time::seconds(30);
+
+ // Initialize multiple timestamps from the base timestamp. The
+ // values with indexes [-5, 0] are in the past. The remaining
+ // four are in the future.
+ for (int i = -5; i < 5; ++i) {
+ timestamp_[i] = now + boost::posix_time::minutes(i);
+ }
+ }
+
+ /// @brief Returns timestamp associated with a given index.
+ ///
+ /// @param timestamp_index Index of the timestamp to be returned.
+ boost::posix_time::ptime getTimestamp(const int timestamp_index) {
+ return (timestamp_[timestamp_index]);
+ }
+
+ /// @brief Returns timestamp to be associated with a given object type.
+ ///
+ /// The object types correspond to the names of the SQL tables holding
+ /// them, e.g. dhcp4_global_parameter, dhcp4_subnet etc.
+ ///
+ /// @param object_type Object type.
+ boost::posix_time::ptime getTimestamp(const std::string& object_type) {
+ return (object_timestamp_[object_type]);
+ }
+
+ /// @brief Associates object type with a timestamp.
+ ///
+ /// When adding objects to the database, each one is associated with
+ /// a modification time value. This value is setup by unit tests
+ /// via this method.
+ void setTimestamp(const std::string& object_type, const int timestamp_index) {
+ object_timestamp_[object_type] = timestamp_[timestamp_index];
+ }
+
+ /// @brief Sets timestamps for various object types to the same value.
+ ///
+ /// @param timestamp_index Index of the timestamp to be set.
+ virtual void setAllTimestamps(const int timestamp_index) = 0;
+
+ /// @brief Checks if @c databaseConfigApply should fetch updates for specified
+ /// object types.
+ ///
+ /// @param object_type Object type.
+ bool hasConfigElement(const std::string& object_type) const {
+ if (!audit_entries_.empty()) {
+ const auto& index = audit_entries_.get<AuditEntryObjectTypeTag>();
+ auto range = index.equal_range(object_type);
+ for (auto it = range.first; it != range.second; ++it) {
+ if (((*it)->getModificationType() != AuditEntry::ModificationType::DELETE)) {
+ return (true);
+ }
+ }
+ return (false);
+ }
+
+ return (true);
+ }
+
+ /// @brief Check if @c databaseConfigApply should delete a given object from the
+ /// local configuration.
+ ///
+ /// @param object_type Object type.
+ /// @param object_id Object identifier.
+ bool deleteConfigElement(const std::string& object_type,
+ const uint64_t object_id) const {
+ if (!audit_entries_.empty()) {
+ const auto& index = audit_entries_.get<AuditEntryObjectTypeTag>();
+ auto range = index.equal_range(boost::make_tuple(object_type,
+ AuditEntry::ModificationType::DELETE));
+ for (auto it = range.first; it != range.second; ++it) {
+ if ((*it)->getObjectId() == object_id) {
+ return (true);
+ }
+ }
+ }
+ return (false);
+ }
+
+ /// @brief Callback that stores received callout name and received value.
+ ///
+ /// @param callout_handle Callout handle.
+ static int
+ cb4_updated_callout(CalloutHandle& callout_handle) {
+ callback_name_ = std::string("cb4_updated");
+ callout_handle.getArgument("audit_entries", callback_audit_entries_);
+ return (0);
+ }
+
+ /// @brief Callback that stores received callout name and received value.
+ ///
+ /// @param callout_handle Callout handle.
+ static int
+ cb6_updated_callout(CalloutHandle& callout_handle) {
+ callback_name_ = std::string("cb6_updated");
+ callout_handle.getArgument("audit_entries", callback_audit_entries_);
+ return (0);
+ }
+
+ /// @brief Holds test timestamps.
+ std::map<int, boost::posix_time::ptime> timestamp_;
+
+ /// @brief Holds mapping of the objects types to their timestamps.
+ std::map<std::string, boost::posix_time::ptime> object_timestamp_;
+
+ /// @brief Collection of audit entries used in the unit tests.
+ AuditEntryCollection audit_entries_;
+
+ /// @brief Modification id counter.
+ uint64_t modification_id_;
+
+ /// @brief Callback name.
+ static std::string callback_name_;
+
+ /// @brief Callback value.
+ static AuditEntryCollectionPtr callback_audit_entries_;
+};
+
+std::string CBControlDHCPTest::callback_name_;
+AuditEntryCollectionPtr CBControlDHCPTest::callback_audit_entries_;
+
+// ************************ V4 tests *********************
+
+/// @brief Naked @c CBControlDHCPv4 class exposing protected methods.
+class TestCBControlDHCPv4 : public CBControlDHCPv4 {
+public:
+ /// @brief Constructor.
+ TestCBControlDHCPv4() {
+ CfgMgr::instance().setFamily(AF_INET);
+ }
+
+ using CBControlDHCPv4::getInitialAuditRevisionTime;
+ using CBControlDHCPv4::databaseConfigApply;
+};
+
+/// @brief Test fixture class for @c CBControlDHCPv4 unit tests.
+class CBControlDHCPv4Test : public CBControlDHCPTest {
+public:
+
+ /// @brief Constructor.
+ CBControlDHCPv4Test()
+ : CBControlDHCPTest(), ctl_() {
+ ConfigBackendDHCPv4Mgr::instance().registerBackendFactory("memfile",
+ [](const DatabaseConnection::ParameterMap& params)
+ -> ConfigBackendDHCPv4Ptr {
+ return (TestConfigBackendDHCPv4Ptr(new TestConfigBackendDHCPv4(params)));
+ });
+ ConfigBackendDHCPv4Mgr::instance().addBackend("type=memfile");
+
+ // By default, set timestamps for all object types to -4. That leaves
+ // us with the possibility to use index -5 (earlier) to use as lower
+ // bound modification time so as all objects are fetched.
+ setAllTimestamps(-4);
+ }
+
+ /// @brief Sets timestamps of all DHCPv4 specific object types.
+ ///
+ /// @param timestamp_index Index of the timestamp to be set.
+ virtual void setAllTimestamps(const int timestamp_index) {
+ setTimestamp("dhcp4_global_parameter", timestamp_index);
+ setTimestamp("dhcp4_option_def", timestamp_index);
+ setTimestamp("dhcp4_options", timestamp_index);
+ setTimestamp("dhcp4_shared_network", timestamp_index);
+ setTimestamp("dhcp4_subnet", timestamp_index);
+ setTimestamp("dhcp4_client_class", timestamp_index);
+ }
+
+ /// @brief Creates test server configuration and stores it in a test
+ /// configuration backend.
+ ///
+ /// There are pairs of configuration elements stored in the database.
+ /// For example: two global parameters, two option definitions etc.
+ /// Having two elements of each type in the database is useful in tests
+ /// which verify that an element is deleted from the local configuration
+ /// as a result of being deleted from the configuration backend. In that
+ /// case the test verifies that one of the elements of the given type
+ /// is deleted and one is left.
+ void remoteStoreTestConfiguration() {
+ auto& mgr = ConfigBackendDHCPv4Mgr::instance();
+
+ // Insert global parameters into a database.
+ StampedValuePtr global_parameter = StampedValue::create("comment", "bar");
+ global_parameter->setModificationTime(getTimestamp("dhcp4_global_parameter"));
+ ASSERT_NO_THROW(mgr.getPool()->createUpdateGlobalParameter4(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ global_parameter));
+
+ global_parameter = StampedValue::create("next-server", "teta");
+ global_parameter->setModificationTime(getTimestamp("dhcp4_global_parameter"));
+ ASSERT_NO_THROW(mgr.getPool()->createUpdateGlobalParameter4(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ global_parameter));
+
+ // Insert option definitions into the database.
+ OptionDefinitionPtr def(new OptionDefinition("one", 101, "isc", "uint16"));
+ def->setId(1);
+ def->setModificationTime(getTimestamp("dhcp4_option_def"));
+ ASSERT_NO_THROW(mgr.getPool()->createUpdateOptionDef4(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ def));
+ def.reset(new OptionDefinition("two", 102, "isc", "uint16"));
+ def->setId(2);
+ def->setModificationTime(getTimestamp("dhcp4_option_def"));
+ ASSERT_NO_THROW(mgr.getPool()->createUpdateOptionDef4(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ def));
+
+ // Insert global options into the database.
+ OptionDescriptorPtr opt(new OptionDescriptor(createOption<OptionString>
+ (Option::V4, DHO_HOST_NAME,
+ true, false, "new.example.com")));
+ opt->setId(1);
+ opt->space_name_ = DHCP4_OPTION_SPACE;
+ opt->setModificationTime(getTimestamp("dhcp4_options"));
+ mgr.getPool()->createUpdateOption4(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ opt);
+
+ opt.reset(new OptionDescriptor(createOption<OptionString>
+ (Option::V4, DHO_TFTP_SERVER_NAME,
+ true, false, "tftp-my")));
+ opt->setId(2);
+ opt->space_name_ = DHCP4_OPTION_SPACE;
+ opt->setModificationTime(getTimestamp("dhcp4_options"));
+ mgr.getPool()->createUpdateOption4(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ opt);
+
+ // Insert shared networks into the database.
+ SharedNetwork4Ptr network(new SharedNetwork4("one"));
+ network->setId(1);
+ network->setModificationTime(getTimestamp("dhcp4_shared_network"));
+ mgr.getPool()->createUpdateSharedNetwork4(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ network);
+
+ network.reset(new SharedNetwork4("two"));
+ network->setId(2);
+ network->setModificationTime(getTimestamp("dhcp4_shared_network"));
+ mgr.getPool()->createUpdateSharedNetwork4(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ network);
+
+ // Insert subnets into the database.
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.3.0"), 26, 1, 2, 3, SubnetID(1)));
+ subnet->setModificationTime(getTimestamp("dhcp4_subnet"));
+ subnet->setSharedNetworkName("one");
+ mgr.getPool()->createUpdateSubnet4(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ subnet);
+
+ subnet.reset(new Subnet4(IOAddress("192.0.4.0"), 26, 1, 2, 3, SubnetID(2)));
+ subnet->setModificationTime(getTimestamp("dhcp4_subnet"));
+ mgr.getPool()->createUpdateSubnet4(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ subnet);
+
+ // Insert client classes into the database.
+ auto expression = boost::make_shared<Expression>();
+ ClientClassDefPtr client_class = boost::make_shared<ClientClassDef>("first-class", expression);
+ client_class->setTest("substring(option[1].hex, 0, 8) == 'my-value'");
+ client_class->setId(1);
+ client_class->setModificationTime(getTimestamp("dhcp4_client_class"));
+
+ // Add a standard option to the class.
+ OptionPtr option = Option::create(Option::V4, DHO_BOOT_FILE_NAME);
+ OptionDescriptorPtr desc = OptionDescriptor::create(option, true, "bogus-file.txt");
+ desc->space_name_ = DHCP4_OPTION_SPACE;
+ desc->setModificationTime(getTimestamp("dhcp4_client_class"));
+ client_class->getCfgOption()->add(*desc, desc->space_name_);
+
+ // Add a custom option definition to the class.
+ CfgOptionDefPtr cc_cfg_option_def(new CfgOptionDef());
+ def.reset(new OptionDefinition("v4str", 201, "isc", "string"));
+ def->setId(201);
+ def->setModificationTime(getTimestamp("dhcp4_client_class"));
+ cc_cfg_option_def->add(def);
+ client_class->setCfgOptionDef(cc_cfg_option_def);
+
+ // Add a custom option to the class.
+ option = Option::create(Option::V4, 201);
+ desc = OptionDescriptor::create(option, true, "custom-stuff");
+ desc->space_name_ = "isc";
+ desc->setModificationTime(getTimestamp("dhcp4_client_class"));
+ client_class->getCfgOption()->add(*desc, desc->space_name_);
+
+ mgr.getPool()->createUpdateClientClass4(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ client_class, "");
+
+ client_class = boost::make_shared<ClientClassDef>("second-class", expression);
+ client_class->setId(2);
+ client_class->setModificationTime(getTimestamp("dhcp4_client_class"));
+ mgr.getPool()->createUpdateClientClass4(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ client_class, "");
+ }
+
+ /// @brief Deletes specified global parameter from the configuration
+ /// backend and generates audit entry.
+ ///
+ /// Note that the current Kea implementation does not track database
+ /// identifiers of the global parameters. Therefore, the identifier to
+ /// be used to create the audit entry for the deleted parameter must
+ /// be explicitly specified.
+ ///
+ /// @param parameter_name Parameter name.
+ /// @param id Parameter id.
+ void remoteDeleteGlobalParameter(const std::string& parameter_name,
+ const uint64_t id) {
+ auto& mgr = ConfigBackendDHCPv4Mgr::instance();
+ mgr.getPool()->deleteGlobalParameter4(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ parameter_name);
+ addDeleteAuditEntry("dhcp4_global_parameter", id);
+ }
+
+ /// @brief Deletes specified option definition from the configuration
+ /// backend and generates audit entry.
+ ///
+ /// @param code Option code.
+ /// @param space Option space.
+ void remoteDeleteOptionDef(const uint16_t code, const std::string& space) {
+ auto& mgr = ConfigBackendDHCPv4Mgr::instance();
+
+ auto option_def = mgr.getPool()->getOptionDef4(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ code, space);
+
+ if (option_def) {
+ mgr.getPool()->deleteOptionDef4(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ code, space);
+ addDeleteAuditEntry("dhcp4_option_def", option_def->getId());
+ }
+ }
+
+ /// @brief Deletes specified global option from the configuration backend
+ /// and generates audit entry.
+ ///
+ /// @param code Option code.
+ /// @param space Option space.
+ void remoteDeleteOption(const uint16_t code, const std::string& space) {
+ auto& mgr = ConfigBackendDHCPv4Mgr::instance();
+
+ auto option = mgr.getPool()->getOption4(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ code, space);
+
+ if (option) {
+ mgr.getPool()->deleteOptionDef4(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ code, space);
+ addDeleteAuditEntry("dhcp4_option_def", option->getId());
+ }
+ }
+
+ /// @brief Deletes specified shared network from the configuration backend
+ /// and generates audit entry.
+ ///
+ /// @param name Name of the shared network to be deleted.
+ void remoteDeleteSharedNetwork(const std::string& name) {
+ auto& mgr = ConfigBackendDHCPv4Mgr::instance();
+
+ auto network = mgr.getPool()->getSharedNetwork4(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ name);
+
+ if (network) {
+ mgr.getPool()->deleteSharedNetwork4(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ name);
+ addDeleteAuditEntry("dhcp4_shared_network", network->getId());
+ }
+ }
+
+ /// @brief Deletes specified subnet from the configuration backend and
+ /// generates audit entry.
+ void remoteDeleteSubnet(const SubnetID& id) {
+ auto& mgr = ConfigBackendDHCPv4Mgr::instance();
+
+ mgr.getPool()->deleteSubnet4(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ id);
+ addDeleteAuditEntry("dhcp4_subnet", id);
+ }
+
+ /// @brief Deletes specified client class from the configuration backend
+ /// and generates audit entry.
+ ///
+ /// @param name Name of the client class to be deleted.
+ void remoteDeleteClientClass(const std::string& name) {
+ auto& mgr = ConfigBackendDHCPv4Mgr::instance();
+
+ auto client_class = mgr.getPool()->getClientClass4(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ name);
+
+ if (client_class) {
+ mgr.getPool()->deleteClientClass4(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ name);
+ addDeleteAuditEntry("dhcp4_client_class", client_class->getId());
+ }
+ }
+
+ /// @brief Tests the @c CBControlDHCPv4::databaseConfigApply method.
+ ///
+ /// This test inserts configuration elements of each type into the
+ /// configuration database. Next, it calls the @c databaseConfigApply,
+ /// which should merge each object from the database for which the
+ /// CREATE or UPDATE audit entry is found. The test then verifies
+ /// if the appropriate entries have been merged.
+ ///
+ /// @param lb_modification_time Lower bound modification time to be
+ /// passed to the @c databaseConfigApply.
+ void testDatabaseConfigApply(const boost::posix_time::ptime& lb_modification_time) {
+ remoteStoreTestConfiguration();
+
+ ASSERT_FALSE(audit_entries_.empty())
+ << "Require at least one audit entry. The test is broken!";
+
+ ASSERT_NO_THROW_LOG(ctl_.databaseConfigApply(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ lb_modification_time, audit_entries_));
+
+ // The updates should have been merged into current configuration.
+ auto srv_cfg = CfgMgr::instance().getCurrentCfg();
+
+ // If there is an audit entry for global parameter and the parameter
+ // modification time is later than last audit revision time it should
+ // be merged.
+ if (hasConfigElement("dhcp4_global_parameter") &&
+ (getTimestamp("dhcp4_global_parameter") > lb_modification_time)) {
+ checkConfiguredGlobal(srv_cfg, "comment", Element::create("bar"));
+
+ } else {
+ // Otherwise it shouldn't exist.
+ EXPECT_FALSE(srv_cfg->getConfiguredGlobals()->get("comment"));
+ }
+
+ // If there is an audit entry for option definition and the definition
+ // modification time is later than last audit revision time it should
+ // be merged.
+ auto found_def = srv_cfg->getCfgOptionDef()->get("isc", "one");
+ if (hasConfigElement("dhcp4_option_def") &&
+ getTimestamp("dhcp4_option_def") > lb_modification_time) {
+ ASSERT_TRUE(found_def);
+ EXPECT_EQ(101, found_def->getCode());
+ EXPECT_EQ(OptionDataType::OPT_UINT16_TYPE, found_def->getType());
+
+ } else {
+ EXPECT_FALSE(found_def);
+ }
+
+ // If there is an audit entry for an option and the option
+ // modification time is later than last audit revision time it should
+ // be merged.
+ auto options = srv_cfg->getCfgOption();
+ auto found_opt = options->get(DHCP4_OPTION_SPACE, DHO_HOST_NAME);
+ if (hasConfigElement("dhcp4_options") &&
+ (getTimestamp("dhcp4_options") > lb_modification_time)) {
+ ASSERT_TRUE(found_opt.option_);
+ EXPECT_EQ("new.example.com", found_opt.option_->toString());
+
+ } else {
+ EXPECT_FALSE(found_opt.option_);
+ }
+
+ // If there is an audit entry for a shared network and the network
+ // modification time is later than last audit revision time it should
+ // be merged.
+ auto networks = srv_cfg->getCfgSharedNetworks4();
+ auto found_network = networks->getByName("one");
+ if (hasConfigElement("dhcp4_shared_network") &&
+ (getTimestamp("dhcp4_shared_network") > lb_modification_time)) {
+ ASSERT_TRUE(found_network);
+ EXPECT_TRUE(found_network->hasFetchGlobalsFn());
+
+ } else {
+ EXPECT_FALSE(found_network);
+ }
+
+ // If there is an audit entry for a subnet and the subnet modification
+ // time is later than last audit revision time it should be merged.
+ auto subnets = srv_cfg->getCfgSubnets4();
+ auto found_subnet = subnets->getBySubnetId(1);
+ if (hasConfigElement("dhcp4_subnet") &&
+ (getTimestamp("dhcp4_subnet") > lb_modification_time)) {
+ ASSERT_TRUE(found_subnet);
+ EXPECT_TRUE(found_subnet->hasFetchGlobalsFn());
+
+ } else {
+ EXPECT_FALSE(found_subnet);
+ }
+
+ auto client_classes = srv_cfg->getClientClassDictionary();
+ auto found_class = client_classes->findClass("first-class");
+ if (hasConfigElement("dhcp4_client_class") &&
+ (getTimestamp("dhcp4_client_class") > lb_modification_time)) {
+ ASSERT_TRUE(found_class);
+ ASSERT_TRUE(found_class->getMatchExpr());
+ EXPECT_GT(found_class->getMatchExpr()->size(), 0);
+ EXPECT_EQ("first-class", found_class->getName());
+
+ // Check for the standard class option, make sure it has been "created".
+ auto cfg_option_desc = found_class->getCfgOption();
+ ASSERT_TRUE(cfg_option_desc);
+ auto option_desc = cfg_option_desc->get(DHCP4_OPTION_SPACE, DHO_BOOT_FILE_NAME);
+ auto option = option_desc.option_;
+ ASSERT_TRUE(option);
+ EXPECT_EQ(OptionBuffer(option_desc.formatted_value_.begin(),
+ option_desc.formatted_value_.end()),
+ option->getData());
+
+ // Check for the custom class option, make sure it has been "created".
+ option_desc = cfg_option_desc->get("isc", 201);
+ option = option_desc.option_;
+ ASSERT_TRUE(option);
+ EXPECT_EQ(OptionBuffer(option_desc.formatted_value_.begin(),
+ option_desc.formatted_value_.end()),
+ option->getData());
+ } else {
+ EXPECT_FALSE(found_class);
+ }
+ }
+
+ /// @brief Tests deletion of the configuration elements by the
+ /// @c CBControlDHCPv4::databaseConfigApply method.
+ ///
+ /// This test inserts configuration elements of each type into the
+ /// configuration database and calls the @c databaseConfigApply
+ /// to fetch this configuration and merge into the local server
+ /// configuration.
+ ///
+ /// Next, the test calls the specified callback function, i.e.
+ /// @c db_modifications, which deletes selected configuration
+ /// elements from the database and generates appropriate audit
+ /// entries. Finally, it calls the @c databaseConfigApply again
+ /// to process the audit entries and checks if the appropriate
+ /// configuration elements are deleted from the local configuration
+ ///
+ /// @param lb_modification_time Lower bound modification time to be
+ /// passed to the @c databaseConfigApply.
+ /// @param db_modifications Pointer to the callback function which
+ /// applies test specific modifications into the database.
+ void testDatabaseConfigApplyDelete(const boost::posix_time::ptime& lb_modification_time,
+ std::function<void()> db_modifications) {
+ // Store initial configuration into the database.
+ remoteStoreTestConfiguration();
+
+ // Since we pass an empty audit collection the server treats this
+ // as if the server is starting up and fetches the entire
+ // configuration from the database.
+ ctl_.databaseConfigApply(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ ctl_.getInitialAuditRevisionTime(),
+ AuditEntryCollection());
+ // Commit the configuration so as it is moved from the staging
+ // to current.
+ CfgMgr::instance().commit();
+
+ // Run user defined callback which should delete selected configuration
+ // elements from the configuration backend. The appropriate DELETE
+ // audit entries should now be stored in the audit_entries_ collection.
+ if (db_modifications) {
+ db_modifications();
+ }
+
+ // Process the DELETE audit entries.
+ ctl_.databaseConfigApply(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ lb_modification_time, audit_entries_);
+
+ // All changes should have been applied in the current configuration.
+ auto srv_cfg = CfgMgr::instance().getCurrentCfg();
+
+ {
+ SCOPED_TRACE("global parameters");
+ // One of the global parameters should still be there.
+ EXPECT_TRUE(srv_cfg->getConfiguredGlobals()->get("next-server"));
+ if (deleteConfigElement("dhcp4_global_parameter", 1)) {
+ EXPECT_FALSE(srv_cfg->getConfiguredGlobals()->get("comment"));
+
+ } else {
+ EXPECT_TRUE(srv_cfg->getConfiguredGlobals()->get("next-server"));
+ }
+ }
+
+ {
+ SCOPED_TRACE("option definitions");
+ // One of the option definitions should still be there.
+ EXPECT_TRUE(srv_cfg->getCfgOptionDef()->get("isc", "two"));
+ auto found_def = srv_cfg->getCfgOptionDef()->get("isc", "one");
+ if (deleteConfigElement("dhcp4_option_def", 1)) {
+ EXPECT_FALSE(found_def);
+
+ } else {
+ EXPECT_TRUE(found_def);
+ }
+ }
+
+ {
+ SCOPED_TRACE("global options");
+ // One of the options should still be there.
+ EXPECT_TRUE(srv_cfg->getCfgOption()->get(DHCP4_OPTION_SPACE,
+ DHO_TFTP_SERVER_NAME).option_);
+ auto found_opt = srv_cfg->getCfgOption()->get(DHCP4_OPTION_SPACE,
+ DHO_HOST_NAME);
+ if (deleteConfigElement("dhcp4_options", 1)) {
+ EXPECT_FALSE(found_opt.option_);
+
+ } else {
+ EXPECT_TRUE(found_opt.option_);
+ }
+ }
+
+ {
+ SCOPED_TRACE("shared networks");
+ // One of the shared networks should still be there.
+ EXPECT_TRUE(srv_cfg->getCfgSharedNetworks4()->getByName("two"));
+ auto found_network = srv_cfg->getCfgSharedNetworks4()->getByName("one");
+ if (deleteConfigElement("dhcp4_shared_network", 1)) {
+ EXPECT_FALSE(found_network);
+
+ } else {
+ EXPECT_TRUE(found_network);
+ }
+ }
+
+ {
+ SCOPED_TRACE("subnets");
+ // One of the subnets should still be there.
+ EXPECT_TRUE(srv_cfg->getCfgSubnets4()->getBySubnetId(2));
+ auto found_subnet = srv_cfg->getCfgSubnets4()->getBySubnetId(1);
+ if (deleteConfigElement("dhcp4_subnet", 1)) {
+ EXPECT_FALSE(found_subnet);
+
+ // If the subnet has been deleted, make sure that
+ // it was detached from the shared network it belonged
+ // to, if the shared network still exists.
+ auto found_network = srv_cfg->getCfgSharedNetworks4()->getByName("one");
+ if (found_network) {
+ EXPECT_TRUE(found_network->getAllSubnets()->empty());
+ }
+
+ } else {
+ EXPECT_TRUE(found_subnet);
+ }
+ }
+
+ {
+ SCOPED_TRACE("client classes");
+ // One of the subnets should still be there.
+ EXPECT_TRUE(srv_cfg->getClientClassDictionary()->findClass("second-class"));
+ auto found_client_class = srv_cfg->getClientClassDictionary()->findClass("first-class");
+ if (deleteConfigElement("dhcp4_client_class", 1)) {
+ EXPECT_FALSE(found_client_class);
+
+ } else {
+ EXPECT_TRUE(found_client_class);
+ }
+ }
+ }
+
+ /// @brief Instance of the @c CBControlDHCPv4 used for testing.
+ TestCBControlDHCPv4 ctl_;
+};
+
+
+// This test verifies that the configuration updates for all object
+// types are merged into the current configuration.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyAll) {
+
+ addCreateAuditEntry("dhcp4_global_parameter", 1);
+ addCreateAuditEntry("dhcp4_global_parameter", 2);
+ addCreateAuditEntry("dhcp4_option_def", 1);
+ addCreateAuditEntry("dhcp4_option_def", 2);
+ addCreateAuditEntry("dhcp4_options", 1);
+ addCreateAuditEntry("dhcp4_options", 2);
+ addCreateAuditEntry("dhcp4_shared_network", 1);
+ addCreateAuditEntry("dhcp4_shared_network", 2);
+ addCreateAuditEntry("dhcp4_subnet", 1);
+ addCreateAuditEntry("dhcp4_subnet", 2);
+ addCreateAuditEntry("dhcp4_client_class", 1);
+ addCreateAuditEntry("dhcp4_client_class", 2);
+
+ testDatabaseConfigApply(getTimestamp(-5));
+}
+
+// This test verifies that multiple configuration elements are
+// deleted from the local configuration as a result of being
+// deleted from the database.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyDeleteAll) {
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ remoteDeleteGlobalParameter("comment", 1);
+ remoteDeleteOptionDef(101, "isc");
+ remoteDeleteOption(DHO_HOST_NAME, DHCP4_OPTION_SPACE);
+ remoteDeleteSharedNetwork("one");
+ remoteDeleteSubnet(SubnetID(1));
+ remoteDeleteClientClass("first-class");
+ });
+}
+
+// This test verifies that an attempt to delete non-existing
+// configuration element does not cause an error.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyDeleteNonExisting) {
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ // Add several audit entries instructing to delete the
+ // non-existing configuration elements. The ids are set
+ // to 3, but the only existing elements have ids of 1
+ // and 2.
+ addDeleteAuditEntry("dhcp4_global_parameter", 3);
+ addDeleteAuditEntry("dhcp4_option_def", 3);
+ addDeleteAuditEntry("dhcp4_options", 3);
+ addDeleteAuditEntry("dhcp4_shared_network", 3);
+ addDeleteAuditEntry("dhcp4_subnet", 3);
+ addDeleteAuditEntry("dhcp4_client_class", 3);
+ });
+}
+
+// This test verifies that only a global parameter is merged into
+// the current configuration.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyGlobal) {
+ addCreateAuditEntry("dhcp4_global_parameter", 1);
+ addCreateAuditEntry("dhcp4_global_parameter", 2);
+ testDatabaseConfigApply(getTimestamp(-5));
+}
+
+// This test verifies that the global parameter is deleted from
+// the local configuration as a result of being deleted from the
+// database.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyDeleteGlobal) {
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ remoteDeleteGlobalParameter("comment", 1);
+ });
+}
+
+// This test verifies that global parameter is not fetched from the
+// database when the modification time is earlier than the last
+// fetched audit entry.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyGlobalNotFetched) {
+ addCreateAuditEntry("dhcp4_global_parameter", 1);
+ addCreateAuditEntry("dhcp4_global_parameter", 2);
+ testDatabaseConfigApply(getTimestamp(-3));
+}
+
+// This test verifies that only an option definition is merged into
+// the current configuration.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyOptionDef) {
+ addCreateAuditEntry("dhcp4_option_def", 1);
+ addCreateAuditEntry("dhcp4_option_def", 2);
+ testDatabaseConfigApply(getTimestamp(-5));
+}
+
+// This test verifies that the option definition is deleted from
+// the local configuration as a result of being deleted from the
+// database.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyDeleteOptionDef) {
+ addDeleteAuditEntry("dhcp4_option_def", 1);
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ remoteDeleteOptionDef(101, "isc");
+ });
+}
+
+// This test verifies that option definition is not fetched from the
+// database when the modification time is earlier than the last
+// fetched audit entry.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyOptionDefNotFetched) {
+ addCreateAuditEntry("dhcp4_option_def", 1);
+ addCreateAuditEntry("dhcp4_option_def", 2);
+ testDatabaseConfigApply(getTimestamp(-3));
+}
+
+// This test verifies that only a DHCPv4 option is merged into the
+// current configuration.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyOption) {
+ addCreateAuditEntry("dhcp4_options", 1);
+ addCreateAuditEntry("dhcp4_options", 2);
+ testDatabaseConfigApply(getTimestamp(-5));
+}
+
+// This test verifies that the global option is deleted from
+// the local configuration as a result of being deleted from the
+// database.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyDeleteOption) {
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ remoteDeleteOption(DHO_HOST_NAME, DHCP4_OPTION_SPACE);
+ });
+}
+
+// This test verifies that DHCPv4 option is not fetched from the
+// database when the modification time is earlier than the last
+// fetched audit entry.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyOptionNotFetched) {
+ addCreateAuditEntry("dhcp4_options", 1);
+ addCreateAuditEntry("dhcp4_options", 2);
+ testDatabaseConfigApply(getTimestamp(-3));
+}
+
+// This test verifies that only a shared network is merged into the
+// current configuration.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplySharedNetwork) {
+ addCreateAuditEntry("dhcp4_shared_network", 1);
+ addCreateAuditEntry("dhcp4_shared_network", 2);
+ testDatabaseConfigApply(getTimestamp(-5));
+}
+
+// This test verifies that the shared network is deleted from
+// the local configuration as a result of being deleted from the
+// database.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyDeleteSharedNetwork) {
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ remoteDeleteSharedNetwork("one");
+ });
+}
+
+// This test verifies that shared network is not fetched from the
+// database when the modification time is earlier than the last
+// fetched audit entry.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplySharedNetworkNotFetched) {
+ addCreateAuditEntry("dhcp4_shared_network", 1);
+ addCreateAuditEntry("dhcp4_shared_network", 2);
+ testDatabaseConfigApply(getTimestamp(-3));
+}
+
+// This test verifies that only a subnet is merged into the current
+// configuration.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplySubnet) {
+ addCreateAuditEntry("dhcp4_shared_network", 1);
+ addCreateAuditEntry("dhcp4_shared_network", 2);
+ addCreateAuditEntry("dhcp4_subnet", 1);
+ addCreateAuditEntry("dhcp4_subnet", 2);
+ testDatabaseConfigApply(getTimestamp(-5));
+}
+
+// This test verifies that the subnet is deleted from the local
+// configuration as a result of being deleted from the database.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyDeleteSubnet) {
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ remoteDeleteSubnet(SubnetID(1));
+ });
+}
+
+// This test verifies that subnet is not fetched from the database
+// when the modification time is earlier than the last fetched audit
+// entry.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplySubnetNotFetched) {
+ addCreateAuditEntry("dhcp4_subnet", 1);
+ addCreateAuditEntry("dhcp4_subnet", 2);
+ testDatabaseConfigApply(getTimestamp(-3));
+}
+
+// This test verifies that only client classes are merged into the current
+// configuration.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyClientClasses) {
+ addCreateAuditEntry("dhcp4_client_class", 1);
+ addCreateAuditEntry("dhcp4_client_class", 2);
+ testDatabaseConfigApply(getTimestamp(-5));
+}
+
+// This test verifies that a client class is deleted from the local
+// configuration as a result of being deleted from the database.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyDeleteClientClass) {
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ remoteDeleteClientClass("first-class");
+ });
+}
+
+// This test verifies that the configuration update calls the hook.
+TEST_F(CBControlDHCPv4Test, databaseConfigApplyHook) {
+
+ // Initialize Hooks Manager.
+ HooksManager::loadLibraries(HookLibsCollection());
+
+ // Install cb4_updated.
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "cb4_updated", cb4_updated_callout));
+
+ // Create audit entries.
+ addCreateAuditEntry("dhcp4_global_parameter", 1);
+ addCreateAuditEntry("dhcp4_global_parameter", 2);
+ addCreateAuditEntry("dhcp4_option_def", 1);
+ addCreateAuditEntry("dhcp4_option_def", 2);
+ addCreateAuditEntry("dhcp4_options", 1);
+ addCreateAuditEntry("dhcp4_options", 2);
+ addCreateAuditEntry("dhcp4_shared_network", 1);
+ addCreateAuditEntry("dhcp4_shared_network", 2);
+ addCreateAuditEntry("dhcp4_subnet", 1);
+ addCreateAuditEntry("dhcp4_subnet", 2);
+
+ // Run the test.
+ testDatabaseConfigApply(getTimestamp(-5));
+
+ // Checks the callout.
+ EXPECT_EQ("cb4_updated", callback_name_);
+ ASSERT_TRUE(callback_audit_entries_);
+ EXPECT_TRUE(audit_entries_ == *callback_audit_entries_);
+}
+
+// This test verifies that it is possible to set ip-reservations-unique
+// parameter via configuration backend and that it is successful when
+// host database backend accepts the new setting.
+TEST_F(CBControlDHCPv4Test, ipReservationsNonUniqueAccepted) {
+ // Create host data source which accepts setting non-unique IP
+ // reservations.
+ MemHostDataSourcePtr hds(new MemHostDataSource());
+ auto testFactory = [hds](const DatabaseConnection::ParameterMap&) {
+ return (hds);
+ };
+ HostDataSourceFactory::registerFactory("test", testFactory);
+ HostMgr::addBackend("type=test");
+
+ // Insert ip-reservations-unique value set to false into the database.
+ auto& mgr = ConfigBackendDHCPv4Mgr::instance();
+ StampedValuePtr global_parameter = StampedValue::create("ip-reservations-unique",
+ Element::create(false));
+ ASSERT_NO_THROW(mgr.getPool()->createUpdateGlobalParameter4(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ global_parameter));
+ // Adding audit entry simulates the case when the server is already configured
+ // and we're adding incremental changes. These changes should be applied to
+ // the current configuration.
+ addCreateAuditEntry("dhcp4_global_parameter", 1);
+
+ // Apply the configuration.
+ ASSERT_NO_THROW(ctl_.databaseConfigApply(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ getTimestamp(-5),
+ audit_entries_));
+ // The new setting should be visible in both CfgDbAccess and HostMgr.
+ EXPECT_FALSE(CfgMgr::instance().getCurrentCfg()->getCfgDbAccess()->getIPReservationsUnique());
+ EXPECT_FALSE(HostMgr::instance().getIPReservationsUnique());
+}
+
+// This test verifies that the new setting of ip-reservations-unique is not
+// accepted when one of the host database backends does not support it.
+TEST_F(CBControlDHCPv4Test, ipReservationsNonUniqueRefused) {
+ // Create host data source which does not accept setting IP reservations
+ // non-unique setting.
+ NonUniqueHostDataSourcePtr hds(new NonUniqueHostDataSource());
+ auto testFactory = [hds](const DatabaseConnection::ParameterMap&) {
+ return (hds);
+ };
+ HostDataSourceFactory::registerFactory("test", testFactory);
+ HostMgr::addBackend("type=test");
+
+ // Insert ip-reservations-unique value set to false into the database.
+ auto& mgr = ConfigBackendDHCPv4Mgr::instance();
+ StampedValuePtr global_parameter = StampedValue::create("ip-reservations-unique",
+ Element::create(false));
+ ASSERT_NO_THROW(mgr.getPool()->createUpdateGlobalParameter4(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ global_parameter));
+ // Adding audit entry simulates the case when the server is already configured
+ // and we're adding incremental changes. These changes should be applied to
+ // the current configuration.
+ addCreateAuditEntry("dhcp4_global_parameter", 1);
+
+ // Apply the configuration.
+ ASSERT_NO_THROW(ctl_.databaseConfigApply(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ getTimestamp(-5),
+ audit_entries_));
+ // The default setting should be applied, because the backend refused to
+ // set it to false.
+ EXPECT_TRUE(CfgMgr::instance().getCurrentCfg()->getCfgDbAccess()->getIPReservationsUnique());
+ EXPECT_TRUE(HostMgr::instance().getIPReservationsUnique());
+}
+
+// ************************ V6 tests *********************
+
+/// @brief Naked @c CBControlDHCPv6 class exposing protected methods.
+class TestCBControlDHCPv6 : public CBControlDHCPv6 {
+public:
+ /// @brief Constructor.
+ TestCBControlDHCPv6() {
+ CfgMgr::instance().setFamily(AF_INET6);
+ }
+
+ using CBControlDHCPv6::getInitialAuditRevisionTime;
+ using CBControlDHCPv6::databaseConfigApply;
+};
+
+/// @brief Test fixture class for @c CBControlDHCPv6 unit tests.
+class CBControlDHCPv6Test : public CBControlDHCPTest {
+public:
+
+ /// @brief Constructor.
+ CBControlDHCPv6Test()
+ : CBControlDHCPTest(), ctl_() {
+ ConfigBackendDHCPv6Mgr::instance().registerBackendFactory("memfile",
+ [](const DatabaseConnection::ParameterMap& params)
+ -> ConfigBackendDHCPv6Ptr {
+ return (TestConfigBackendDHCPv6Ptr(new TestConfigBackendDHCPv6(params)));
+ });
+ ConfigBackendDHCPv6Mgr::instance().addBackend("type=memfile");
+
+ // By default, set timestamps for all object types to -4. That leaves
+ // us with the possibility to use index -5 (earlier) to use as lower
+ // bound modification time so as all objects are fetched.
+ setAllTimestamps(-4);
+ }
+
+ /// @brief Sets timestamps of all DHCPv6 specific object types.
+ ///
+ /// @param timestamp_index Index of the timestamp to be set.
+ virtual void setAllTimestamps(const int timestamp_index) {
+ setTimestamp("dhcp6_global_parameter", timestamp_index);
+ setTimestamp("dhcp6_option_def", timestamp_index);
+ setTimestamp("dhcp6_options", timestamp_index);
+ setTimestamp("dhcp6_shared_network", timestamp_index);
+ setTimestamp("dhcp6_subnet", timestamp_index);
+ setTimestamp("dhcp6_client_class", timestamp_index);
+ }
+
+ /// @brief Creates test server configuration and stores it in a test
+ /// configuration backend.
+ ///
+ /// There are pairs of configuration elements stored in the database.
+ /// For example: two global parameters, two option definitions etc.
+ /// Having two elements of each type in the database is useful in tests
+ /// which verify that an element is deleted from the local configuration
+ /// as a result of being deleted from the configuration backend. In that
+ /// case the test verifies that one of the elements of the given type
+ /// is deleted and one is left.
+ void remoteStoreTestConfiguration() {
+ auto& mgr = ConfigBackendDHCPv6Mgr::instance();
+
+ // Insert global parameters into a database.
+ StampedValuePtr global_parameter = StampedValue::create("comment", "bar");
+ global_parameter->setModificationTime(getTimestamp("dhcp6_global_parameter"));
+ ASSERT_NO_THROW(mgr.getPool()->createUpdateGlobalParameter6(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ global_parameter));
+
+ global_parameter = StampedValue::create("data-directory", "teta");
+ global_parameter->setModificationTime(getTimestamp("dhcp6_global_parameter"));
+ ASSERT_NO_THROW(mgr.getPool()->createUpdateGlobalParameter6(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ global_parameter));
+
+ // Insert option definitions into the database.
+ OptionDefinitionPtr def(new OptionDefinition("one", 101, "isc", "uint16"));
+ def->setId(1);
+ def->setModificationTime(getTimestamp("dhcp6_option_def"));
+ ASSERT_NO_THROW(mgr.getPool()->createUpdateOptionDef6(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ def));
+ def.reset(new OptionDefinition("two", 102, "isc", "uint16"));
+ def->setId(2);
+ def->setModificationTime(getTimestamp("dhcp6_option_def"));
+ ASSERT_NO_THROW(mgr.getPool()->createUpdateOptionDef6(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ def));
+
+ // Insert global options into the database.
+ OptionDescriptorPtr opt(new OptionDescriptor(createOption<OptionString>
+ (Option::V6, D6O_BOOTFILE_URL,
+ true, false, "some.bootfile")));
+ opt->setId(1);
+ opt->space_name_ = DHCP6_OPTION_SPACE;
+ opt->setModificationTime(getTimestamp("dhcp6_options"));
+ mgr.getPool()->createUpdateOption6(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ opt);
+
+ opt.reset(new OptionDescriptor(createOption<OptionString>
+ (Option::V6, D6O_AFTR_NAME,
+ true, true, "some.example.com")));
+ opt->setId(2);
+ opt->space_name_ = DHCP6_OPTION_SPACE;
+ opt->setModificationTime(getTimestamp("dhcp6_options"));
+ mgr.getPool()->createUpdateOption6(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ opt);
+
+ // Insert shared networks into the database.
+ SharedNetwork6Ptr network(new SharedNetwork6("one"));
+ network->setId(1);
+ network->setModificationTime(getTimestamp("dhcp6_shared_network"));
+ mgr.getPool()->createUpdateSharedNetwork6(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ network);
+
+ network.reset(new SharedNetwork6("two"));
+ network->setId(2);
+ network->setModificationTime(getTimestamp("dhcp6_shared_network"));
+ mgr.getPool()->createUpdateSharedNetwork6(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ network);
+
+ // Insert subnets into the database.
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 64, 1, 2, 3, 4, SubnetID(1)));
+ subnet->setModificationTime(getTimestamp("dhcp6_subnet"));
+ subnet->setSharedNetworkName("one");
+ mgr.getPool()->createUpdateSubnet6(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ subnet);
+
+ subnet.reset(new Subnet6(IOAddress("2001:db8:2::"), 64, 1, 2, 3, 4, SubnetID(2)));
+ subnet->setModificationTime(getTimestamp("dhcp6_subnet"));
+ mgr.getPool()->createUpdateSubnet6(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ subnet);
+
+ // Insert client classes into the database.
+ auto expression = boost::make_shared<Expression>();
+ ClientClassDefPtr client_class = boost::make_shared<ClientClassDef>("first-class", expression);
+ client_class->setTest("substring(option[1].hex, 0, 8) == 'my-value'");
+ client_class->setId(1);
+ client_class->setModificationTime(getTimestamp("dhcp6_client_class"));
+
+ // Add an option to the class.
+ OptionPtr option = Option::create(Option::V6, D6O_BOOTFILE_URL);
+ OptionDescriptorPtr desc = OptionDescriptor::create(option, true, "client.boot.url");
+ desc->space_name_ = DHCP6_OPTION_SPACE;
+ desc->setModificationTime(getTimestamp("dhcp6_client_class"));
+ client_class->getCfgOption()->add(*desc, desc->space_name_);
+
+ mgr.getPool()->createUpdateClientClass6(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ client_class, "");
+
+ client_class = boost::make_shared<ClientClassDef>("second-class", expression);
+ client_class->setId(2);
+ client_class->setModificationTime(getTimestamp("dhcp6_client_class"));
+ mgr.getPool()->createUpdateClientClass6(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ client_class, "");
+ }
+
+ /// @brief Deletes specified global parameter from the configuration
+ /// backend and generates audit entry.
+ ///
+ /// Note that the current Kea implementation does not track database
+ /// identifiers of the global parameters. Therefore, the identifier to
+ /// be used to create the audit entry for the deleted parameter must
+ /// be explicitly specified.
+ ///
+ /// @param parameter_name Parameter name.
+ /// @param id Parameter id.
+ void remoteDeleteGlobalParameter(const std::string& parameter_name,
+ const uint64_t id) {
+ auto& mgr = ConfigBackendDHCPv6Mgr::instance();
+ mgr.getPool()->deleteGlobalParameter6(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ parameter_name);
+ addDeleteAuditEntry("dhcp6_global_parameter", id);
+ }
+
+ /// @brief Deletes specified option definition from the configuration
+ /// backend and generates audit entry.
+ ///
+ /// @param code Option code.
+ /// @param space Option space.
+ void remoteDeleteOptionDef(const uint16_t code, const std::string& space) {
+ auto& mgr = ConfigBackendDHCPv6Mgr::instance();
+
+ auto option_def = mgr.getPool()->getOptionDef6(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ code, space);
+
+ if (option_def) {
+ mgr.getPool()->deleteOptionDef6(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ code, space);
+ addDeleteAuditEntry("dhcp6_option_def", option_def->getId());
+ }
+ }
+
+ /// @brief Deletes specified global option from the configuration backend
+ /// and generates audit entry.
+ ///
+ /// @param code Option code.
+ /// @param space Option space.
+ void remoteDeleteOption(const uint16_t code, const std::string& space) {
+ auto& mgr = ConfigBackendDHCPv6Mgr::instance();
+
+ auto option = mgr.getPool()->getOption6(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ code, space);
+
+ if (option) {
+ mgr.getPool()->deleteOptionDef6(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ code, space);
+ addDeleteAuditEntry("dhcp6_option_def", option->getId());
+ }
+ }
+
+ /// @brief Deletes specified shared network from the configuration backend
+ /// and generates audit entry.
+ ///
+ /// @param name Name of the shared network to be deleted.
+ void remoteDeleteSharedNetwork(const std::string& name) {
+ auto& mgr = ConfigBackendDHCPv6Mgr::instance();
+
+ auto network = mgr.getPool()->getSharedNetwork6(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ name);
+
+ if (network) {
+ mgr.getPool()->deleteSharedNetwork6(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ name);
+ addDeleteAuditEntry("dhcp6_shared_network", network->getId());
+ }
+ }
+
+ /// @brief Deletes specified subnet from the configuration backend and
+ /// generates audit entry.
+ void remoteDeleteSubnet(const SubnetID& id) {
+ auto& mgr = ConfigBackendDHCPv6Mgr::instance();
+
+ mgr.getPool()->deleteSubnet6(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ id);
+ addDeleteAuditEntry("dhcp6_subnet", id);
+ }
+
+ /// @brief Deletes specified client class from the configuration backend
+ /// and generates audit entry.
+ ///
+ /// @param name Name of the client class to be deleted.
+ void remoteDeleteClientClass(const std::string& name) {
+ auto& mgr = ConfigBackendDHCPv6Mgr::instance();
+
+ auto client_class = mgr.getPool()->getClientClass6(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ name);
+
+ if (client_class) {
+ mgr.getPool()->deleteClientClass6(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ name);
+ addDeleteAuditEntry("dhcp6_client_class", client_class->getId());
+ }
+ }
+
+ /// @brief Tests the @c CBControlDHCPv6::databaseConfigApply method.
+ ///
+ /// This test inserts configuration elements of each type into the
+ /// configuration database. Next, it calls the @c databaseConfigApply,
+ /// which should merge each object from the database for which the
+ /// CREATE or UPDATE audit entry is found. The test then verifies
+ /// if the appropriate entries have been merged.
+ ///
+ /// @param lb_modification_time Lower bound modification time to be
+ /// passed to the @c databaseConfigApply.
+ void testDatabaseConfigApply(const boost::posix_time::ptime& lb_modification_time) {
+ remoteStoreTestConfiguration();
+
+ ASSERT_FALSE(audit_entries_.empty())
+ << "Require at least one audit entry. The test is broken!";
+
+ ctl_.databaseConfigApply(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ lb_modification_time, audit_entries_);
+
+ // The updates should have been merged into current configuration.
+ auto srv_cfg = CfgMgr::instance().getCurrentCfg();
+
+ // If there is an audit entry for global parameter and the parameter
+ // modification time is later than last audit revision time it should
+ // be merged.
+ if (hasConfigElement("dhcp6_global_parameter") &&
+ (getTimestamp("dhcp6_global_parameter") > lb_modification_time)) {
+ checkConfiguredGlobal(srv_cfg, "comment", Element::create("bar"));
+
+ } else {
+ // Otherwise it shouldn't exist.
+ EXPECT_FALSE(srv_cfg->getConfiguredGlobals()->get("comment"));
+ }
+
+ // If there is an audit entry for option definition and the definition
+ // modification time is later than last audit revision time it should
+ // be merged.
+ auto found_def = srv_cfg->getCfgOptionDef()->get("isc", "one");
+ if (hasConfigElement("dhcp6_option_def") &&
+ getTimestamp("dhcp6_option_def") > lb_modification_time) {
+ ASSERT_TRUE(found_def);
+ EXPECT_EQ(101, found_def->getCode());
+ EXPECT_EQ(OptionDataType::OPT_UINT16_TYPE, found_def->getType());
+
+ } else {
+ EXPECT_FALSE(found_def);
+ }
+
+ // If there is an audit entry for an option and the option
+ // modification time is later than last audit revision time it should
+ // be merged.
+ auto options = srv_cfg->getCfgOption();
+ auto found_opt = options->get(DHCP6_OPTION_SPACE, D6O_BOOTFILE_URL);
+ if (hasConfigElement("dhcp6_options") &&
+ (getTimestamp("dhcp6_options") > lb_modification_time)) {
+ ASSERT_TRUE(found_opt.option_);
+ EXPECT_EQ("some.bootfile", found_opt.option_->toString());
+
+ } else {
+ EXPECT_FALSE(found_opt.option_);
+ }
+
+ // If there is an audit entry for a shared network and the network
+ // modification time is later than last audit revision time it should
+ // be merged.
+ auto networks = srv_cfg->getCfgSharedNetworks6();
+ auto found_network = networks->getByName("one");
+ if (hasConfigElement("dhcp6_shared_network") &&
+ (getTimestamp("dhcp6_shared_network") > lb_modification_time)) {
+ ASSERT_TRUE(found_network);
+ EXPECT_TRUE(found_network->hasFetchGlobalsFn());
+
+ } else {
+ EXPECT_FALSE(found_network);
+ }
+
+ // If there is an audit entry for a subnet and the subnet modification
+ // time is later than last audit revision time it should be merged.
+ auto subnets = srv_cfg->getCfgSubnets6();
+ auto found_subnet = subnets->getBySubnetId(1);
+ if (hasConfigElement("dhcp6_subnet") &&
+ (getTimestamp("dhcp6_subnet") > lb_modification_time)) {
+ ASSERT_TRUE(found_subnet);
+ EXPECT_TRUE(found_subnet->hasFetchGlobalsFn());
+
+ } else {
+ EXPECT_FALSE(found_subnet);
+ }
+
+ auto client_classes = srv_cfg->getClientClassDictionary();
+ auto found_class = client_classes->findClass("first-class");
+ if (hasConfigElement("dhcp6_client_class") &&
+ (getTimestamp("dhcp6_client_class") > lb_modification_time)) {
+ ASSERT_TRUE(found_class);
+ ASSERT_TRUE(found_class->getMatchExpr());
+ EXPECT_GT(found_class->getMatchExpr()->size(), 0);
+ EXPECT_EQ("first-class", found_class->getName());
+
+ // Check for class option, make sure it has been "created".
+ auto cfg_option_desc = found_class->getCfgOption();
+ ASSERT_TRUE(cfg_option_desc);
+ auto option_desc = cfg_option_desc->get(DHCP6_OPTION_SPACE, D6O_BOOTFILE_URL);
+ auto option = option_desc.option_;
+ ASSERT_TRUE(option);
+ EXPECT_EQ(OptionBuffer(option_desc.formatted_value_.begin(),
+ option_desc.formatted_value_.end()),
+ option->getData());
+ } else {
+ EXPECT_FALSE(found_class);
+ }
+ }
+
+ /// @brief Tests deletion of the configuration elements by the
+ /// @c CBControlDHCPv6::databaseConfigApply method.
+ ///
+ /// This test inserts configuration elements of each type into the
+ /// configuration database and calls the @c databaseConfigApply
+ /// to fetch this configuration and merge into the local server
+ /// configuration.
+ ///
+ /// Next, the test calls the specified callback function, i.e.
+ /// @c db_modifications, which deletes selected configuration
+ /// elements from the database and generates appropriate audit
+ /// entries. Finally, it calls the @c databaseConfigApply again
+ /// to process the audit entries and checks if the appropriate
+ /// configuration elements are deleted from the local configuration
+ ///
+ /// @param lb_modification_time Lower bound modification time to be
+ /// passed to the @c databaseConfigApply.
+ /// @param db_modifications Pointer to the callback function which
+ /// applies test specific modifications into the database.
+ void testDatabaseConfigApplyDelete(const boost::posix_time::ptime& lb_modification_time,
+ std::function<void()> db_modifications) {
+ // Store initial configuration into the database.
+ remoteStoreTestConfiguration();
+
+ // Since we pass an empty audit collection the server treats this
+ // as if the server is starting up and fetches the entire
+ // configuration from the database.
+ ctl_.databaseConfigApply(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ ctl_.getInitialAuditRevisionTime(),
+ AuditEntryCollection());
+ // Commit the configuration so as it is moved from the staging
+ // to current.
+ CfgMgr::instance().commit();
+
+ // Run user defined callback which should delete selected configuration
+ // elements from the configuration backend. The appropriate DELETE
+ // audit entries should now be stored in the audit_entries_ collection.
+ if (db_modifications) {
+ db_modifications();
+ }
+
+ // Process the DELETE audit entries.
+ ctl_.databaseConfigApply(BackendSelector::UNSPEC(), ServerSelector::ALL(),
+ lb_modification_time, audit_entries_);
+
+ // All changes should have been applied in the current configuration.
+ auto srv_cfg = CfgMgr::instance().getCurrentCfg();
+
+ {
+ SCOPED_TRACE("global parameters");
+ // One of the global parameters should still be there.
+ EXPECT_TRUE(srv_cfg->getConfiguredGlobals()->get("data-directory"));
+ if (deleteConfigElement("dhcp6_global_parameter", 1)) {
+ EXPECT_FALSE(srv_cfg->getConfiguredGlobals()->get("comment"));
+
+ } else {
+ EXPECT_TRUE(srv_cfg->getConfiguredGlobals()->get("comment"));
+ }
+ }
+
+ {
+ SCOPED_TRACE("option definitions");
+ // One of the option definitions should still be there.
+ EXPECT_TRUE(srv_cfg->getCfgOptionDef()->get("isc", "two"));
+ auto found_def = srv_cfg->getCfgOptionDef()->get("isc", "one");
+ if (deleteConfigElement("dhcp6_option_def", 1)) {
+ EXPECT_FALSE(found_def);
+
+ } else {
+ EXPECT_TRUE(found_def);
+ }
+ }
+
+ {
+ SCOPED_TRACE("global options");
+ // One of the options should still be there.
+ EXPECT_TRUE(srv_cfg->getCfgOption()->get(DHCP6_OPTION_SPACE,
+ D6O_AFTR_NAME).option_);
+ auto found_opt = srv_cfg->getCfgOption()->get(DHCP6_OPTION_SPACE,
+ D6O_AFTR_NAME);
+ if (deleteConfigElement("dhcp6_options", 1)) {
+ EXPECT_FALSE(found_opt.option_);
+
+ } else {
+ EXPECT_TRUE(found_opt.option_);
+ }
+ }
+
+ {
+ SCOPED_TRACE("shared networks");
+ // One of the shared networks should still be there.
+ EXPECT_TRUE(srv_cfg->getCfgSharedNetworks6()->getByName("two"));
+ auto found_network = srv_cfg->getCfgSharedNetworks6()->getByName("one");
+ if (deleteConfigElement("dhcp6_shared_network", 1)) {
+ EXPECT_FALSE(found_network);
+
+ } else {
+ EXPECT_TRUE(found_network);
+ }
+ }
+
+ {
+ SCOPED_TRACE("subnets");
+ // One of the subnets should still be there.
+ EXPECT_TRUE(srv_cfg->getCfgSubnets6()->getBySubnetId(2));
+ auto found_subnet = srv_cfg->getCfgSubnets6()->getBySubnetId(1);
+ if (deleteConfigElement("dhcp6_subnet", 1)) {
+ EXPECT_FALSE(found_subnet);
+ // If the subnet has been deleted, make sure that
+ // it was detached from the shared network it belonged
+ // to, if the shared network still exists.
+ auto found_network = srv_cfg->getCfgSharedNetworks6()->getByName("one");
+ if (found_network) {
+ EXPECT_TRUE(found_network->getAllSubnets()->empty());
+ }
+
+ } else {
+ EXPECT_TRUE(found_subnet);
+ }
+ }
+ }
+
+ /// @brief Instance of the @c CBControlDHCPv6 used for testing.
+ TestCBControlDHCPv6 ctl_;
+};
+
+
+// This test verifies that the configuration updates for all object
+// types are merged into the current configuration.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyAll) {
+
+ addCreateAuditEntry("dhcp6_global_parameter", 1);
+ addCreateAuditEntry("dhcp6_global_parameter", 2);
+ addCreateAuditEntry("dhcp6_option_def", 1);
+ addCreateAuditEntry("dhcp6_option_def", 2);
+ addCreateAuditEntry("dhcp6_options", 1);
+ addCreateAuditEntry("dhcp6_options", 2);
+ addCreateAuditEntry("dhcp6_shared_network", 1);
+ addCreateAuditEntry("dhcp6_shared_network", 2);
+ addCreateAuditEntry("dhcp6_subnet", 1);
+ addCreateAuditEntry("dhcp6_subnet", 2);
+ addCreateAuditEntry("dhcp6_client_class", 1);
+ addCreateAuditEntry("dhcp6_client_class", 2);
+
+ testDatabaseConfigApply(getTimestamp(-5));
+}
+
+// This test verifies that multiple configuration elements are
+// deleted from the local configuration as a result of being
+// deleted from the database.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyDeleteAll) {
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ remoteDeleteGlobalParameter("comment", 1);
+ remoteDeleteOptionDef(101, "isc");
+ remoteDeleteOption(D6O_BOOTFILE_URL, DHCP6_OPTION_SPACE);
+ remoteDeleteSharedNetwork("one");
+ remoteDeleteSubnet(SubnetID(1));
+ });
+}
+
+// This test verifies that an attempt to delete non-existing
+// configuration element does not cause an error.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyDeleteNonExisting) {
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ // Add several audit entries instructing to delete the
+ // non-existing configuration elements. The ids are set
+ // to 3, but the only existing elements have ids of 1
+ // and 2.
+ addDeleteAuditEntry("dhcp6_global_parameter", 3);
+ addDeleteAuditEntry("dhcp6_option_def", 3);
+ addDeleteAuditEntry("dhcp6_options", 3);
+ addDeleteAuditEntry("dhcp6_shared_network", 3);
+ addDeleteAuditEntry("dhcp6_subnet", 3);
+ });
+}
+
+// This test verifies that only a global parameter is merged into
+// the current configuration.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyGlobal) {
+ addCreateAuditEntry("dhcp6_global_parameter", 1);
+ addCreateAuditEntry("dhcp6_global_parameter", 2);
+ testDatabaseConfigApply(getTimestamp(-5));
+}
+
+// This test verifies that the global parameter is deleted from
+// the local configuration as a result of being deleted from the
+// database.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyDeleteGlobal) {
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ remoteDeleteGlobalParameter("comment", 1);
+ });
+}
+
+// This test verifies that global parameter is not fetched from the
+// database when the modification time is earlier than the last
+// fetched audit entry.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyGlobalNotFetched) {
+ addCreateAuditEntry("dhcp6_global_parameter", 1);
+ addCreateAuditEntry("dhcp6_global_parameter", 2);
+ testDatabaseConfigApply(getTimestamp(-3));
+}
+
+// This test verifies that only an option definition is merged into
+// the current configuration.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyOptionDef) {
+ addCreateAuditEntry("dhcp6_option_def", 1);
+ addCreateAuditEntry("dhcp6_option_def", 2);
+ testDatabaseConfigApply(getTimestamp(-5));
+}
+
+// This test verifies that the option definition is deleted from
+// the local configuration as a result of being deleted from the
+// database.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyDeleteOptionDef) {
+ addDeleteAuditEntry("dhcp6_option_def", 1);
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ remoteDeleteOptionDef(101, "isc");
+ });
+}
+
+// This test verifies that option definition is not fetched from the
+// database when the modification time is earlier than the last
+// fetched audit entry.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyOptionDefNotFetched) {
+ addCreateAuditEntry("dhcp6_option_def", 1);
+ addCreateAuditEntry("dhcp6_option_def", 2);
+ testDatabaseConfigApply(getTimestamp(-3));
+}
+
+// This test verifies that only a DHCPv6 option is merged into the
+// current configuration.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyOption) {
+ addCreateAuditEntry("dhcp6_options", 1);
+ addCreateAuditEntry("dhcp6_options", 2);
+ testDatabaseConfigApply(getTimestamp(-5));
+}
+
+// This test verifies that the global option is deleted from
+// the local configuration as a result of being deleted from the
+// database.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyDeleteOption) {
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ remoteDeleteOption(D6O_BOOTFILE_URL, DHCP6_OPTION_SPACE);
+ });
+}
+
+// This test verifies that DHCPv6 option is not fetched from the
+// database when the modification time is earlier than the last
+// fetched audit entry.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyOptionNotFetched) {
+ addCreateAuditEntry("dhcp6_options", 1);
+ addCreateAuditEntry("dhcp6_options", 2);
+ testDatabaseConfigApply(getTimestamp(-3));
+}
+
+// This test verifies that only a shared network is merged into the
+// current configuration.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplySharedNetwork) {
+ addCreateAuditEntry("dhcp6_shared_network", 1);
+ addCreateAuditEntry("dhcp6_shared_network", 2);
+ testDatabaseConfigApply(getTimestamp(-5));
+}
+
+// This test verifies that the shared network is deleted from
+// the local configuration as a result of being deleted from the
+// database.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyDeleteSharedNetwork) {
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ remoteDeleteSharedNetwork("one");
+ });
+}
+
+// This test verifies that shared network is not fetched from the
+// database when the modification time is earlier than the last
+// fetched audit entry.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplySharedNetworkNotFetched) {
+ addCreateAuditEntry("dhcp6_shared_network", 1);
+ addCreateAuditEntry("dhcp6_shared_network", 2);
+ testDatabaseConfigApply(getTimestamp(-3));
+}
+
+// This test verifies that only a subnet is merged into the current
+// configuration.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplySubnet) {
+ addCreateAuditEntry("dhcp6_shared_network", 1);
+ addCreateAuditEntry("dhcp6_shared_network", 2);
+ addCreateAuditEntry("dhcp6_subnet", 1);
+ addCreateAuditEntry("dhcp6_subnet", 2);
+ testDatabaseConfigApply(getTimestamp(-5));
+}
+
+// This test verifies that the subnet is deleted from the local
+// configuration as a result of being deleted from the database.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyDeleteSubnet) {
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ remoteDeleteSubnet(SubnetID(1));
+ });
+}
+
+// This test verifies that subnet is not fetched from the database
+// when the modification time is earlier than the last fetched audit
+// entry.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplySubnetNotFetched) {
+ addCreateAuditEntry("dhcp6_subnet", 1);
+ addCreateAuditEntry("dhcp6_subnet", 2);
+ testDatabaseConfigApply(getTimestamp(-3));
+}
+
+// This test verifies that only client classes are merged into the current
+// configuration.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyClientClasses) {
+ addCreateAuditEntry("dhcp6_client_class", 1);
+ addCreateAuditEntry("dhcp6_client_class", 2);
+ testDatabaseConfigApply(getTimestamp(-5));
+}
+
+// This test verifies that a client class is deleted from the local
+// configuration as a result of being deleted from the database.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyDeleteClientClass) {
+ testDatabaseConfigApplyDelete(getTimestamp(-5), [this]() {
+ remoteDeleteClientClass("first-class");
+ });
+}
+
+// This test verifies that the configuration update calls the hook.
+TEST_F(CBControlDHCPv6Test, databaseConfigApplyHook) {
+
+ // Initialize Hooks Manager.
+ HooksManager::loadLibraries(HookLibsCollection());
+
+ // Install cb6_updated.
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "cb6_updated", cb6_updated_callout));
+
+ // Create audit entries.
+ addCreateAuditEntry("dhcp6_global_parameter", 1);
+ addCreateAuditEntry("dhcp6_global_parameter", 2);
+ addCreateAuditEntry("dhcp6_option_def", 1);
+ addCreateAuditEntry("dhcp6_option_def", 2);
+ addCreateAuditEntry("dhcp6_options", 1);
+ addCreateAuditEntry("dhcp6_options", 2);
+ addCreateAuditEntry("dhcp6_shared_network", 1);
+ addCreateAuditEntry("dhcp6_shared_network", 2);
+ addCreateAuditEntry("dhcp6_subnet", 1);
+ addCreateAuditEntry("dhcp6_subnet", 2);
+
+ // Run the test.
+ testDatabaseConfigApply(getTimestamp(-5));
+
+ // Checks the callout.
+ EXPECT_EQ("cb6_updated", callback_name_);
+ ASSERT_TRUE(callback_audit_entries_);
+ EXPECT_TRUE(audit_entries_ == *callback_audit_entries_);
+}
+
+// This test verifies that it is possible to set ip-reservations-unique
+// parameter via configuration backend and that it is successful when
+// host database backend accepts the new setting.
+TEST_F(CBControlDHCPv6Test, ipReservationsNonUniqueAccepted) {
+ // Create host data source which accepts setting non-unique IP
+ // reservations.
+ MemHostDataSourcePtr hds(new MemHostDataSource());
+ auto testFactory = [hds](const DatabaseConnection::ParameterMap&) {
+ return (hds);
+ };
+ HostDataSourceFactory::registerFactory("test", testFactory);
+ HostMgr::addBackend("type=test");
+
+ // Insert ip-reservations-unique value set to false into the database.
+ auto& mgr = ConfigBackendDHCPv6Mgr::instance();
+ StampedValuePtr global_parameter = StampedValue::create("ip-reservations-unique",
+ Element::create(false));
+ ASSERT_NO_THROW(mgr.getPool()->createUpdateGlobalParameter6(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ global_parameter));
+ // Adding audit entry simulates the case when the server is already configured
+ // and we're adding incremental changes. These changes should be applied to
+ // the current configuration.
+ addCreateAuditEntry("dhcp6_global_parameter", 1);
+
+ // Apply the configuration.
+ ASSERT_NO_THROW(ctl_.databaseConfigApply(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ getTimestamp(-5),
+ audit_entries_));
+ // The new setting should be visible in both CfgDbAccess and HostMgr.
+ EXPECT_FALSE(CfgMgr::instance().getCurrentCfg()->getCfgDbAccess()->getIPReservationsUnique());
+ EXPECT_FALSE(HostMgr::instance().getIPReservationsUnique());
+}
+
+// This test verifies that the new setting of ip-reservations-unique is not
+// accepted when one of the host database backends does not support it.
+TEST_F(CBControlDHCPv6Test, ipReservationsNonUniqueRefused) {
+ // Create host data source which does not accept setting IP reservations
+ // non-unique setting.
+ NonUniqueHostDataSourcePtr hds(new NonUniqueHostDataSource());
+ auto testFactory = [hds](const DatabaseConnection::ParameterMap&) {
+ return (hds);
+ };
+ HostDataSourceFactory::registerFactory("test", testFactory);
+ HostMgr::addBackend("type=test");
+
+ // Insert ip-reservations-unique value set to false into the database.
+ auto& mgr = ConfigBackendDHCPv6Mgr::instance();
+ StampedValuePtr global_parameter = StampedValue::create("ip-reservations-unique",
+ Element::create(false));
+ ASSERT_NO_THROW(mgr.getPool()->createUpdateGlobalParameter6(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ global_parameter));
+ // Adding audit entry simulates the case when the server is already configured
+ // and we're adding incremental changes. These changes should be applied to
+ // the current configuration.
+ addCreateAuditEntry("dhcp6_global_parameter", 1);
+
+ // Apply the configuration.
+ ASSERT_NO_THROW(ctl_.databaseConfigApply(BackendSelector::UNSPEC(),
+ ServerSelector::ALL(),
+ getTimestamp(-5),
+ audit_entries_));
+ // The default setting should be applied, because the backend refused to
+ // set it to false.
+ EXPECT_TRUE(CfgMgr::instance().getCurrentCfg()->getCfgDbAccess()->getIPReservationsUnique());
+ EXPECT_TRUE(HostMgr::instance().getIPReservationsUnique());
+}
+
+}
diff --git a/src/lib/dhcpsrv/tests/cfg_db_access_unittest.cc b/src/lib/dhcpsrv/tests/cfg_db_access_unittest.cc
new file mode 100644
index 0000000..738ddfc
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_db_access_unittest.cc
@@ -0,0 +1,338 @@
+// Copyright (C) 2016-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <cc/data.h>
+#include <database/dbaccess_parser.h>
+#include <dhcpsrv/cfg_db_access.h>
+#include <dhcpsrv/host_data_source_factory.h>
+#include <dhcpsrv/host_mgr.h>
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <testutils/test_to_element.h>
+#include <gtest/gtest.h>
+
+#if defined HAVE_MYSQL
+#include <mysql/testutils/mysql_schema.h>
+#endif
+
+#if defined HAVE_PGSQL
+#include <pgsql/testutils/pgsql_schema.h>
+#endif
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::test;
+
+namespace {
+
+// Test default values of the lease and host database access strings.
+TEST(CfgDbAccessTest, defaults) {
+ CfgDbAccess cfg;
+ EXPECT_EQ("type=memfile", cfg.getLeaseDbAccessString());
+ std::string expected = "{ \"type\": \"memfile\" }";
+ runToElementTest<CfgLeaseDbAccess>(expected, CfgLeaseDbAccess(cfg));
+
+ EXPECT_TRUE(cfg.getHostDbAccessString().empty());
+ runToElementTest<CfgHostDbAccess>("[ ]", CfgHostDbAccess(cfg));
+}
+
+// This test verifies that it is possible to set the lease database
+// string.
+TEST(CfgDbAccessTest, setLeaseDbAccessString) {
+ CfgDbAccess cfg;
+ ASSERT_NO_THROW(cfg.setLeaseDbAccessString("type=mysql"));
+ EXPECT_EQ("type=mysql", cfg.getLeaseDbAccessString());
+
+ // Check unparse
+ std::string expected = "{ \"type\": \"mysql\" }";
+ runToElementTest<CfgLeaseDbAccess>(expected, CfgLeaseDbAccess(cfg));
+
+ // Append additional parameter.
+ ASSERT_NO_THROW(cfg.setAppendedParameters("universe=4"));
+ EXPECT_EQ("type=mysql universe=4", cfg.getLeaseDbAccessString());
+
+ // Additional parameters are not in lease_db_access_
+ runToElementTest<CfgLeaseDbAccess>(expected, CfgLeaseDbAccess(cfg));
+
+ // If access string is empty, no parameters will be appended.
+ ASSERT_NO_THROW(cfg.setLeaseDbAccessString(""));
+ EXPECT_TRUE(cfg.getLeaseDbAccessString().empty());
+}
+
+
+// This test verifies that it is possible to set the host database
+// string.
+TEST(CfgDbAccessTest, setHostDbAccessString) {
+ CfgDbAccess cfg;
+ ASSERT_NO_THROW(cfg.setHostDbAccessString("type=mysql"));
+ EXPECT_EQ("type=mysql", cfg.getHostDbAccessString());
+
+ // Check unparse
+ std::string expected = "[ { \"type\": \"mysql\" } ]";
+ runToElementTest<CfgHostDbAccess>(expected, CfgHostDbAccess(cfg));
+
+ // Append additional parameter.
+ ASSERT_NO_THROW(cfg.setAppendedParameters("universe=4"));
+ EXPECT_EQ("type=mysql universe=4", cfg.getHostDbAccessString());
+
+ // Additional parameters are not in host_db_access_
+ runToElementTest<CfgHostDbAccess>(expected, CfgHostDbAccess(cfg));
+
+ // If access string is empty, no parameters will be appended.
+ CfgDbAccess cfg1;
+ ASSERT_NO_THROW(cfg1.setHostDbAccessString(""));
+ EXPECT_TRUE(cfg1.getHostDbAccessString().empty());
+}
+
+// This test verifies that it is possible to set multiple host
+// database string.
+TEST(CfgDbAccessTest, pushHostDbAccessString) {
+ // Push a string.
+ CfgDbAccess cfg;
+ ASSERT_NO_THROW(cfg.setHostDbAccessString("type=foo"));
+
+ // Push another in front.
+ ASSERT_NO_THROW(cfg.setHostDbAccessString("type=mysql", true));
+ EXPECT_EQ("type=mysql", cfg.getHostDbAccessString());
+
+ // Push a third string.
+ ASSERT_NO_THROW(cfg.setHostDbAccessString("type=bar"));
+
+ // Check unparse
+ std::string expected = "[ { \"type\": \"mysql\" }, ";
+ expected += "{ \"type\": \"foo\" }, { \"type\": \"bar\" } ]";
+ runToElementTest<CfgHostDbAccess>(expected, CfgHostDbAccess(cfg));
+
+ // Check access strings
+ std::list<std::string> hal = cfg.getHostDbAccessStringList();
+ ASSERT_EQ(3, hal.size());
+ std::list<std::string>::const_iterator it = hal.cbegin();
+ ASSERT_NE(hal.cend(), it);
+ EXPECT_EQ("type=mysql", *it);
+ ASSERT_NE(hal.cend(), ++it);
+ EXPECT_EQ("type=foo", *it);
+ ASSERT_NE(hal.cend(), ++it);
+ EXPECT_EQ("type=bar", *it);
+
+ // Build a similar list with the first string empty so it will be ignored.
+ CfgDbAccess cfg1;
+ ASSERT_NO_THROW(cfg1.setHostDbAccessString(""));
+ ASSERT_NO_THROW(cfg1.setHostDbAccessString("type=foo"));
+ ASSERT_NO_THROW(cfg1.setHostDbAccessString("type=bar"));
+ expected = "[ { \"type\": \"foo\" }, { \"type\": \"bar\" } ]";
+ runToElementTest<CfgHostDbAccess>(expected, CfgHostDbAccess(cfg1));
+ hal = cfg1.getHostDbAccessStringList();
+ ASSERT_EQ(2, hal.size());
+ EXPECT_EQ("type=foo", hal.front());
+ EXPECT_EQ("type=bar", hal.back());
+}
+
+// Tests that lease manager can be created from a specified configuration.
+TEST(CfgDbAccessTest, createLeaseMgr) {
+ CfgDbAccess cfg;
+ ASSERT_NO_THROW(cfg.setLeaseDbAccessString("type=memfile persist=false universe=4"));
+ ASSERT_NO_THROW(cfg.createManagers());
+
+ ASSERT_NO_THROW({
+ LeaseMgr& lease_mgr = LeaseMgrFactory::instance();
+ EXPECT_EQ("memfile",lease_mgr.getType());
+ });
+}
+
+// The following tests require MySQL enabled.
+#if defined HAVE_MYSQL
+
+/// @brief Test fixture class for testing @ref CfgDbAccessTest using MySQL
+/// backend.
+class CfgMySQLDbAccessTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ CfgMySQLDbAccessTest() {
+ // Ensure we have the proper schema with no transient data.
+ db::test::createMySQLSchema();
+ }
+
+ /// @brief Destructor.
+ virtual ~CfgMySQLDbAccessTest() {
+ // If data wipe enabled, delete transient data otherwise destroy the schema
+ db::test::destroyMySQLSchema();
+ LeaseMgrFactory::destroy();
+ }
+};
+
+
+// Tests that MySQL lease manager and host data source can be created from a
+// specified configuration.
+TEST_F(CfgMySQLDbAccessTest, createManagers) {
+ CfgDbAccess cfg;
+ ASSERT_NO_THROW(cfg.setLeaseDbAccessString(db::test::validMySQLConnectionString()));
+ ASSERT_NO_THROW(cfg.setHostDbAccessString(db::test::validMySQLConnectionString()));
+ ASSERT_NO_THROW(cfg.createManagers());
+
+ ASSERT_NO_THROW({
+ LeaseMgr& lease_mgr = LeaseMgrFactory::instance();
+ EXPECT_EQ("mysql", lease_mgr.getType());
+ });
+
+ ASSERT_NO_THROW({
+ const HostDataSourcePtr& host_data_source =
+ HostMgr::instance().getHostDataSource();
+ ASSERT_TRUE(host_data_source);
+ EXPECT_EQ("mysql", host_data_source->getType());
+ });
+
+ // Because of the lazy initialization of the HostMgr instance, it is
+ // possible that the first call to the instance() function tosses
+ // existing connection to the database created by the call to
+ // createManagers(). Let's make sure that this doesn't happen.
+ ASSERT_NO_THROW(HostMgr::instance());
+
+ ASSERT_NO_THROW({
+ const HostDataSourcePtr& host_data_source =
+ HostMgr::instance().getHostDataSource();
+ ASSERT_TRUE(host_data_source);
+ EXPECT_EQ("mysql", host_data_source->getType());
+ });
+
+ EXPECT_TRUE(HostMgr::instance().getIPReservationsUnique());
+}
+
+// Tests that the createManagers function utilizes the setting in the
+// CfgDbAccess class which controls whether the IP reservations must
+// be unique or can be non-unique.
+TEST_F(CfgMySQLDbAccessTest, createManagersIPResrvUnique) {
+ CfgDbAccess cfg;
+
+ cfg.setIPReservationsUnique(false);
+
+ ASSERT_NO_THROW(cfg.setLeaseDbAccessString(db::test::validMySQLConnectionString()));
+ ASSERT_NO_THROW(cfg.setHostDbAccessString(db::test::validMySQLConnectionString()));
+ ASSERT_NO_THROW(cfg.createManagers());
+
+ ASSERT_NO_THROW({
+ const HostDataSourcePtr& host_data_source =
+ HostMgr::instance().getHostDataSource();
+ ASSERT_TRUE(host_data_source);
+ EXPECT_EQ("mysql", host_data_source->getType());
+ });
+
+ // Because of the lazy initialization of the HostMgr instance, it is
+ // possible that the first call to the instance() function tosses
+ // existing connection to the database created by the call to
+ // createManagers(). Let's make sure that this doesn't happen.
+ ASSERT_NO_THROW(HostMgr::instance());
+
+ ASSERT_NO_THROW({
+ const HostDataSourcePtr& host_data_source =
+ HostMgr::instance().getHostDataSource();
+ ASSERT_TRUE(host_data_source);
+ EXPECT_EQ("mysql", host_data_source->getType());
+ });
+
+ EXPECT_FALSE(HostMgr::instance().getIPReservationsUnique());
+}
+
+#endif
+
+// The following tests require PgSQL enabled.
+#if defined HAVE_PGSQL
+
+/// @brief Test fixture class for testing @ref CfgDbAccessTest using PgSQL
+/// backend.
+class CfgPgSQLDbAccessTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ CfgPgSQLDbAccessTest() {
+ // Ensure we have the proper schema with no transient data.
+ db::test::createPgSQLSchema();
+ }
+
+ /// @brief Destructor.
+ virtual ~CfgPgSQLDbAccessTest() {
+ // If data wipe enabled, delete transient data otherwise destroy the schema
+ db::test::destroyPgSQLSchema();
+ LeaseMgrFactory::destroy();
+ }
+};
+
+
+// Tests that PgSQL lease manager and host data source can be created from a
+// specified configuration.
+TEST_F(CfgPgSQLDbAccessTest, createManagers) {
+ CfgDbAccess cfg;
+ ASSERT_NO_THROW(cfg.setLeaseDbAccessString(db::test::validPgSQLConnectionString()));
+ ASSERT_NO_THROW(cfg.setHostDbAccessString(db::test::validPgSQLConnectionString()));
+ ASSERT_NO_THROW(cfg.createManagers());
+
+ ASSERT_NO_THROW({
+ LeaseMgr& lease_mgr = LeaseMgrFactory::instance();
+ EXPECT_EQ("postgresql", lease_mgr.getType());
+ });
+
+ ASSERT_NO_THROW({
+ const HostDataSourcePtr& host_data_source =
+ HostMgr::instance().getHostDataSource();
+ ASSERT_TRUE(host_data_source);
+ EXPECT_EQ("postgresql", host_data_source->getType());
+ });
+
+ // Because of the lazy initialization of the HostMgr instance, it is
+ // possible that the first call to the instance() function tosses
+ // existing connection to the database created by the call to
+ // createManagers(). Let's make sure that this doesn't happen.
+ ASSERT_NO_THROW(HostMgr::instance());
+
+ ASSERT_NO_THROW({
+ const HostDataSourcePtr& host_data_source =
+ HostMgr::instance().getHostDataSource();
+ ASSERT_TRUE(host_data_source);
+ EXPECT_EQ("postgresql", host_data_source->getType());
+ });
+
+ EXPECT_TRUE(HostMgr::instance().getIPReservationsUnique());
+}
+
+// Tests that the createManagers function utilizes the setting in the
+// CfgDbAccess class which controls whether the IP reservations must
+// be unique or can be non-unique.
+TEST_F(CfgPgSQLDbAccessTest, createManagersIPResrvUnique) {
+ CfgDbAccess cfg;
+
+ cfg.setIPReservationsUnique(false);
+
+ ASSERT_NO_THROW(cfg.setLeaseDbAccessString(db::test::validPgSQLConnectionString()));
+ ASSERT_NO_THROW(cfg.setHostDbAccessString(db::test::validPgSQLConnectionString()));
+ ASSERT_NO_THROW(cfg.createManagers());
+
+ ASSERT_NO_THROW({
+ const HostDataSourcePtr& host_data_source =
+ HostMgr::instance().getHostDataSource();
+ ASSERT_TRUE(host_data_source);
+ EXPECT_EQ("postgresql", host_data_source->getType());
+ });
+
+ // Because of the lazy initialization of the HostMgr instance, it is
+ // possible that the first call to the instance() function tosses
+ // existing connection to the database created by the call to
+ // createManagers(). Let's make sure that this doesn't happen.
+ ASSERT_NO_THROW(HostMgr::instance());
+
+ ASSERT_NO_THROW({
+ const HostDataSourcePtr& host_data_source =
+ HostMgr::instance().getHostDataSource();
+ ASSERT_TRUE(host_data_source);
+ EXPECT_EQ("postgresql", host_data_source->getType());
+ });
+
+ EXPECT_FALSE(HostMgr::instance().getIPReservationsUnique());
+}
+
+#endif
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/cfg_duid_unittest.cc b/src/lib/dhcpsrv/tests/cfg_duid_unittest.cc
new file mode 100644
index 0000000..10da5b9
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_duid_unittest.cc
@@ -0,0 +1,280 @@
+// Copyright (C) 2015-2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/duid.h>
+#include <dhcpsrv/cfg_duid.h>
+#include <exceptions/exceptions.h>
+#include <testutils/io_utils.h>
+#include <testutils/test_to_element.h>
+#include <util/encode/hex.h>
+#include <gtest/gtest.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <sstream>
+#include <string>
+#include <vector>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::test;
+using namespace isc::data;
+
+namespace {
+
+/// @brief Specifies file name holding DUID.
+const std::string DUID_FILE_NAME = "test.duid";
+
+/// @brief Test fixture class for @c CfgDUID.
+class CfgDUIDTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Removes DUID file if present.
+ CfgDUIDTest() {
+ static_cast<void>(remove(absolutePath(DUID_FILE_NAME).c_str()));
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Removes DUID file if present.
+ virtual ~CfgDUIDTest() {
+ static_cast<void>(remove(absolutePath(DUID_FILE_NAME).c_str()));
+ }
+
+ /// @brief Returns absolute path to a file used by tests.
+ ///
+ /// @param filename File name.
+ std::string absolutePath(const std::string& filename) const;
+
+ /// @brief Converts vector to string of hexadecimal digits.
+ ///
+ /// @param vec Input vector.
+ /// @return String of hexadecimal digits converted from vector.
+ std::string toString(const std::vector<uint8_t>& vec) const;
+};
+
+std::string
+CfgDUIDTest::absolutePath(const std::string& filename) const {
+ std::ostringstream s;
+ s << DHCP_DATA_DIR << "/" << filename;
+ return (s.str());
+}
+
+/// @brief Converts vector to string of hexadecimal digits.
+///
+/// @param vec Input vector.
+/// @return String of hexadecimal digits converted from vector.
+std::string
+CfgDUIDTest::toString(const std::vector<uint8_t>& vec) const {
+ try {
+ return (util::encode::encodeHex(vec));
+ } catch (...) {
+ ADD_FAILURE() << "toString: unable to encode vector to"
+ " hexadecimal string";
+ }
+ return ("");
+}
+
+
+// This test verifies default values of the DUID configuration.
+TEST_F(CfgDUIDTest, defaults) {
+ CfgDUID cfg_duid;
+ EXPECT_EQ(DUID::DUID_LLT, cfg_duid.getType());
+
+ EXPECT_TRUE(cfg_duid.getIdentifier().empty())
+ << "expected empty identifier, found: "
+ << toString(cfg_duid.getIdentifier());
+
+ EXPECT_EQ(0, cfg_duid.getHType());
+ EXPECT_EQ(0, cfg_duid.getTime());
+ EXPECT_EQ(0, cfg_duid.getEnterpriseId());
+ EXPECT_TRUE(cfg_duid.persist());
+ EXPECT_FALSE(cfg_duid.getContext());
+
+ std::string expected = "{ \"type\": \"LLT\",\n"
+ "\"identifier\": \"\",\n"
+ "\"htype\": 0,\n"
+ "\"time\": 0,\n"
+ "\"enterprise-id\": 0,\n"
+ "\"persist\": true }";
+ runToElementTest<CfgDUID>(expected, cfg_duid);
+}
+
+// This test verifies that it is possible to set values for the CfgDUID.
+TEST_F(CfgDUIDTest, setValues) {
+ CfgDUID cfg_duid;
+ // Set values.
+ ASSERT_NO_THROW(cfg_duid.setType(DUID::DUID_EN));
+ ASSERT_NO_THROW(cfg_duid.setIdentifier("ABCDEF"));
+ ASSERT_NO_THROW(cfg_duid.setHType(100));
+ ASSERT_NO_THROW(cfg_duid.setTime(32100));
+ ASSERT_NO_THROW(cfg_duid.setEnterpriseId(10));
+ ASSERT_NO_THROW(cfg_duid.setPersist(false));
+ std::string user_context = "{ \"comment\": \"bar\", \"foo\": 1 }";
+ ASSERT_NO_THROW(cfg_duid.setContext(Element::fromJSON(user_context)));
+
+ // Check that values have been set correctly.
+ EXPECT_EQ(DUID::DUID_EN, cfg_duid.getType());
+ EXPECT_EQ("ABCDEF", toString(cfg_duid.getIdentifier()));
+ EXPECT_EQ(100, cfg_duid.getHType());
+ EXPECT_EQ(32100, cfg_duid.getTime());
+ EXPECT_EQ(10, cfg_duid.getEnterpriseId());
+ EXPECT_FALSE(cfg_duid.persist());
+ ASSERT_TRUE(cfg_duid.getContext());
+ EXPECT_EQ(user_context, cfg_duid.getContext()->str());
+
+ std::string expected = "{\n"
+ " \"type\": \"EN\",\n"
+ " \"identifier\": \"ABCDEF\",\n"
+ " \"htype\": 100,\n"
+ " \"time\": 32100,\n"
+ " \"enterprise-id\": 10,\n"
+ " \"persist\": false,\n"
+ " \"user-context\": { \"foo\": 1,\n"
+ " \"comment\": \"bar\" }\n"
+ "}";
+ runToElementTest<CfgDUID>(expected, cfg_duid);
+}
+
+// This test checks positive scenarios for setIdentifier.
+TEST_F(CfgDUIDTest, setIdentifier) {
+ CfgDUID cfg_duid;
+ // Check that hexadecimal characters may be lower case.
+ ASSERT_NO_THROW(cfg_duid.setIdentifier("a1b2c3"));
+ EXPECT_EQ("A1B2C3", toString(cfg_duid.getIdentifier()));
+
+ // Check that whitespaces are allowed.
+ ASSERT_NO_THROW(cfg_duid.setIdentifier(" ABC DEF "));
+ EXPECT_EQ("ABCDEF", toString(cfg_duid.getIdentifier()));
+
+ // Check that identifier including only whitespaces is ignored.
+ ASSERT_NO_THROW(cfg_duid.setIdentifier(" "));
+ EXPECT_TRUE(cfg_duid.getIdentifier().empty())
+ << "expected empty identifier, found: "
+ << toString(cfg_duid.getIdentifier());
+}
+
+// This test verifies that the invalid identifier is rejected and
+// exception is thrown.
+TEST_F(CfgDUIDTest, setInvalidIdentifier) {
+ CfgDUID cfg_duid;
+ // Check that hexadecimal characters may be lower case.
+ ASSERT_NO_THROW(cfg_duid.setIdentifier("a1b2c3"));
+ EXPECT_EQ("A1B2C3", toString(cfg_duid.getIdentifier()));
+
+ // Try to set invalid value. This should not modify original
+ // value.
+ ASSERT_THROW(cfg_duid.setIdentifier("hola!"), isc::BadValue);
+ EXPECT_EQ("A1B2C3", toString(cfg_duid.getIdentifier()));
+}
+
+// This method checks that the DUID-LLT can be created from the
+// specified configuration.
+TEST_F(CfgDUIDTest, createLLT) {
+ CfgDUID cfg;
+ ASSERT_NO_THROW(cfg.setType(DUID::DUID_LLT));
+ ASSERT_NO_THROW(cfg.setTime(0x1123));
+ ASSERT_NO_THROW(cfg.setHType(8));
+ ASSERT_NO_THROW(cfg.setIdentifier("12564325A63F"));
+
+ // Generate DUID from this configuration.
+ DuidPtr duid;
+ ASSERT_NO_THROW(duid = cfg.create(absolutePath(DUID_FILE_NAME)));
+ ASSERT_TRUE(duid);
+
+ // Verify if the DUID is correct.
+ EXPECT_EQ("00:01:00:08:00:00:11:23:12:56:43:25:a6:3f",
+ duid->toText());
+
+ // Verify that the DUID file has been created.
+ EXPECT_TRUE(fileExists(absolutePath(DUID_FILE_NAME)));
+
+ // Verify getCurrentDuid() returns the value created.
+ DuidPtr current_duid = cfg.getCurrentDuid();
+ ASSERT_TRUE(current_duid);
+ EXPECT_EQ(*current_duid, *duid);
+}
+
+// This method checks that the DUID-EN can be created from the
+// specified configuration.
+TEST_F(CfgDUIDTest, createEN) {
+ CfgDUID cfg;
+ ASSERT_NO_THROW(cfg.setType(DUID::DUID_EN));
+ ASSERT_NO_THROW(cfg.setIdentifier("250F3E26A762"));
+ ASSERT_NO_THROW(cfg.setEnterpriseId(0x1010));
+
+ // Generate DUID from this configuration.
+ DuidPtr duid;
+ ASSERT_NO_THROW(duid = cfg.create(absolutePath(DUID_FILE_NAME)));
+ ASSERT_TRUE(duid);
+
+ // Verify if the DUID is correct.
+ EXPECT_EQ("00:02:00:00:10:10:25:0f:3e:26:a7:62", duid->toText());
+
+ // Verify that the DUID file has been created.
+ EXPECT_TRUE(fileExists(absolutePath(DUID_FILE_NAME)));
+
+ // Verify getCurrentDuid() returns the value created.
+ DuidPtr current_duid = cfg.getCurrentDuid();
+ ASSERT_TRUE(current_duid);
+ EXPECT_EQ(*current_duid, *duid);
+}
+
+// This method checks that the DUID-LL can be created from the
+// specified configuration.
+TEST_F(CfgDUIDTest, createLL) {
+ CfgDUID cfg;
+ ASSERT_NO_THROW(cfg.setType(DUID::DUID_LL));
+ ASSERT_NO_THROW(cfg.setIdentifier("124134A4B367"));
+ ASSERT_NO_THROW(cfg.setHType(2));
+
+ // Generate DUID from this configuration.
+ DuidPtr duid;
+ ASSERT_NO_THROW(duid = cfg.create(absolutePath(DUID_FILE_NAME)));
+ ASSERT_TRUE(duid);
+
+ // Verify if the DUID is correct.
+ EXPECT_EQ("00:03:00:02:12:41:34:a4:b3:67", duid->toText());
+
+ // Verify that the DUID file has been created.
+ EXPECT_TRUE(fileExists(absolutePath(DUID_FILE_NAME)));
+
+ // Verify getCurrentDuid() returns the value created.
+ DuidPtr current_duid = cfg.getCurrentDuid();
+ ASSERT_TRUE(current_duid);
+ EXPECT_EQ(*current_duid, *duid);
+}
+
+// This test verifies that it is possible to disable storing
+// generated DUID on a hard drive.
+TEST_F(CfgDUIDTest, createDisableWrite) {
+ CfgDUID cfg;
+ ASSERT_NO_THROW(cfg.setType(DUID::DUID_EN));
+ ASSERT_NO_THROW(cfg.setIdentifier("250F3E26A762"));
+ ASSERT_NO_THROW(cfg.setEnterpriseId(0x1010));
+ ASSERT_NO_THROW(cfg.setPersist(false));
+
+ // Generate DUID from this configuration.
+ DuidPtr duid;
+ ASSERT_NO_THROW(duid = cfg.create(absolutePath(DUID_FILE_NAME)));
+ ASSERT_TRUE(duid);
+
+ // Verify if the DUID is correct.
+ EXPECT_EQ("00:02:00:00:10:10:25:0f:3e:26:a7:62", duid->toText());
+
+ // DUID persistence is disabled so there should be no DUID file.
+ EXPECT_FALSE(fileExists(absolutePath(DUID_FILE_NAME)));
+
+ // Verify getCurrentDuid() returns the value created.
+ DuidPtr current_duid = cfg.getCurrentDuid();
+ ASSERT_TRUE(current_duid);
+ EXPECT_EQ(*current_duid, *duid);
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/cfg_expiration_unittest.cc b/src/lib/dhcpsrv/tests/cfg_expiration_unittest.cc
new file mode 100644
index 0000000..b7e74d8
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_expiration_unittest.cc
@@ -0,0 +1,433 @@
+// Copyright (C) 2015-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/interval_timer.h>
+#include <dhcpsrv/cfg_expiration.h>
+#include <dhcpsrv/timer_mgr.h>
+#include <exceptions/exceptions.h>
+#include <testutils/test_to_element.h>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+#include <functional>
+#include <stdint.h>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+
+namespace {
+
+/// @brief Type definition of the @c CfgExpiration modified function.
+typedef std::function<void(CfgExpiration*, const int64_t)> ModifierFun;
+/// @brief Type definition of the @c CfgExpiration accessor function
+/// returning uint16_t value.
+typedef std::function<uint16_t(CfgExpiration*)> AccessorFunUint16;
+/// @brief Type definition of the @c CfgExpiration accessor function
+/// returning uint32_t value.
+typedef std::function<uint32_t(CfgExpiration*)> AccessorFunUint32;
+
+/// @brief Tests the accessor and modifier function for a particular
+/// configuration parameter held in @c CfgExpiration.
+///
+/// This is a simple test which tries to set the given parameter to
+/// different values:
+/// - value greater than maximum allowed for this parameter - expects
+/// the exception to be thrown,
+/// - value lower than 0 - expects the exception to be thrown,
+/// - value equal to the maximum allowed,
+/// - value equal to maximum allowed minus 1.
+///
+/// @param limit Maximum allowed value for the parameter.
+/// @param modifier Pointer to the modifier function to be tested.
+/// @param accessor Pointer to the accessor function to be tested.
+/// @tparam ReturnType Type of the value returned by the accessor,
+/// i.e. uint16_t or uint32_t.
+template<typename ReturnType>
+void
+testAccessModify(const int64_t limit, const ModifierFun& modifier,
+ const std::function<ReturnType(CfgExpiration*)>& accessor) {
+ CfgExpiration cfg;
+
+ // Setting the value to maximum allowed + 1 should result in
+ // an exception.
+ ASSERT_THROW(modifier(&cfg, limit + 1), OutOfRange);
+
+ // Setting to the negative value should result in an exception.
+ ASSERT_THROW(modifier(&cfg, -1), OutOfRange);
+
+ // Setting the value to the maximum allowed should pass.
+ ASSERT_NO_THROW(modifier(&cfg, limit));
+ EXPECT_EQ(limit, accessor(&cfg));
+
+ // Setting the value to the maximum allowed - 1 should pass.
+ ASSERT_NO_THROW(modifier(&cfg, limit - 1));
+ EXPECT_EQ(limit - 1, accessor(&cfg));
+
+ // Setting the value to 0 should pass.
+ ASSERT_NO_THROW(modifier(&cfg, 0));
+ EXPECT_EQ(0, accessor(&cfg));
+}
+
+/// @brief Tests that modifier and the accessor returning uint16_t value
+/// work as expected.
+///
+/// @param limit Maximum allowed value for the parameter.
+/// @param modifier Pointer to the modifier function to be tested.
+/// @param accessor Pointer to the accessor function to be tested.
+void
+testAccessModifyUint16(const int64_t limit, const ModifierFun& modifier,
+ const AccessorFunUint16& accessor) {
+ testAccessModify<uint16_t>(limit, modifier, accessor);
+}
+
+/// @brief Tests that modifier and the accessor returning uint32_t value
+/// work as expected.
+///
+/// @param limit Maximum allowed value for the parameter.
+/// @param modifier Pointer to the modifier function to be tested.
+/// @param accessor Pointer to the accessor function to be tested.
+void
+testAccessModifyUint32(const int64_t limit, const ModifierFun& modifier,
+ const AccessorFunUint32& accessor) {
+ testAccessModify<uint32_t>(limit, modifier, accessor);
+}
+
+/// Test the default values of CfgExpiration object.
+TEST(CfgExpirationTest, defaults) {
+ CfgExpiration cfg;
+ EXPECT_EQ(CfgExpiration::DEFAULT_RECLAIM_TIMER_WAIT_TIME,
+ cfg.getReclaimTimerWaitTime());
+ EXPECT_EQ(CfgExpiration::DEFAULT_FLUSH_RECLAIMED_TIMER_WAIT_TIME,
+ cfg.getFlushReclaimedTimerWaitTime());
+ EXPECT_EQ(CfgExpiration::DEFAULT_HOLD_RECLAIMED_TIME,
+ cfg.getHoldReclaimedTime());
+ EXPECT_EQ(CfgExpiration::DEFAULT_MAX_RECLAIM_LEASES,
+ cfg.getMaxReclaimLeases());
+ EXPECT_EQ(CfgExpiration::DEFAULT_MAX_RECLAIM_TIME,
+ cfg.getMaxReclaimTime());
+ EXPECT_EQ(CfgExpiration::DEFAULT_UNWARNED_RECLAIM_CYCLES,
+ cfg.getUnwarnedReclaimCycles());
+}
+
+/// @brief Tests that unparse returns an expected value
+TEST(CfgExpirationTest, unparse) {
+ CfgExpiration cfg;
+ std::string defaults = "{\n"
+ "\"reclaim-timer-wait-time\": 10,\n"
+ "\"flush-reclaimed-timer-wait-time\": 25,\n"
+ "\"hold-reclaimed-time\": 3600,\n"
+ "\"max-reclaim-leases\": 100,\n"
+ "\"max-reclaim-time\": 250,\n"
+ "\"unwarned-reclaim-cycles\": 5 }";
+ isc::test::runToElementTest<CfgExpiration>(defaults, cfg);
+}
+
+// Test the {get,set}ReclaimTimerWaitTime.
+TEST(CfgExpirationTest, getReclaimTimerWaitTime) {
+ testAccessModify<uint16_t>(CfgExpiration::LIMIT_RECLAIM_TIMER_WAIT_TIME,
+ &CfgExpiration::setReclaimTimerWaitTime,
+ &CfgExpiration::getReclaimTimerWaitTime);
+}
+
+// Test the {get,set}FlushReclaimedTimerWaitTime.
+TEST(CfgExpirationTest, getFlushReclaimedTimerWaitTime) {
+ testAccessModifyUint16(CfgExpiration::LIMIT_FLUSH_RECLAIMED_TIMER_WAIT_TIME,
+ &CfgExpiration::setFlushReclaimedTimerWaitTime,
+ &CfgExpiration::getFlushReclaimedTimerWaitTime);
+}
+
+// Test the {get,set}HoldReclaimedTime.
+TEST(CfgExpirationTest, getHoldReclaimedTime) {
+ testAccessModifyUint32(CfgExpiration::LIMIT_HOLD_RECLAIMED_TIME,
+ &CfgExpiration::setHoldReclaimedTime,
+ &CfgExpiration::getHoldReclaimedTime);
+}
+
+// Test the {get,set}MaxReclaimLeases.
+TEST(CfgExpirationTest, getMaxReclaimLeases) {
+ testAccessModifyUint32(CfgExpiration::LIMIT_MAX_RECLAIM_LEASES,
+ &CfgExpiration::setMaxReclaimLeases,
+ &CfgExpiration::getMaxReclaimLeases);
+}
+
+// Test the {get,set}MaxReclaimTime.
+TEST(CfgExpirationTest, getMaxReclaimTime) {
+ testAccessModifyUint16(CfgExpiration::LIMIT_MAX_RECLAIM_TIME,
+ &CfgExpiration::setMaxReclaimTime,
+ &CfgExpiration::getMaxReclaimTime);
+}
+
+// Test the {get,set}UnwarnedReclaimCycles.
+TEST(CfgExpirationTest, getUnwarnedReclaimCycles) {
+ testAccessModifyUint16(CfgExpiration::LIMIT_UNWARNED_RECLAIM_CYCLES,
+ &CfgExpiration::setUnwarnedReclaimCycles,
+ &CfgExpiration::getUnwarnedReclaimCycles);
+}
+
+/// @brief Implements test routines for leases reclamation.
+///
+/// This class implements two routines called by the @c CfgExpiration object
+/// instead of the typical routines for leases' reclamation in the
+/// @c AllocEngine. These methods do not perform the actual reclamation,
+/// but instead they record the number of calls to them and the parameters
+/// with which they were executed. This allows for checking if the
+/// @c CfgExpiration object calls the leases reclamation routine with the
+/// appropriate parameters.
+class LeaseReclamationStub {
+public:
+
+ /// @brief Collection of parameters with which the @c reclaimExpiredLeases
+ /// method is called.
+ ///
+ /// Examination of these values allows for assessment if the @c CfgExpiration
+ /// calls the routine with the appropriate values.
+ struct RecordedParams {
+ /// @brief Maximum number of leases to be processed.
+ size_t max_leases;
+
+ /// @brief Timeout for processing leases in milliseconds.
+ uint16_t timeout;
+
+ /// @brief Boolean flag which indicates if the leases should be removed
+ /// when reclaimed.
+ bool remove_lease;
+
+ /// @brief Maximum number of reclamation attempts after which all leases
+ /// should be reclaimed.
+ uint16_t max_unwarned_cycles;
+
+ /// @brief Constructor
+ ///
+ /// Sets all numeric values to 0xFFFF and the boolean values to false.
+ RecordedParams()
+ : max_leases(0xFFFF), timeout(0xFFFF), remove_lease(false),
+ max_unwarned_cycles(0xFFFF) {
+ }
+ };
+
+ /// @brief Constructor.
+ ///
+ /// Resets recorded parameters and obtains the instance of the @c TimerMgr.
+ LeaseReclamationStub()
+ : reclaim_calls_count_(0), delete_calls_count_(0), reclaim_params_(),
+ secs_param_(0), timer_mgr_(TimerMgr::instance()) {
+ }
+
+ /// @brief Stub implementation of the leases' reclamation routine.
+ ///
+ /// @param max_leases Maximum number of leases to be processed.
+ /// @param timeout Timeout for processing leases in milliseconds.
+ /// @remove_lease Boolean flag which indicates if the leases should be
+ /// removed when it is reclaimed.
+ /// @param Maximum number of reclamation attempts after which all leases
+ /// should be reclaimed.
+ void
+ reclaimExpiredLeases(const size_t max_leases, const uint16_t timeout,
+ const bool remove_lease,
+ const uint16_t max_unwarned_cycles) {
+ // Increase calls counter for this method.
+ ++reclaim_calls_count_;
+ // Record all parameters with which this method has been called.
+ reclaim_params_.max_leases = max_leases;
+ reclaim_params_.timeout = timeout;
+ reclaim_params_.remove_lease = remove_lease;
+ reclaim_params_.max_unwarned_cycles = max_unwarned_cycles;
+
+ // Leases' reclamation routine is responsible for re-scheduling
+ // the timer.
+ timer_mgr_->setup(CfgExpiration::RECLAIM_EXPIRED_TIMER_NAME);
+ }
+
+ /// @brief Stub implementation of the routine which flushes
+ /// expired-reclaimed leases.
+ ///
+ /// @param secs Specifies the minimum amount of time, expressed in
+ /// seconds, that must elapse before the expired-reclaimed lease is
+ /// deleted from the database.
+ void
+ deleteReclaimedLeases(const uint32_t secs) {
+ // Increase calls counter for this method.
+ ++delete_calls_count_;
+ // Record the value of the parameter.
+ secs_param_ = secs;
+
+ // Routine which flushes the reclaimed leases is responsible for
+ // re-scheduling the timer.
+ timer_mgr_->setup(CfgExpiration::FLUSH_RECLAIMED_TIMER_NAME);
+ }
+
+ /// @brief Counter holding the number of calls to @c reclaimExpiredLeases.
+ long reclaim_calls_count_;
+
+ /// @brief Counter holding the number of calls to @c deleteReclaimedLeases.
+ long delete_calls_count_;
+
+ /// @brief Structure holding values of parameters with which the
+ /// @c reclaimExpiredLeases was called.
+ ///
+ /// These values are overridden on subsequent calls to this method.
+ RecordedParams reclaim_params_;
+
+ /// @brief Value of the parameter with which the @c deleteReclaimedLeases
+ /// was called.
+ uint32_t secs_param_;
+
+private:
+
+ /// @brief Pointer to the @c TimerMgr.
+ TimerMgrPtr timer_mgr_;
+
+};
+
+/// @brief Pointer to the @c LeaseReclamationStub.
+typedef boost::shared_ptr<LeaseReclamationStub> LeaseReclamationStubPtr;
+
+/// @brief Test fixture class for the @c CfgExpiration.
+class CfgExpirationTimersTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Creates instance of the test fixture class. Besides initialization
+ /// of the class members, it also stops the @c TimerMgr worker thread
+ /// and removes any registered timers.
+ CfgExpirationTimersTest()
+ : io_service_(new IOService()),
+ timer_mgr_(TimerMgr::instance()),
+ stub_(new LeaseReclamationStub()),
+ cfg_(true) {
+ cleanupTimerMgr();
+ }
+
+ /// @brief Destructor.
+ ///
+ /// It stops the @c TimerMgr worker thread and removes any registered
+ /// timers.
+ virtual ~CfgExpirationTimersTest() {
+ cleanupTimerMgr();
+ }
+
+ /// @brief Stop @c TimerMgr worker thread and remove the timers.
+ void cleanupTimerMgr() const {
+ timer_mgr_->unregisterTimers();
+ timer_mgr_->setIOService(io_service_);
+ }
+
+ /// @brief Runs IOService and stops after a specified time.
+ ///
+ /// @param timeout_ms Amount of time after which the method returns.
+ void runTimersWithTimeout(const long timeout_ms) {
+ IntervalTimer timer(*io_service_);
+ timer.setup([this]() {
+ io_service_->stop();
+ }, timeout_ms, IntervalTimer::ONE_SHOT);
+
+ io_service_->run();
+ io_service_->get_io_service().reset();
+ }
+
+ /// @brief Setup timers according to the configuration and run them
+ /// for the specified amount of time.
+ ///
+ /// @param timeout_ms Timeout in milliseconds.
+ void setupAndRun(const long timeout_ms) {
+ cfg_.setupTimers(&LeaseReclamationStub::reclaimExpiredLeases,
+ &LeaseReclamationStub::deleteReclaimedLeases,
+ stub_.get());
+ // Run timers.
+ ASSERT_NO_THROW({
+ runTimersWithTimeout(timeout_ms);
+ });
+ }
+
+ /// @brief Pointer to the IO service used by the tests.
+ IOServicePtr io_service_;
+
+ /// @brief Pointer to the @c TimerMgr.
+ TimerMgrPtr timer_mgr_;
+
+ /// @brief Pointer to the @c LeaseReclamationStub instance.
+ LeaseReclamationStubPtr stub_;
+
+ /// @brief Instance of the @c CfgExpiration class used by the tests.
+ CfgExpiration cfg_;
+};
+
+// Test that the reclamation routines are called with the appropriate parameters.
+TEST_F(CfgExpirationTimersTest, reclamationParameters) {
+ // Set this value to true, to make sure that the timer callback would
+ // modify this value to false.
+ stub_->reclaim_params_.remove_lease = true;
+
+ // Set parameters to some non-default values.
+ cfg_.setMaxReclaimLeases(1000);
+ cfg_.setMaxReclaimTime(1500);
+ cfg_.setHoldReclaimedTime(1800);
+ cfg_.setUnwarnedReclaimCycles(13);
+
+ // Run timers for 500ms.
+ ASSERT_NO_FATAL_FAILURE(setupAndRun(500));
+
+ // Make sure we had more than one call to the reclamation routine.
+ ASSERT_GT(stub_->reclaim_calls_count_, 1);
+ // Make sure it was called with appropriate arguments.
+ EXPECT_EQ(1000, stub_->reclaim_params_.max_leases);
+ EXPECT_EQ(1500, stub_->reclaim_params_.timeout);
+ EXPECT_FALSE(stub_->reclaim_params_.remove_lease);
+ EXPECT_EQ(13, stub_->reclaim_params_.max_unwarned_cycles);
+
+ // Make sure we had more than one call to the routine which flushes
+ // expired reclaimed leases.
+ ASSERT_GT(stub_->delete_calls_count_, 1);
+ // Make sure that the argument was correct.
+ EXPECT_EQ(1800, stub_->secs_param_);
+}
+
+// This test verifies that if the value of "flush-reclaimed-timer-wait-time"
+// configuration parameter is set to 0, the lease reclamation routine would
+// delete reclaimed leases from a lease database.
+TEST_F(CfgExpirationTimersTest, noLeaseAffinity) {
+ // Set the timer for flushing leases to 0. This effectively disables
+ // the timer.
+ cfg_.setFlushReclaimedTimerWaitTime(0);
+
+ // Run the lease reclamation timer for a while.
+ ASSERT_NO_FATAL_FAILURE(setupAndRun(500));
+
+ // Make sure that the lease reclamation routine has been executed a
+ // couple of times.
+ ASSERT_GT(stub_->reclaim_calls_count_, 1);
+ EXPECT_EQ(CfgExpiration::DEFAULT_MAX_RECLAIM_LEASES,
+ stub_->reclaim_params_.max_leases);
+ EXPECT_EQ(CfgExpiration::DEFAULT_MAX_RECLAIM_TIME,
+ stub_->reclaim_params_.timeout);
+ // When the "flush" timer is disabled, the lease reclamation routine is
+ // responsible for removal of reclaimed leases. This is controlled using
+ // the "remove_lease" parameter which should be set to true in this case.
+ EXPECT_TRUE(stub_->reclaim_params_.remove_lease);
+
+ // The routine flushing reclaimed leases should not be run at all.
+ EXPECT_EQ(0, stub_->delete_calls_count_);
+}
+
+// This test verifies that lease reclamation may be disabled.
+TEST_F(CfgExpirationTimersTest, noLeaseReclamation) {
+ // Disable both timers.
+ cfg_.setReclaimTimerWaitTime(0);
+ cfg_.setFlushReclaimedTimerWaitTime(0);
+
+ // Wait for 500ms.
+ ASSERT_NO_FATAL_FAILURE(setupAndRun(500));
+
+ // Make sure that neither leases' reclamation routine nor the routine
+ // flushing expired-reclaimed leases was run.
+ EXPECT_EQ(0, stub_->reclaim_calls_count_);
+ EXPECT_EQ(0, stub_->delete_calls_count_);
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/cfg_host_operations_unittest.cc b/src/lib/dhcpsrv/tests/cfg_host_operations_unittest.cc
new file mode 100644
index 0000000..0176eb4
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_host_operations_unittest.cc
@@ -0,0 +1,113 @@
+// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcp/dhcp6.h>
+#include <dhcpsrv/cfg_host_operations.h>
+#include <dhcpsrv/host.h>
+#include <testutils/test_to_element.h>
+#include <gtest/gtest.h>
+#include <algorithm>
+#include <iterator>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::test;
+
+namespace {
+
+/// @brief Checks if specified identifier is present.
+///
+/// @param cfg Object holding current configuration.
+/// @param id Identifier type which presence should be checked.
+/// @return true if specified identifier is present.
+bool
+identifierPresent(const CfgHostOperations& cfg, const Host::IdentifierType& id) {
+ CfgHostOperations::IdentifierTypes types = cfg.getIdentifierTypes();
+ return (std::find(types.begin(), types.end(), id) != types.end());
+}
+
+/// @brief Checks if specified identifier is at specified position.
+///
+/// @param cfg Object holding current configuration.
+/// @param id Identifier type which presence should be checked.
+/// @param pos Position at which the identifier is expected on the list.
+/// @return true if specified identifier exists at specified position.
+bool
+identifierAtPosition(const CfgHostOperations& cfg, const Host::IdentifierType& id,
+ const size_t pos) {
+ CfgHostOperations::IdentifierTypes types = cfg.getIdentifierTypes();
+ if (types.size() > pos) {
+ CfgHostOperations::IdentifierTypes::const_iterator type = types.begin();
+ std::advance(type, pos);
+ return (*type == id);
+ }
+ return (false);
+}
+
+// This test checks that the list of identifiers is initially
+// empty.
+TEST(CfgHostOperationsTest, defaults) {
+ CfgHostOperations cfg;
+ EXPECT_TRUE(cfg.getIdentifierTypes().empty());
+ runToElementTest<CfgHostOperations>("[ ]", cfg);
+}
+
+// This test verifies that identifier types can be added into an
+// ordered collection and then removed.
+TEST(CfgHostOperationsTest, addIdentifier) {
+ CfgHostOperations cfg;
+
+ // Only HW address added.
+ ASSERT_NO_THROW(cfg.addIdentifierType("hw-address"));
+ EXPECT_TRUE(identifierAtPosition(cfg, Host::IDENT_HWADDR, 0));
+ EXPECT_FALSE(identifierPresent(cfg, Host::IDENT_DUID));
+ EXPECT_FALSE(identifierPresent(cfg, Host::IDENT_CIRCUIT_ID));
+
+ // HW address and DUID should be present.
+ ASSERT_NO_THROW(cfg.addIdentifierType("duid"));
+ EXPECT_TRUE(identifierAtPosition(cfg, Host::IDENT_HWADDR, 0));
+ EXPECT_TRUE(identifierAtPosition(cfg, Host::IDENT_DUID, 1));
+ EXPECT_FALSE(identifierPresent(cfg, Host::IDENT_CIRCUIT_ID));
+
+ // All three identifiers should be present now.
+ ASSERT_NO_THROW(cfg.addIdentifierType("circuit-id"));
+ EXPECT_TRUE(identifierAtPosition(cfg, Host::IDENT_HWADDR, 0));
+ EXPECT_TRUE(identifierAtPosition(cfg, Host::IDENT_DUID, 1));
+ EXPECT_TRUE(identifierAtPosition(cfg, Host::IDENT_CIRCUIT_ID, 2));
+
+ // Check unparse
+ std::string ids = "[ \"hw-address\", \"duid\", \"circuit-id\" ]";
+ runToElementTest<CfgHostOperations>(ids, cfg);
+
+ // Let's clear and make sure no identifiers are present.
+ ASSERT_NO_THROW(cfg.clearIdentifierTypes());
+ EXPECT_TRUE(cfg.getIdentifierTypes().empty());
+ runToElementTest<CfgHostOperations>("[ ]", cfg);
+}
+
+// This test verifies that the default DHCPv4 configuration is created
+// as expected.
+TEST(CfgHostOperationsTest, createConfig4) {
+ CfgHostOperationsPtr cfg = CfgHostOperations::createConfig4();
+
+ EXPECT_TRUE(identifierAtPosition(*cfg, Host::IDENT_HWADDR, 0));
+ EXPECT_TRUE(identifierAtPosition(*cfg, Host::IDENT_DUID, 1));
+ EXPECT_TRUE(identifierAtPosition(*cfg, Host::IDENT_CIRCUIT_ID, 2));
+ EXPECT_TRUE(identifierAtPosition(*cfg, Host::IDENT_CLIENT_ID, 3));
+}
+
+// This test verifies that the default DHCPv6 configuration is created
+// as expected.
+TEST(CfgHostOperationsTest, createConfig6) {
+ CfgHostOperationsPtr cfg = CfgHostOperations::createConfig6();
+
+ EXPECT_TRUE(identifierAtPosition(*cfg, Host::IDENT_HWADDR, 0));
+ EXPECT_TRUE(identifierAtPosition(*cfg, Host::IDENT_DUID, 1));
+ EXPECT_FALSE(identifierPresent(*cfg, Host::IDENT_CIRCUIT_ID));
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/cfg_hosts_unittest.cc b/src/lib/dhcpsrv/tests/cfg_hosts_unittest.cc
new file mode 100644
index 0000000..a4cb460
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_hosts_unittest.cc
@@ -0,0 +1,1204 @@
+// Copyright (C) 2014-2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
+#include <dhcpsrv/cfg_hosts.h>
+#include <dhcpsrv/cfg_hosts_util.h>
+#include <dhcpsrv/host.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <gtest/gtest.h>
+#include <sstream>
+#include <set>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::asiolink;
+
+namespace {
+
+/// @brief Test fixture class for testing @c CfgHost object holding
+/// host reservations specified in the configuration file.
+class CfgHostsTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// This constructor allocates a collection of @c HWAddr and @c DuidPtr
+ /// objects used by the unit tests.
+ ///
+ /// The allocated HW addresses use the following pattern: 01:02:0A:BB:03:XX
+ /// where XX is a number between 0 and 0x32. All of them are of the
+ /// HTYPE_ETHER type.
+ ///
+ /// The allocated DUID LLTs use the following pattern:
+ /// 01:02:03:04:05:06:07:08:09:0A:XX where the XX is a number between
+ /// 0 and 0x32.
+ CfgHostsTest();
+
+ /// @brief Destructor.
+ ///
+ /// This destructor resets global state after tests are run.
+ ~CfgHostsTest();
+
+ /// @brief Increases last byte of an address.
+ ///
+ /// @param address Address to be increased.
+ IOAddress increase(const IOAddress& address, const uint8_t num) const;
+
+ /// @brief Collection of HW address objects allocated for unit tests.
+ std::vector<HWAddrPtr> hwaddrs_;
+ /// @brief Collection of DUIDs allocated for unit tests.
+ std::vector<DuidPtr> duids_;
+ /// @brief Collection of IPv4 address objects allocated for unit tests.
+ std::vector<IOAddress> addressesa_;
+ std::vector<IOAddress> addressesb_;
+};
+
+CfgHostsTest::CfgHostsTest() {
+ const uint8_t mac_template[] = {
+ 0x01, 0x02, 0x0A, 0xBB, 0x03, 0x00
+ };
+ for (unsigned i = 0; i < 50; ++i) {
+ std::vector<uint8_t> vec(mac_template,
+ mac_template + sizeof(mac_template));
+ vec[vec.size() - 1] = i;
+ HWAddrPtr hwaddr(new HWAddr(vec, HTYPE_ETHER));
+ hwaddrs_.push_back(hwaddr);
+ }
+
+ const uint8_t duid_template[] = {
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x00
+ };
+ for (unsigned i = 0; i < 50; ++i) {
+ std::vector<uint8_t> vec(duid_template,
+ duid_template + sizeof(mac_template));
+ vec[vec.size() - 1] = i;
+ DuidPtr duid(new DUID(vec));
+ duids_.push_back(duid);
+ }
+
+ const uint32_t addra_template = 0xc0000205; // 192.0.2.5
+ const uint32_t addrb_template = 0xc00a020a; // 192.10.2.10
+ for (unsigned i = 0; i < 50; ++i) {
+ IOAddress addra(addra_template + i);
+ addressesa_.push_back(addra);
+ IOAddress addrb(addrb_template + i);
+ addressesb_.push_back(addrb);
+ }
+}
+
+CfgHostsTest::~CfgHostsTest() {
+ CfgMgr::instance().setFamily(AF_INET);
+}
+
+IOAddress
+CfgHostsTest::increase(const IOAddress& address, const uint8_t num) const {
+ std::vector<uint8_t> vec = address.toBytes();
+ if (!vec.empty()) {
+ vec[vec.size() - 1] += num;
+ return (IOAddress::fromBytes(address.getFamily(), &vec[0]));
+ }
+ return (address);
+}
+
+// This test checks that hosts with unique HW addresses and DUIDs can be
+// retrieved from the host configuration.
+TEST_F(CfgHostsTest, getAllNonRepeatingHosts) {
+ CfgHosts cfg;
+ // Add 25 hosts identified by HW address and 25 hosts identified by
+ // DUID. They are added to different subnets.
+ for (unsigned i = 0; i < 25; ++i) {
+ cfg.add(HostPtr(new Host(hwaddrs_[i]->toText(false),
+ "hw-address",
+ SubnetID(i % 10 + 1), SubnetID(i % 5 + 1),
+ addressesa_[i])));
+
+ cfg.add(HostPtr(new Host(duids_[i]->toText(), "duid",
+ SubnetID(i % 5 + 1), SubnetID(i % 10 + 1),
+ addressesb_[i])));
+
+ }
+
+ // Try to retrieve each added reservation using HW address and DUID. Do it
+ // in the reverse order to make sure that the order doesn't matter.
+ for (int i = 24; i >= 0; --i) {
+ // Get host identified by HW address.
+ HostCollection hosts = cfg.getAll(Host::IDENT_HWADDR,
+ &hwaddrs_[i]->hwaddr_[0],
+ hwaddrs_[i]->hwaddr_.size());
+ ASSERT_EQ(1, hosts.size());
+ EXPECT_EQ(i % 10 + 1, hosts[0]->getIPv4SubnetID());
+ EXPECT_EQ(addressesa_[i].toText(),
+ hosts[0]->getIPv4Reservation().toText());
+
+ // Get host identified by DUID.
+ hosts = cfg.getAll(Host::IDENT_DUID,
+ &duids_[i]->getDuid()[0],
+ duids_[i]->getDuid().size());
+ ASSERT_EQ(1, hosts.size());
+ EXPECT_EQ(i % 5 + 1, hosts[0]->getIPv4SubnetID());
+ EXPECT_EQ(addressesb_[i].toText(),
+ hosts[0]->getIPv4Reservation().toText());
+ }
+
+ // Make sure that the reservations do not exist for the hardware addresses
+ // and DUIDs from the range of 25 to 49.
+ for (int i = 49; i >= 25; --i) {
+ EXPECT_TRUE(cfg.getAll(Host::IDENT_HWADDR, &hwaddrs_[i]->hwaddr_[0],
+ hwaddrs_[i]->hwaddr_.size()).empty());
+ EXPECT_TRUE(cfg.getAll(Host::IDENT_DUID, &duids_[i]->getDuid()[0],
+ duids_[i]->getDuid().size()).empty());
+ }
+}
+
+// This test verifies that the host can be added to multiple subnets and
+// that the getAll message retrieves all instances of the host.
+TEST_F(CfgHostsTest, getAllRepeatingHosts) {
+ CfgHosts cfg;
+ // Add hosts.
+ for (unsigned i = 0; i < 25; ++i) {
+ // Add two hosts, using the same HW address to two distinct subnets.
+ cfg.add(HostPtr(new Host(hwaddrs_[i]->toText(false),
+ "hw-address",
+ SubnetID(1), SubnetID(2),
+ addressesa_[i])));
+ cfg.add(HostPtr(new Host(hwaddrs_[i]->toText(false),
+ "hw-address",
+ SubnetID(2), SubnetID(3),
+ addressesb_[i])));
+
+ // Add two hosts, using the same DUID to two distinct subnets.
+ cfg.add(HostPtr(new Host(duids_[i]->toText(), "duid",
+ SubnetID(1), SubnetID(2),
+ addressesb_[i])));
+ cfg.add(HostPtr(new Host(duids_[i]->toText(), "duid",
+ SubnetID(2), SubnetID(3),
+ addressesa_[i])));
+ }
+
+ // Verify that hosts can be retrieved.
+ for (unsigned i = 0; i < 25; ++i) {
+ // Get host by HW address.
+ HostCollection hosts = cfg.getAll(Host::IDENT_HWADDR,
+ &hwaddrs_[i]->hwaddr_[0],
+ hwaddrs_[i]->hwaddr_.size());
+ ASSERT_EQ(2, hosts.size());
+ EXPECT_EQ(1, hosts[0]->getIPv4SubnetID());
+ EXPECT_EQ(addressesa_[i], hosts[0]->getIPv4Reservation().toText());
+ EXPECT_EQ(2, hosts[1]->getIPv4SubnetID());
+ EXPECT_EQ(addressesb_[i], hosts[1]->getIPv4Reservation().toText());
+
+ // The HW address is non-null but there are no reservations
+ // for the HW addresses from the range of 25 to 49.
+ hosts = cfg.getAll(Host::IDENT_HWADDR,
+ &hwaddrs_[i + 25]->hwaddr_[0],
+ hwaddrs_[i + 25]->hwaddr_.size());
+ EXPECT_TRUE(hosts.empty());
+
+ // Get host by DUID.
+ hosts = cfg.getAll(Host::IDENT_DUID,
+ &duids_[i]->getDuid()[0],
+ duids_[i]->getDuid().size());
+
+ // The DUID is non-null but there are no reservations
+ // for the DUIDs from the range of 25 to 49.
+ hosts = cfg.getAll(Host::IDENT_DUID,
+ &duids_[i + 25]->getDuid()[0],
+ duids_[i + 25]->getDuid().size());
+ EXPECT_TRUE(hosts.empty());
+ }
+}
+
+// This test checks that hosts in the same subnet can be retrieved from
+// the host configuration.
+TEST_F(CfgHostsTest, getAll4BySubnet) {
+ CfgHosts cfg;
+ // Add 25 hosts identified by HW address in the same subnet.
+ for (unsigned i = 0; i < 25; ++i) {
+ cfg.add(HostPtr(new Host(hwaddrs_[i]->toText(false),
+ "hw-address",
+ SubnetID(1), SubnetID(1),
+ addressesa_[i])));
+ }
+
+ // Check that other subnets are empty.
+ HostCollection hosts = cfg.getAll4(SubnetID(100));
+ EXPECT_EQ(0, hosts.size());
+
+ // Try to retrieve all added reservations.
+ hosts = cfg.getAll4(SubnetID(1));
+ ASSERT_EQ(25, hosts.size());
+ for (unsigned i = 0; i < 25; ++i) {
+ EXPECT_EQ(1, hosts[i]->getIPv4SubnetID());
+ EXPECT_EQ(addressesa_[i].toText(),
+ hosts[i]->getIPv4Reservation().toText());
+ }
+}
+
+// This test checks that hosts in the same subnet can be retrieved from
+// the host configuration.
+TEST_F(CfgHostsTest, getAll6BySubnet) {
+ CfgHosts cfg;
+ // Add 25 hosts identified by DUID in the same subnet.
+ for (unsigned i = 0; i < 25; ++i) {
+ HostPtr host = HostPtr(new Host(duids_[i]->toText(), "duid",
+ SubnetID(1), SubnetID(1),
+ IOAddress("0.0.0.0")));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ increase(IOAddress("2001:db8:1::1"),
+ i)));
+ cfg.add(host);
+ }
+
+ // Check that other subnets are empty.
+ HostCollection hosts = cfg.getAll6(SubnetID(100));
+ EXPECT_EQ(0, hosts.size());
+
+ // Try to retrieve all added reservations.
+ hosts = cfg.getAll6(SubnetID(1));
+ ASSERT_EQ(25, hosts.size());
+ for (unsigned i = 0; i < 25; ++i) {
+ EXPECT_EQ(1, hosts[i]->getIPv6SubnetID());
+ IPv6ResrvRange reservations =
+ hosts[i]->getIPv6Reservations(IPv6Resrv::TYPE_NA);
+ ASSERT_EQ(1, std::distance(reservations.first, reservations.second));
+ EXPECT_EQ(increase(IOAddress("2001:db8:1::1"), i),
+ reservations.first->second.getPrefix());
+ }
+}
+
+// This test checks that hosts in the same subnet can be retrieved from
+// the host configuration by pages.
+TEST_F(CfgHostsTest, getPage4) {
+ CfgHosts cfg;
+ // Add 25 hosts identified by DUID in the same subnet.
+ for (unsigned i = 0; i < 25; ++i) {
+ cfg.add(HostPtr(new Host(duids_[i]->toText(), "duid",
+ SubnetID(1), SubnetID(1),
+ addressesa_[i])));
+ }
+ size_t idx(0);
+ uint64_t host_id(0);
+ HostPageSize page_size(10);
+
+ // Check that other subnets are empty.
+ HostCollection page = cfg.getPage4(SubnetID(100), idx, host_id, page_size);
+ EXPECT_EQ(0, page.size());
+
+ // Try to retrieve all added reservations.
+ // Get first page.
+ page = cfg.getPage4(SubnetID(1), idx, host_id, page_size);
+ EXPECT_EQ(10, page.size());
+ host_id = page[9]->getHostId();
+
+ // Get second and last pages.
+ page = cfg.getPage4(SubnetID(1), idx, host_id, page_size);
+ EXPECT_EQ(10, page.size());
+ host_id = page[9]->getHostId();
+ page = cfg.getPage4(SubnetID(1), idx, host_id, page_size);
+ EXPECT_EQ(5, page.size());
+ host_id = page[4]->getHostId();
+
+ // Verify we have everything.
+ page = cfg.getPage4(SubnetID(1), idx, host_id, page_size);
+ EXPECT_EQ(0, page.size());
+}
+
+// This test checks that hosts in the same subnet can be retrieved from
+// the host configuration by pages.
+TEST_F(CfgHostsTest, getPage6) {
+ CfgHosts cfg;
+ // Add 25 hosts identified by HW address in the same subnet.
+ for (unsigned i = 0; i < 25; ++i) {
+ HostPtr host = HostPtr(new Host(hwaddrs_[i]->toText(false),
+ "hw-address",
+ SubnetID(1), SubnetID(1),
+ IOAddress("0.0.0.0")));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ increase(IOAddress("2001:db8:1::1"),
+ i)));
+ cfg.add(host);
+ }
+ size_t idx(0);
+ uint64_t host_id(0);
+ HostPageSize page_size(10);
+
+ // Check that other subnets are empty.
+ HostCollection page = cfg.getPage6(SubnetID(100), idx, host_id, page_size);
+ EXPECT_EQ(0, page.size());
+
+ // Try to retrieve all added reservations.
+ // Get first page.
+ page = cfg.getPage6(SubnetID(1), idx, host_id, page_size);
+ EXPECT_EQ(10, page.size());
+ host_id = page[9]->getHostId();
+
+ // Get second and last pages.
+ page = cfg.getPage6(SubnetID(1), idx, host_id, page_size);
+ EXPECT_EQ(10, page.size());
+ host_id = page[9]->getHostId();
+ page = cfg.getPage6(SubnetID(1), idx, host_id, page_size);
+ EXPECT_EQ(5, page.size());
+ host_id = page[4]->getHostId();
+
+ // Verify we have everything.
+ page = cfg.getPage6(SubnetID(1), idx, host_id, page_size);
+ EXPECT_EQ(0, page.size());
+}
+
+// This test checks that all hosts can be retrieved from the host
+// configuration by pages.
+TEST_F(CfgHostsTest, getPage4All) {
+ CfgHosts cfg;
+ // Add 25 hosts identified by DUID.
+ for (unsigned i = 0; i < 25; ++i) {
+ cfg.add(HostPtr(new Host(duids_[i]->toText(), "duid",
+ SubnetID(i), SubnetID(i),
+ addressesa_[i])));
+ }
+ size_t idx(0);
+ uint64_t host_id(0);
+ HostPageSize page_size(10);
+
+ // Try to retrieve all added reservations.
+ // Get first page.
+ HostCollection page = cfg.getPage4(idx, host_id, page_size);
+ EXPECT_EQ(10, page.size());
+ host_id = page[9]->getHostId();
+
+ // Get second and last pages.
+ page = cfg.getPage4(idx, host_id, page_size);
+ EXPECT_EQ(10, page.size());
+ host_id = page[9]->getHostId();
+ page = cfg.getPage4(idx, host_id, page_size);
+ EXPECT_EQ(5, page.size());
+ host_id = page[4]->getHostId();
+
+ // Verify we have everything.
+ page = cfg.getPage4(idx, host_id, page_size);
+ EXPECT_EQ(0, page.size());
+}
+
+// This test checks that all hosts can be retrieved from the host
+// configuration by pages.
+TEST_F(CfgHostsTest, getPage6All) {
+ CfgHosts cfg;
+ // Add 25 hosts identified by HW address.
+ for (unsigned i = 0; i < 25; ++i) {
+ HostPtr host = HostPtr(new Host(hwaddrs_[i]->toText(false),
+ "hw-address",
+ SubnetID(i), SubnetID(i),
+ IOAddress("0.0.0.0")));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ increase(IOAddress("2001:db8:1::1"),
+ i)));
+ cfg.add(host);
+ }
+ size_t idx(0);
+ uint64_t host_id(0);
+ HostPageSize page_size(10);
+
+ // Try to retrieve all added reservations.
+ // Get first page.
+ HostCollection page = cfg.getPage6(idx, host_id, page_size);
+ EXPECT_EQ(10, page.size());
+ host_id = page[9]->getHostId();
+
+ // Get second and last pages.
+ page = cfg.getPage6(idx, host_id, page_size);
+ EXPECT_EQ(10, page.size());
+ host_id = page[9]->getHostId();
+ page = cfg.getPage6(idx, host_id, page_size);
+ EXPECT_EQ(5, page.size());
+ host_id = page[4]->getHostId();
+
+ // Verify we have everything.
+ page = cfg.getPage6(idx, host_id, page_size);
+ EXPECT_EQ(0, page.size());
+}
+
+// This test checks that all reservations for the specified IPv4 address can
+// be retrieved.
+TEST_F(CfgHostsTest, getAll4ByAddress) {
+ CfgHosts cfg;
+ // Add hosts.
+ for (unsigned i = 0; i < 25; ++i) {
+ // Add host identified by the HW address.
+ cfg.add(HostPtr(new Host(hwaddrs_[i]->toText(false),
+ "hw-address",
+ SubnetID(1 + i), SUBNET_ID_UNUSED,
+ IOAddress("192.0.2.5"))));
+ // Add host identified by the DUID.
+ cfg.add(HostPtr(new Host(duids_[i]->toText(),
+ "duid",
+ SubnetID(1 + i), SUBNET_ID_UNUSED,
+ IOAddress("192.0.2.10"))));
+ }
+
+ HostCollection hosts = cfg.getAll4(IOAddress("192.0.2.10"));
+ std::set<uint32_t> subnet_ids;
+ for (HostCollection::const_iterator host = hosts.begin(); host != hosts.end();
+ ++host) {
+ subnet_ids.insert((*host)->getIPv4SubnetID());
+ }
+ ASSERT_EQ(25, subnet_ids.size());
+ EXPECT_EQ(1, *subnet_ids.begin());
+ EXPECT_EQ(25, *subnet_ids.rbegin());
+}
+
+// This test checks that all reservations for the specified IPv4 subnet can
+// be deleted.
+TEST_F(CfgHostsTest, deleteAll4) {
+ CfgHosts cfg;
+ // Add hosts.
+ for (unsigned i = 0; i < 25; ++i) {
+ // Hosts will differ by hostname. It is easier than differentiating by
+ // IPv4 address because if they all have zero IPv4 address it is
+ // easier to retrieve all of them to check the host counts.
+ std::ostringstream s;
+ s << "hostname" << i;
+
+ // Add host identified by the HW address.
+ cfg.add(HostPtr(new Host(hwaddrs_[i]->toText(false),
+ "hw-address",
+ SubnetID(1 + i % 2), SUBNET_ID_UNUSED,
+ IOAddress::IPV4_ZERO_ADDRESS(),
+ "hostname")));
+ }
+
+ // Get all inserted hosts.
+ HostCollection hosts = cfg.getAll4(IOAddress::IPV4_ZERO_ADDRESS());
+ std::set<uint32_t> subnet_ids;
+ for (HostCollection::const_iterator host = hosts.begin(); host != hosts.end();
+ ++host) {
+ subnet_ids.insert((*host)->getIPv4SubnetID());
+ }
+ // Make sure there are two unique subnets: 1 and 2.
+ ASSERT_EQ(2, subnet_ids.size());
+ EXPECT_EQ(1, *subnet_ids.begin());
+ EXPECT_EQ(2, *subnet_ids.rbegin());
+
+ // Delete all hosts for subnet id 2. There should be 12 of them.
+ EXPECT_EQ(12, cfg.delAll4(SubnetID(2)));
+
+ // Gather the host counts again.
+ subnet_ids.clear();
+ hosts = cfg.getAll4(IOAddress::IPV4_ZERO_ADDRESS());
+ for (HostCollection::const_iterator host = hosts.begin(); host != hosts.end();
+ ++host) {
+ subnet_ids.insert((*host)->getIPv4SubnetID());
+ }
+ // We should only have hosts for one subnet and it should be the subnet
+ // with ID of 1.
+ ASSERT_EQ(1, subnet_ids.size());
+ EXPECT_EQ(1, *subnet_ids.begin());
+}
+
+// This test checks that the reservations can be retrieved for the particular
+// host connected to the specific IPv4 subnet (by subnet id).
+TEST_F(CfgHostsTest, get4) {
+ CfgHosts cfg;
+ // Add hosts.
+ for (unsigned i = 0; i < 25; ++i) {
+ // Add host identified by HW address.
+ cfg.add(HostPtr(new Host(hwaddrs_[i]->toText(false),
+ "hw-address",
+ SubnetID(1 + i % 2), SubnetID(13),
+ increase(IOAddress("192.0.2.5"), i))));
+
+ // Add host identified by DUID.
+ cfg.add(HostPtr(new Host(duids_[i]->toText(), "duid",
+ SubnetID(1 + i % 2), SubnetID(13),
+ increase(IOAddress("192.0.2.100"), i))));
+ }
+
+ for (unsigned i = 0; i < 25; ++i) {
+ // Retrieve host by HW address.
+ HostPtr host = cfg.get4(SubnetID(1 + i % 2), Host::IDENT_HWADDR,
+ &hwaddrs_[i]->hwaddr_[0],
+ hwaddrs_[i]->hwaddr_.size());
+ ASSERT_TRUE(host);
+ EXPECT_EQ(1 + i % 2, host->getIPv4SubnetID());
+ EXPECT_EQ(increase(IOAddress("192.0.2.5"), i),
+ host->getIPv4Reservation());
+
+ // Retrieve host by DUID.
+ host = cfg.get4(SubnetID(1 + i % 2), Host::IDENT_DUID,
+ &duids_[i]->getDuid()[0], duids_[i]->getDuid().size());
+ ASSERT_TRUE(host);
+ EXPECT_EQ(1 + i % 2, host->getIPv4SubnetID());
+ EXPECT_EQ(increase(IOAddress("192.0.2.100"), i),
+ host->getIPv4Reservation());
+
+ }
+}
+
+// This test checks that the DHCPv4 reservations can be unparsed
+TEST_F(CfgHostsTest, unparsed4) {
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHosts cfg;
+ CfgHostsList list;
+ // Add hosts.
+ for (unsigned i = 0; i < 25; ++i) {
+ // Add host identified by HW address.
+ cfg.add(HostPtr(new Host(hwaddrs_[i]->toText(false),
+ "hw-address",
+ SubnetID(1 + i), SubnetID(13),
+ increase(IOAddress("192.0.2.5"), i))));
+
+ // Add host identified by DUID.
+ cfg.add(HostPtr(new Host(duids_[i]->toText(), "duid",
+ SubnetID(1 + i), SubnetID(13),
+ increase(IOAddress("192.0.2.100"), i))));
+ }
+
+ using namespace isc::data;
+ ConstElementPtr cfg_unparsed;
+ ASSERT_NO_THROW(cfg_unparsed = cfg.toElement());
+ ASSERT_NO_THROW(list.internalize(cfg_unparsed));
+ for (unsigned i = 0; i < 25; ++i) {
+ ConstElementPtr unparsed = list.get(SubnetID(1 + i));
+ ASSERT_TRUE(unparsed);
+ ASSERT_EQ(Element::list, unparsed->getType());
+ EXPECT_EQ(2, unparsed->size());
+ ASSERT_NE(0, unparsed->size());
+
+ // Check by HW address entries
+ bool checked_hw = false;
+ for (unsigned j = 0; j < unparsed->size(); ++j) {
+ ConstElementPtr host = unparsed->get(j);
+ ASSERT_TRUE(host);
+ ASSERT_EQ(Element::map, host->getType());
+ if (!host->contains("hw-address")) {
+ continue;
+ }
+ checked_hw = true;
+ // Not both hw-address and duid
+ EXPECT_FALSE(host->contains("duid"));
+ // Check the HW address
+ ConstElementPtr hw = host->get("hw-address");
+ ASSERT_TRUE(hw);
+ ASSERT_EQ(Element::string, hw->getType());
+ EXPECT_EQ(hwaddrs_[i]->toText(false), hw->stringValue());
+ // Check the reservation
+ ConstElementPtr resv = host->get("ip-address");
+ ASSERT_TRUE(resv);
+ ASSERT_EQ(Element::string, resv->getType());
+ EXPECT_EQ(increase(IOAddress("192.0.2.5"), i),
+ IOAddress(resv->stringValue()));
+ }
+ ASSERT_TRUE(checked_hw);
+
+ // Check by DUID entries
+ bool checked_duid = false;
+ for (unsigned j = 0; j < unparsed->size(); ++j) {
+ ConstElementPtr host = unparsed->get(j);
+ ASSERT_TRUE(host);
+ ASSERT_EQ(Element::map, host->getType());
+ if (!host->contains("duid")) {
+ continue;
+ }
+ checked_duid = true;
+ // Not both hw-address and duid
+ EXPECT_FALSE(host->contains("hw-address"));
+ // Check the DUID
+ ConstElementPtr duid = host->get("duid");
+ ASSERT_TRUE(duid);
+ ASSERT_EQ(Element::string, duid->getType());
+ EXPECT_EQ(duids_[i]->toText(), duid->stringValue());
+ // Check the reservation
+ ConstElementPtr resv = host->get("ip-address");
+ ASSERT_TRUE(resv);
+ ASSERT_EQ(Element::string, resv->getType());
+ EXPECT_EQ(increase(IOAddress("192.0.2.100"), i),
+ IOAddress(resv->stringValue()));
+ }
+ ASSERT_TRUE(checked_duid);
+ }
+}
+
+// This test checks that the reservations can be retrieved for the particular
+// host connected to the specific IPv6 subnet (by subnet id).
+TEST_F(CfgHostsTest, get6) {
+ CfgHosts cfg;
+ // Add hosts.
+ for (unsigned i = 0; i < 25; ++i) {
+ // Add host identified by HW address.
+ HostPtr host = HostPtr(new Host(hwaddrs_[i]->toText(false),
+ "hw-address",
+ SubnetID(10), SubnetID(1 + i % 2),
+ IOAddress("0.0.0.0")));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ increase(IOAddress("2001:db8:1::1"),
+ i)));
+ cfg.add(host);
+
+ // Add host identified by DUID.
+ host = HostPtr(new Host(duids_[i]->toText(), "duid",
+ SubnetID(10), SubnetID(1 + i % 2),
+ IOAddress("0.0.0.0")));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ increase(IOAddress("2001:db8:2::1"),
+ i)));
+ cfg.add(host);
+ }
+
+ for (unsigned i = 0; i < 25; ++i) {
+ // Retrieve host by HW address.
+ HostPtr host = cfg.get6(SubnetID(1 + i % 2), Host::IDENT_HWADDR,
+ &hwaddrs_[i]->hwaddr_[0],
+ hwaddrs_[i]->hwaddr_.size());
+ ASSERT_TRUE(host);
+ EXPECT_EQ(1 + i % 2, host->getIPv6SubnetID());
+ IPv6ResrvRange reservations =
+ host->getIPv6Reservations(IPv6Resrv::TYPE_NA);
+ ASSERT_EQ(1, std::distance(reservations.first, reservations.second));
+ EXPECT_EQ(increase(IOAddress("2001:db8:1::1"), i),
+ reservations.first->second.getPrefix());
+
+ // Retrieve host by DUID.
+ host = cfg.get6(SubnetID(1 + i % 2), Host::IDENT_DUID,
+ &duids_[i]->getDuid()[0], duids_[i]->getDuid().size());
+ ASSERT_TRUE(host);
+ EXPECT_EQ(1 + i % 2, host->getIPv6SubnetID());
+ reservations = host->getIPv6Reservations(IPv6Resrv::TYPE_NA);
+ ASSERT_EQ(1, std::distance(reservations.first, reservations.second));
+ EXPECT_EQ(increase(IOAddress("2001:db8:2::1"), i),
+ reservations.first->second.getPrefix());
+ }
+}
+
+// This test checks that all reservations for the specified IPv6 subnet can
+// be deleted.
+TEST_F(CfgHostsTest, deleteAll6) {
+ CfgHosts cfg;
+ // Add hosts.
+ for (unsigned i = 0; i < 25; ++i) {
+ // Add host identified by HW address. The subnet for which we're
+ // adding the host has id of 1 for even values of i and 2 for
+ // odd values of i.
+ HostPtr host = HostPtr(new Host(hwaddrs_[i]->toText(false),
+ "hw-address",
+ SubnetID(10), SubnetID(1 + i % 2),
+ IOAddress("0.0.0.0")));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ increase(IOAddress("2001:db8:1::1"),
+ i)));
+ cfg.add(host);
+ }
+
+ // Delete all hosts for subnet id. There should be 13 of them.
+ EXPECT_EQ(13, cfg.delAll6(SubnetID(1)));
+
+ for (unsigned i = 0; i < 25; ++i) {
+ // Calculate subnet id for the given i.
+ SubnetID subnet_id = 1 + i % 2;
+
+ // Try to retrieve host by HW address.
+ HostPtr host = cfg.get6(subnet_id, Host::IDENT_HWADDR,
+ &hwaddrs_[i]->hwaddr_[0],
+ hwaddrs_[i]->hwaddr_.size());
+ // The host should exist for subnet id of 2.
+ if (subnet_id == 2) {
+ ASSERT_TRUE(host);
+ EXPECT_EQ(subnet_id, host->getIPv6SubnetID());
+ IPv6ResrvRange reservations =
+ host->getIPv6Reservations(IPv6Resrv::TYPE_NA);
+ ASSERT_EQ(1, std::distance(reservations.first, reservations.second));
+ EXPECT_EQ(increase(IOAddress("2001:db8:1::1"), i),
+ reservations.first->second.getPrefix());
+
+ } else {
+ // All hosts for subnet id 2 should be gone.
+ EXPECT_FALSE(host);
+ }
+ }
+}
+
+// This test checks that the DHCPv6 reservations can be unparsed
+TEST_F(CfgHostsTest, unparse6) {
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHosts cfg;
+ CfgHostsList list;
+ // Add hosts.
+ for (unsigned i = 0; i < 25; ++i) {
+ // Add host identified by HW address.
+ HostPtr host = HostPtr(new Host(hwaddrs_[i]->toText(false),
+ "hw-address",
+ SubnetID(10), SubnetID(1 + i),
+ IOAddress("0.0.0.0")));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ increase(IOAddress("2001:db8:1::1"),
+ i)));
+ cfg.add(host);
+
+ // Add host identified by DUID.
+ host = HostPtr(new Host(duids_[i]->toText(), "duid",
+ SubnetID(10), SubnetID(1 + i),
+ IOAddress("0.0.0.0")));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ increase(IOAddress("2001:db8:2::1"),
+ i)));
+ cfg.add(host);
+ }
+
+ using namespace isc::data;
+ ConstElementPtr cfg_unparsed;
+ ASSERT_NO_THROW(cfg_unparsed = cfg.toElement());
+ ASSERT_NO_THROW(list.internalize(cfg_unparsed));
+ for (unsigned i = 0; i < 25; ++i) {
+ ConstElementPtr unparsed = list.get(SubnetID(1 + i));
+ ASSERT_TRUE(unparsed);
+ ASSERT_EQ(Element::list, unparsed->getType());
+ EXPECT_EQ(2, unparsed->size());
+ ASSERT_NE(0, unparsed->size());
+
+ // Check by HW address entries
+ bool checked_hw = false;
+ for (unsigned j = 0; j < unparsed->size(); ++j) {
+ ConstElementPtr host = unparsed->get(j);
+ ASSERT_TRUE(host);
+ ASSERT_EQ(Element::map, host->getType());
+ if (!host->contains("hw-address")) {
+ continue;
+ }
+ checked_hw = true;
+ // Not both hw-address and duid
+ EXPECT_FALSE(host->contains("duid"));
+ // Check the HW address
+ ConstElementPtr hw = host->get("hw-address");
+ ASSERT_TRUE(hw);
+ ASSERT_EQ(Element::string, hw->getType());
+ EXPECT_EQ(hwaddrs_[i]->toText(false), hw->stringValue());
+ // Check the reservation
+ ConstElementPtr resvs = host->get("ip-addresses");
+ ASSERT_TRUE(resvs);
+ ASSERT_EQ(Element::list, resvs->getType());
+ EXPECT_EQ(1, resvs->size());
+ ASSERT_GE(1, resvs->size());
+ ConstElementPtr resv = resvs->get(0);
+ ASSERT_TRUE(resv);
+ ASSERT_EQ(Element::string, resv->getType());
+ EXPECT_EQ(increase(IOAddress("2001:db8:1::1"), i),
+ IOAddress(resv->stringValue()));
+ }
+ ASSERT_TRUE(checked_hw);
+
+ // Check by DUID entries
+ bool checked_duid = false;
+ for (unsigned j = 0; j < unparsed->size(); ++j) {
+ ConstElementPtr host = unparsed->get(j);
+ ASSERT_TRUE(host);
+ ASSERT_EQ(Element::map, host->getType());
+ if (!host->contains("duid")) {
+ continue;
+ }
+ checked_duid = true;
+ // Not both hw-address and duid
+ EXPECT_FALSE(host->contains("hw-address"));
+ // Check the DUID
+ ConstElementPtr duid = host->get("duid");
+ ASSERT_TRUE(duid);
+ ASSERT_EQ(Element::string, duid->getType());
+ EXPECT_EQ(duids_[i]->toText(), duid->stringValue());
+ // Check the reservation
+ ConstElementPtr resvs = host->get("ip-addresses");
+ ASSERT_TRUE(resvs);
+ ASSERT_EQ(Element::list, resvs->getType());
+ EXPECT_EQ(1, resvs->size());
+ ASSERT_GE(1, resvs->size());
+ ConstElementPtr resv = resvs->get(0);
+ ASSERT_TRUE(resv);
+ ASSERT_EQ(Element::string, resv->getType());
+ EXPECT_EQ(increase(IOAddress("2001:db8:2::1"), i),
+ IOAddress(resv->stringValue()));
+ }
+ ASSERT_TRUE(checked_duid);
+ }
+}
+
+// This test checks that the IPv6 reservations can be retrieved for a particular
+// (subnet-id, address) tuple.
+TEST_F(CfgHostsTest, get6ByAddr) {
+ CfgHosts cfg;
+ // Add hosts.
+ for (unsigned i = 0; i < 25; ++i) {
+
+ // Add host identified by DUID.
+ HostPtr host = HostPtr(new Host(duids_[i]->toText(), "duid",
+ SUBNET_ID_UNUSED, SubnetID(1 + i % 2),
+ IOAddress("0.0.0.0")));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ increase(IOAddress("2001:db8:2::1"),
+ i)));
+ cfg.add(host);
+ }
+
+ for (unsigned i = 0; i < 25; ++i) {
+ // Retrieve host by (subnet-id,address).
+ HostPtr host = cfg.get6(SubnetID(1 + i % 2),
+ increase(IOAddress("2001:db8:2::1"), i));
+ ASSERT_TRUE(host);
+
+ EXPECT_EQ(1 + i % 2, host->getIPv6SubnetID());
+ IPv6ResrvRange reservations =
+ host->getIPv6Reservations(IPv6Resrv::TYPE_NA);
+ ASSERT_EQ(1, std::distance(reservations.first, reservations.second));
+ EXPECT_EQ(increase(IOAddress("2001:db8:2::1"), i),
+ reservations.first->second.getPrefix());
+ }
+}
+
+// This test checks that the IPv6 reservations can be retrieved for a particular
+// (subnet-id, address) tuple.
+TEST_F(CfgHostsTest, get6MultipleAddrs) {
+ CfgHosts cfg;
+
+ // Add 25 hosts. Each host has reservations for 5 addresses.
+ for (unsigned i = 0; i < 25; ++i) {
+
+ // Add host identified by DUID.
+ HostPtr host = HostPtr(new Host(duids_[i]->toText(), "duid",
+ SUBNET_ID_UNUSED, SubnetID(1 + i % 2),
+ IOAddress("0.0.0.0")));
+
+ // Generate 5 unique addresses for this host.
+ for (unsigned j = 0; j < 5; ++j) {
+ std::stringstream address_stream;
+ address_stream << "2001:db8:" << i << "::" << j;
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ address_stream.str()));
+ }
+ cfg.add(host);
+ }
+
+ // Now check if we can retrieve each of those 25 hosts by using each
+ // of their addresses.
+ for (unsigned i = 0; i < 25; ++i) {
+
+ // Check that the host is there.
+ HostPtr by_duid = cfg.get6(SubnetID(1 + i % 2), Host::IDENT_DUID,
+ &duids_[i]->getDuid()[0],
+ duids_[i]->getDuid().size());
+ ASSERT_TRUE(by_duid);
+
+ for (unsigned j = 0; j < 5; ++j) {
+ std::stringstream address_stream;
+ address_stream << "2001:db8:" << i << "::" << j;
+
+ // Retrieve host by (subnet-id,address).
+ HostPtr by_addr = cfg.get6(SubnetID(1 + i % 2),
+ address_stream.str());
+ ASSERT_TRUE(by_addr);
+
+ // The pointers should match. Maybe we should compare contents
+ // rather than just pointers? I think there's no reason why
+ // the code would make any copies of the Host object, so
+ // the pointers should always point to the same object.
+ EXPECT_EQ(by_duid, by_addr);
+ }
+ }
+}
+
+
+// Checks that it's not possible for a second host to reserve an address
+// which is already reserved.
+TEST_F(CfgHostsTest, add4AlreadyReserved) {
+ CfgHosts cfg;
+
+ // First host has a reservation for address 192.0.2.1
+ HostPtr host1 = HostPtr(new Host(hwaddrs_[0]->toText(false),
+ "hw-address",
+ SubnetID(1), SubnetID(SUBNET_ID_UNUSED),
+ IOAddress("192.0.2.1")));
+ // Adding this should work.
+ EXPECT_NO_THROW(cfg.add(host1));
+
+ // The second host has a reservation for the same address.
+ HostPtr host2 = HostPtr(new Host(hwaddrs_[1]->toText(false),
+ "hw-address",
+ SubnetID(1), SUBNET_ID_UNUSED,
+ IOAddress("192.0.2.1")));
+
+ // This second host has a reservation for an address that is already
+ // reserved for the first host, so it should be rejected.
+ EXPECT_THROW(cfg.add(host2), isc::dhcp::ReservedAddress);
+}
+
+// Test that it is possible to allow inserting multiple reservations for
+// the same IP address.
+TEST_F(CfgHostsTest, allow4AlreadyReserved) {
+ CfgHosts cfg;
+ // Allow creating multiple reservations for the same IP address.
+ ASSERT_TRUE(cfg.setIPReservationsUnique(false));
+
+ // First host has a reservation for address 192.0.2.1
+ HostPtr host1 = HostPtr(new Host(hwaddrs_[0]->toText(false),
+ "hw-address",
+ SubnetID(1), SubnetID(SUBNET_ID_UNUSED),
+ IOAddress("192.0.2.1")));
+ ASSERT_NO_THROW(cfg.add(host1));
+
+ // The second host has a reservation for the same address.
+ HostPtr host2 = HostPtr(new Host(hwaddrs_[1]->toText(false),
+ "hw-address",
+ SubnetID(1), SUBNET_ID_UNUSED,
+ IOAddress("192.0.2.1")));
+ // Adding this should work because the HW address is different.
+ ASSERT_NO_THROW(cfg.add(host2));
+
+ // Get both hosts.
+ ConstHostCollection returned;
+ ASSERT_NO_THROW(returned = cfg.getAll4(host1->getIPv4SubnetID(), IOAddress("192.0.2.1")));
+ EXPECT_EQ(2, returned.size());
+
+ // Make sure the address is the same but the identifiers are different.
+ EXPECT_NE(returned[0]->getIdentifierAsText(), returned[1]->getIdentifierAsText());
+ EXPECT_EQ(returned[0]->getIPv4Reservation().toText(),
+ returned[1]->getIPv4Reservation().toText());
+}
+
+// Checks that it's not possible for two hosts to have the same address
+// reserved at the same time.
+TEST_F(CfgHostsTest, add6Invalid2Hosts) {
+ CfgHosts cfg;
+
+ // First host has a reservation for address 2001:db8::1
+ HostPtr host1 = HostPtr(new Host(duids_[0]->toText(), "duid",
+ SUBNET_ID_UNUSED, SubnetID(1),
+ IOAddress("0.0.0.0")));
+ host1->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8::1")));
+ // Adding this should work.
+ EXPECT_NO_THROW(cfg.add(host1));
+
+ // The second host has a reservation for the same address.
+ HostPtr host2 = HostPtr(new Host(duids_[1]->toText(), "duid",
+ SUBNET_ID_UNUSED, SubnetID(1),
+ IOAddress("0.0.0.0")));
+ host2->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8::1")));
+
+ // This second host has a reservation for an address that is already
+ // reserved for the first host, so it should be rejected.
+ EXPECT_THROW(cfg.add(host2), isc::dhcp::DuplicateHost);
+}
+
+// Test that it is possible to allow inserting multiple reservations for
+// the same IPv6 address.
+TEST_F(CfgHostsTest, allowAddress6AlreadyReserved) {
+ CfgHosts cfg;
+ // Allow creating multiple reservations for the same IP address.
+ ASSERT_TRUE(cfg.setIPReservationsUnique(false));
+
+ // First host has a reservation for address 2001:db8::1
+ HostPtr host1 = HostPtr(new Host(duids_[0]->toText(), "duid",
+ SUBNET_ID_UNUSED, SubnetID(1),
+ IOAddress("0.0.0.0")));
+ host1->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8::1")));
+ // Adding this should work.
+ EXPECT_NO_THROW(cfg.add(host1));
+
+ // The second host has a reservation for the same address.
+ HostPtr host2 = HostPtr(new Host(duids_[1]->toText(), "duid",
+ SUBNET_ID_UNUSED, SubnetID(1),
+ IOAddress("0.0.0.0")));
+ host2->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8::1")));
+
+ // Adding this should work because the DUID is different.
+ ASSERT_NO_THROW(cfg.add(host2));
+
+ ConstHostCollection returned;
+ ASSERT_NO_THROW(returned = cfg.getAll6(host1->getIPv6SubnetID(), IOAddress("2001:db8::1")));
+ EXPECT_EQ(2, returned.size());
+
+ // Make sure the address is the same but the identifiers are different.
+ EXPECT_NE(returned[0]->getIdentifierAsText(), returned[1]->getIdentifierAsText());
+
+ auto range0 = returned[0]->getIPv6Reservations(IPv6Resrv::TYPE_NA);
+ EXPECT_EQ(1, std::distance(range0.first, range0.second));
+ auto range1 = returned[1]->getIPv6Reservations(IPv6Resrv::TYPE_NA);
+ EXPECT_EQ(1, std::distance(range1.first, range1.second));
+ EXPECT_EQ(range0.first->second.getPrefix().toText(),
+ range1.first->second.getPrefix().toText());
+}
+
+// Test that it is possible to allow inserting multiple reservations for
+// the same IPv6 delegated prefix.
+TEST_F(CfgHostsTest, allowPrefix6AlreadyReserved) {
+ CfgHosts cfg;
+ // Allow creating multiple reservations for the same delegated prefix.
+ ASSERT_TRUE(cfg.setIPReservationsUnique(false));
+
+ // First host has a reservation for prefix 3000::/64.
+ HostPtr host1 = HostPtr(new Host(duids_[0]->toText(), "duid",
+ SUBNET_ID_UNUSED, SubnetID(1),
+ IOAddress("0.0.0.0")));
+ host1->addReservation(IPv6Resrv(IPv6Resrv::TYPE_PD,
+ IOAddress("3000::"), 64));
+ // Adding this should work.
+ EXPECT_NO_THROW(cfg.add(host1));
+
+ // The second host has a reservation for the same prefix.
+ HostPtr host2 = HostPtr(new Host(duids_[1]->toText(), "duid",
+ SUBNET_ID_UNUSED, SubnetID(1),
+ IOAddress("0.0.0.0")));
+ host2->addReservation(IPv6Resrv(IPv6Resrv::TYPE_PD,
+ IOAddress("3000::"), 64));
+
+ // Adding this should work because the DUID is different.
+ ASSERT_NO_THROW(cfg.add(host2));
+
+ ConstHostCollection returned;
+ ASSERT_NO_THROW(returned = cfg.getAll6(host1->getIPv6SubnetID(), IOAddress("3000::")));
+ EXPECT_EQ(2, returned.size());
+
+ // Make sure the prefix is the same but the identifiers are different.
+ EXPECT_NE(returned[0]->getIdentifierAsText(), returned[1]->getIdentifierAsText());
+
+ auto range0 = returned[0]->getIPv6Reservations(IPv6Resrv::TYPE_PD);
+ EXPECT_EQ(1, std::distance(range0.first, range0.second));
+ auto range1 = returned[1]->getIPv6Reservations(IPv6Resrv::TYPE_PD);
+ EXPECT_EQ(1, std::distance(range1.first, range1.second));
+ EXPECT_EQ(range0.first->second.getPrefix().toText(),
+ range1.first->second.getPrefix().toText());
+}
+
+// Check that no error is reported when adding a host with subnet
+// ids equal to global.
+TEST_F(CfgHostsTest, globalSubnetIDs) {
+ CfgHosts cfg;
+ ASSERT_NO_THROW(cfg.add(HostPtr(new Host(hwaddrs_[0]->toText(false),
+ "hw-address",
+ SUBNET_ID_GLOBAL, SUBNET_ID_GLOBAL,
+ IOAddress("10.0.0.1")))));
+}
+
+
+// Check that error is reported when trying to add a host with subnet
+// ids equal to unused.
+TEST_F(CfgHostsTest, unusedSubnetIDs) {
+ CfgHosts cfg;
+ ASSERT_THROW(cfg.add(HostPtr(new Host(hwaddrs_[0]->toText(false),
+ "hw-address",
+ SUBNET_ID_UNUSED, SUBNET_ID_UNUSED,
+ IOAddress("10.0.0.1")))),
+ isc::BadValue);
+}
+
+// This test verifies that it is not possible to add the same Host to the
+// same IPv4 subnet twice.
+TEST_F(CfgHostsTest, duplicatesSubnet4HWAddr) {
+ CfgHosts cfg;
+ // Add a host.
+ ASSERT_NO_THROW(cfg.add(HostPtr(new Host(hwaddrs_[0]->toText(false),
+ "hw-address",
+ SubnetID(10), SUBNET_ID_UNUSED,
+ IOAddress("10.0.0.1")))));
+
+ // Try to add the host with the same HW address to the same subnet. The fact
+ // that the IP address is different here shouldn't really matter.
+ EXPECT_THROW(cfg.add(HostPtr(new Host(hwaddrs_[0]->toText(false),
+ "hw-address",
+ SubnetID(10), SUBNET_ID_UNUSED,
+ IOAddress("10.0.0.10")))),
+ isc::dhcp::DuplicateHost);
+
+ // Now try to add it to a different subnet. It should go through.
+ EXPECT_NO_THROW(cfg.add(HostPtr(new Host(hwaddrs_[0]->toText(false),
+ "hw-address",
+ SubnetID(11), SUBNET_ID_UNUSED,
+ IOAddress("10.0.0.10")))));
+}
+
+// This test verifies that it is not possible to add the same Host to the
+// same IPv4 subnet twice.
+TEST_F(CfgHostsTest, duplicatesSubnet4DUID) {
+ CfgHosts cfg;
+ // Add a host.
+ ASSERT_NO_THROW(cfg.add(HostPtr(new Host(duids_[0]->toText(),
+ "duid",
+ SubnetID(10), SUBNET_ID_UNUSED,
+ IOAddress("10.0.0.1")))));
+
+ // Try to add the host with the same DUID to the same subnet. The fact
+ // that the IP address is different here shouldn't really matter.
+ EXPECT_THROW(cfg.add(HostPtr(new Host(duids_[0]->toText(),
+ "duid",
+ SubnetID(10), SUBNET_ID_UNUSED,
+ IOAddress("10.0.0.10")))),
+ isc::dhcp::DuplicateHost);
+
+ // Now try to add it to a different subnet. It should go through.
+ EXPECT_NO_THROW(cfg.add(HostPtr(new Host(duids_[0]->toText(),
+ "duid",
+ SubnetID(11), SUBNET_ID_UNUSED,
+ IOAddress("10.0.0.10")))));
+}
+
+// This test verifies that it is not possible to add the same Host to the
+// same IPv6 subnet twice.
+TEST_F(CfgHostsTest, duplicatesSubnet6HWAddr) {
+ CfgHosts cfg;
+ // Add a host.
+ ASSERT_NO_THROW(cfg.add(HostPtr(new Host(hwaddrs_[0]->toText(false),
+ "hw-address",
+ SUBNET_ID_UNUSED, SubnetID(1),
+ IOAddress("0.0.0.0"),
+ "foo.example.com"))));
+
+ // Try to add the host with the same HW address to the same subnet. The fact
+ // that the IP address is different here shouldn't really matter.
+ EXPECT_THROW(cfg.add(HostPtr(new Host(hwaddrs_[0]->toText(false),
+ "hw-address",
+ SUBNET_ID_UNUSED, SubnetID(1),
+ IOAddress("0.0.0.0"),
+ "foo.example.com"))),
+ isc::dhcp::DuplicateHost);
+
+ // Now try to add it to a different subnet. It should go through.
+ EXPECT_NO_THROW(cfg.add(HostPtr(new Host(hwaddrs_[0]->toText(false),
+ "hw-address",
+ SUBNET_ID_UNUSED, SubnetID(2),
+ IOAddress("0.0.0.0"),
+ "foo.example.com"))));
+}
+
+// This test verifies that it is not possible to add the same Host to the
+// same IPv6 subnet twice.
+TEST_F(CfgHostsTest, duplicatesSubnet6DUID) {
+ CfgHosts cfg;
+ // Add a host.
+ ASSERT_NO_THROW(cfg.add(HostPtr(new Host(duids_[0]->toText(),
+ "duid",
+ SUBNET_ID_UNUSED, SubnetID(1),
+ IOAddress("0.0.0.0"),
+ "foo.example.com"))));
+
+ // Try to add the host with the same DUID to the same subnet. The fact
+ // that the IP address is different here shouldn't really matter.
+ EXPECT_THROW(cfg.add(HostPtr(new Host(duids_[0]->toText(),
+ "duid",
+ SUBNET_ID_UNUSED, SubnetID(1),
+ IOAddress("0.0.0.0"),
+ "foo.example.com"))),
+ isc::dhcp::DuplicateHost);
+
+ // Now try to add it to a different subnet. It should go through.
+ EXPECT_NO_THROW(cfg.add(HostPtr(new Host(duids_[0]->toText(),
+ "duid",
+ SUBNET_ID_UNUSED, SubnetID(2),
+ IOAddress("0.0.0.0"),
+ "foo.example.com"))));
+}
+
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc b/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc
new file mode 100644
index 0000000..03591c0
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_iface_unittest.cc
@@ -0,0 +1,1065 @@
+// Copyright (C) 2014-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcp/tests/pkt_filter_test_stub.h>
+#include <dhcp/tests/pkt_filter6_test_stub.h>
+#include <dhcpsrv/cfg_iface.h>
+#include <asiolink/io_service.h>
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/interval_timer.h>
+#include <dhcpsrv/timer_mgr.h>
+#include <testutils/test_to_element.h>
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::test;
+using namespace isc::data;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Test fixture class for testing the @c CfgIface class.
+class CfgIfaceTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// By initializing the @c IfaceMgrTestConfig object it creates a set of
+ /// fake interfaces: lo, eth0, eth1.
+ CfgIfaceTest() :
+ iface_mgr_test_config_(true) {
+ }
+
+ /// @brief Checks if socket of the specified family is opened on interface.
+ ///
+ /// @param iface_name Interface name.
+ /// @param family One of: AF_INET or AF_INET6
+ bool socketOpen(const std::string& iface_name, const int family) const;
+
+ /// @brief Checks if socket is opened on the specified interface and bound
+ /// to a specific IPv4 address.
+ ///
+ /// @param iface_name Interface name.
+ /// @param address Address that the socket should be bound to.
+ bool socketOpen(const std::string& iface_name,
+ const std::string& address) const;
+
+ /// @brief Checks if unicast socket is opened on interface.
+ ///
+ /// @param iface_name Interface name.
+ bool unicastOpen(const std::string& iface_name) const;
+
+ /// @brief Wait for specific timeout.
+ ///
+ /// @param timeout Wait timeout in milliseconds.
+ void doWait(const long timeout);
+
+ /// @brief Holds a fake configuration of the interfaces.
+ IfaceMgrTestConfig iface_mgr_test_config_;
+
+ /// @brief Pointer to IO service used by the tests.
+ asiolink::IOServicePtr io_service_;
+
+private:
+
+ /// @brief Prepares the class for a test.
+ virtual void SetUp();
+
+ /// @brief Cleans up after the test.
+ virtual void TearDown();
+};
+
+void
+CfgIfaceTest::SetUp() {
+ IfaceMgr::instance().setTestMode(true);
+ io_service_.reset(new asiolink::IOService());
+ TimerMgr::instance()->setIOService(io_service_);
+}
+
+void
+CfgIfaceTest::TearDown() {
+ // Remove all timers.
+ TimerMgr::instance()->unregisterTimers();
+
+ IfaceMgr::instance().setTestMode(false);
+ IfaceMgr::instance().clearIfaces();
+ IfaceMgr::instance().closeSockets();
+ IfaceMgr::instance().detectIfaces();
+
+ // Reset global handlers
+ CfgIface::open_sockets_failed_callback_ = 0;
+}
+
+bool
+CfgIfaceTest::socketOpen(const std::string& iface_name,
+ const int family) const {
+ return (iface_mgr_test_config_.socketOpen(iface_name, family));
+}
+
+bool
+CfgIfaceTest::socketOpen(const std::string& iface_name,
+ const std::string& address) const {
+ return (iface_mgr_test_config_.socketOpen(iface_name, address));
+}
+
+bool
+CfgIfaceTest::unicastOpen(const std::string& iface_name) const {
+ return (iface_mgr_test_config_.unicastOpen(iface_name));
+}
+
+void
+CfgIfaceTest::doWait(const long timeout) {
+ asiolink::IntervalTimer timer(*io_service_);
+ timer.setup([this]() {
+ io_service_->stop();
+ }, timeout, asiolink::IntervalTimer::ONE_SHOT);
+ io_service_->run();
+ io_service_->get_io_service().reset();
+}
+
+// This test checks that the interface names can be explicitly selected
+// by their names and IPv4 sockets are opened on these interfaces.
+TEST_F(CfgIfaceTest, explicitNamesV4) {
+ CfgIface cfg;
+ // Specify valid interface names. There should be no error.
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth0"));
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth1"));
+
+ // Open sockets on specified interfaces.
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+
+ // Sockets should be now open on eth0 and eth1, but not on loopback.
+ EXPECT_TRUE(socketOpen("eth0", AF_INET));
+ EXPECT_TRUE(socketOpen("eth1", AF_INET));
+ EXPECT_FALSE(socketOpen("lo", AF_INET));
+
+ // No IPv6 sockets should be present because we wanted IPv4 sockets.
+ EXPECT_FALSE(socketOpen("eth0", AF_INET6));
+ EXPECT_FALSE(socketOpen("eth1", AF_INET6));
+ EXPECT_FALSE(socketOpen("lo", AF_INET6));
+
+ // Close all sockets and make sure they are really closed.
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("eth0", AF_INET));
+ ASSERT_FALSE(socketOpen("eth1", AF_INET));
+ ASSERT_FALSE(socketOpen("lo", AF_INET));
+
+ // Reset configuration and select only one interface this time.
+ cfg.reset();
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth1"));
+
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+
+ // Socket should be open on eth1 only.
+ EXPECT_FALSE(socketOpen("eth0", AF_INET));
+ EXPECT_TRUE(socketOpen("eth1", AF_INET));
+ EXPECT_FALSE(socketOpen("lo", AF_INET));
+}
+
+// This test checks that it is possible to specify an interface and address
+// on this interface to which the socket should be bound. The sockets should
+// not be opened on other addresses on this interface.
+TEST_F(CfgIfaceTest, explicitNamesAndAddressesV4) {
+ CfgIface cfg;
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth0/10.0.0.1"));
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth1/192.0.2.3"));
+
+ // Open sockets on specified interfaces and addresses.
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+
+ EXPECT_TRUE(socketOpen("eth0", "10.0.0.1"));
+ EXPECT_TRUE(socketOpen("eth1", "192.0.2.3"));
+ EXPECT_FALSE(socketOpen("eth1", "192.0.2.5"));
+
+ // Close all sockets and make sure they are really closed.
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("eth0", "10.0.0.1"));
+ ASSERT_FALSE(socketOpen("eth1", "192.0.2.3"));
+ ASSERT_FALSE(socketOpen("eth1", "192.0.2.5"));
+
+ // Reset configuration.
+ cfg.reset();
+
+ // Now check that the socket can be bound to a different address on
+ // eth1.
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth1/192.0.2.5"));
+
+ // Open sockets according to the new configuration.
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+
+ EXPECT_FALSE(socketOpen("eth0", "10.0.0.1"));
+ EXPECT_FALSE(socketOpen("eth1", "192.0.2.3"));
+ EXPECT_TRUE(socketOpen("eth1", "192.0.2.5"));
+}
+
+// This test checks that the invalid interface name and/or IPv4 address
+// results in error.
+TEST_F(CfgIfaceTest, explicitNamesAndAddressesInvalidV4) {
+ CfgIface cfg;
+ // An address not assigned to the interface.
+ EXPECT_THROW(cfg.use(AF_INET, "eth0/10.0.0.2"), NoSuchAddress);
+ // IPv6 address.
+ EXPECT_THROW(cfg.use(AF_INET, "eth0/2001:db8:1::1"), InvalidIfaceName);
+ // Wildcard interface name with an address.
+ EXPECT_THROW(cfg.use(AF_INET, "*/10.0.0.1"), InvalidIfaceName);
+
+ // Duplicated interface.
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth1"));
+ EXPECT_THROW(cfg.use(AF_INET, "eth1/192.0.2.3"), DuplicateIfaceName);
+}
+
+// This test checks that it is possible to explicitly select multiple
+// IPv4 addresses on a single interface.
+TEST_F(CfgIfaceTest, multipleAddressesSameInterfaceV4) {
+ CfgIface cfg;
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth1/192.0.2.3"));
+ // Cannot add the same address twice.
+ ASSERT_THROW(cfg.use(AF_INET, "eth1/192.0.2.3"), DuplicateAddress);
+ // Can add another address on this interface.
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth1/192.0.2.5"));
+ // Can't select the whole interface.
+ ASSERT_THROW(cfg.use(AF_INET, "eth1"), DuplicateIfaceName);
+
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+
+ EXPECT_FALSE(socketOpen("eth0", "10.0.0.1"));
+ EXPECT_TRUE(socketOpen("eth1", "192.0.2.3"));
+ EXPECT_TRUE(socketOpen("eth1", "192.0.2.5"));
+}
+
+// This test checks that it is possible to specify the loopback interface.
+TEST_F(CfgIfaceTest, explicitLoopbackV4) {
+ CfgIface cfg;
+ ASSERT_NO_THROW(cfg.use(AF_INET, "lo"));
+
+ // Use UDP sockets
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, CfgIface::SOCKET_UDP));
+
+ // Open sockets on specified interfaces and addresses.
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+
+ EXPECT_TRUE(socketOpen("lo", "127.0.0.1"));
+
+ // Close all sockets and make sure they are really closed.
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("lo", "127.0.0.1"));
+
+ // Reset configuration.
+ cfg.reset();
+
+ // Retry with wildcard
+ ASSERT_NO_THROW(cfg.use(AF_INET, "*"));
+ ASSERT_NO_THROW(cfg.use(AF_INET, "lo"));
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, CfgIface::SOCKET_UDP));
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+ // It is now allowed to use loopback, even with wildcard.
+ EXPECT_TRUE(socketOpen("lo", "127.0.0.1"));
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("lo", "127.0.0.1"));
+
+ // Retry without UDP sockets (lo can be only used with udp sockets)
+ cfg.reset();
+ ASSERT_NO_THROW(cfg.use(AF_INET, "lo"));
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+ // No loopback socket
+ EXPECT_FALSE(socketOpen("lo", "127.0.0.1"));
+
+ // Retry with a second interface
+ cfg.reset();
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth0"));
+ ASSERT_NO_THROW(cfg.use(AF_INET, "lo"));
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, CfgIface::SOCKET_UDP));
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+ // The logic used to require lo to be the only interface. That constraint
+ // was removed.
+ EXPECT_TRUE(socketOpen("lo", "127.0.0.1"));
+ cfg.closeSockets();
+ EXPECT_FALSE(socketOpen("lo", "127.0.0.1"));
+
+ // Finally with interfaces and addresses
+ cfg.reset();
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth0/10.0.0.1"));
+ ASSERT_NO_THROW(cfg.use(AF_INET, "lo/127.0.0.1"));
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, CfgIface::SOCKET_UDP));
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+ // Only loopback is no longer a constraint
+ EXPECT_TRUE(socketOpen("lo", "127.0.0.1"));
+ cfg.closeSockets();
+ EXPECT_FALSE(socketOpen("lo", "127.0.0.1"));
+}
+
+// This test checks that the interface names can be explicitly selected
+// by their names and IPv6 sockets are opened on these interfaces.
+TEST_F(CfgIfaceTest, explicitNamesV6) {
+ CfgIface cfg;
+ // Specify valid interface names. There should be no error.
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "eth0"));
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "eth1"));
+
+ // Open sockets on specified interfaces.
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+
+ // Sockets should be now open on eth0 and eth1, but not on loopback.
+ EXPECT_TRUE(socketOpen("eth0", AF_INET6));
+ EXPECT_TRUE(socketOpen("eth1", AF_INET6));
+ EXPECT_FALSE(socketOpen("lo", AF_INET6));
+
+ // No IPv4 sockets should be present because we wanted IPv6 sockets.
+ EXPECT_FALSE(socketOpen("eth0", AF_INET));
+ EXPECT_FALSE(socketOpen("eth1", AF_INET));
+ EXPECT_FALSE(socketOpen("lo", AF_INET));
+
+ // Close all sockets and make sure they are really closed.
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("eth0", AF_INET6));
+ ASSERT_FALSE(socketOpen("eth1", AF_INET6));
+ ASSERT_FALSE(socketOpen("lo", AF_INET6));
+
+ // Reset configuration and select only one interface this time.
+ cfg.reset();
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "eth1"));
+
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+
+ // Socket should be open on eth1 only.
+ EXPECT_FALSE(socketOpen("eth0", AF_INET6));
+ EXPECT_TRUE(socketOpen("eth1", AF_INET6));
+ EXPECT_FALSE(socketOpen("lo", AF_INET6));
+}
+
+// This test checks that the wildcard interface name can be specified to
+// select all interfaces to open IPv4 sockets.
+TEST_F(CfgIfaceTest, wildcardV4) {
+ CfgIface cfg;
+ ASSERT_NO_THROW(cfg.use(AF_INET, "*"));
+
+ cfg.openSockets(AF_INET, DHCP4_SERVER_PORT);
+
+ // Sockets should be now open on eth0 and eth1, but not on loopback.
+ EXPECT_TRUE(socketOpen("eth0", AF_INET));
+ EXPECT_TRUE(socketOpen("eth1", AF_INET));
+ EXPECT_FALSE(socketOpen("lo", AF_INET));
+
+ // No IPv6 sockets should be present because we wanted IPv4 sockets.
+ EXPECT_FALSE(socketOpen("eth0", AF_INET6));
+ EXPECT_FALSE(socketOpen("eth1", AF_INET6));
+ EXPECT_FALSE(socketOpen("lo", AF_INET6));
+}
+
+// This test checks that the wildcard interface name can be specified to
+// select all interfaces to open IPv6 sockets.
+TEST_F(CfgIfaceTest, wildcardV6) {
+ CfgIface cfg;
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "*"));
+
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+
+ // Sockets should be now open on eth0 and eth1, but not on loopback.
+ EXPECT_TRUE(socketOpen("eth0", AF_INET6));
+ EXPECT_TRUE(socketOpen("eth1", AF_INET6));
+ EXPECT_FALSE(socketOpen("lo", AF_INET6));
+
+ // No IPv6 sockets should be present because we wanted IPv6 sockets.
+ EXPECT_FALSE(socketOpen("eth0", AF_INET));
+ EXPECT_FALSE(socketOpen("eth1", AF_INET));
+ EXPECT_FALSE(socketOpen("lo", AF_INET));
+}
+
+// Test that unicast address can be specified for the socket to be opened on
+// the interface on which the socket bound to link local address is also
+// opened.
+TEST_F(CfgIfaceTest, validUnicast) {
+ CfgIface cfg;
+
+ // One socket will be opened on link-local address, one on unicast but
+ // on the same interface.
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "eth0"));
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "eth0/2001:db8:1::1"));
+
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+
+ EXPECT_TRUE(socketOpen("eth0", AF_INET6));
+ EXPECT_TRUE(unicastOpen("eth0"));
+}
+
+// Test that when invalid interface names are specified an exception is thrown.
+TEST_F(CfgIfaceTest, invalidValues) {
+ CfgIface cfg;
+ ASSERT_THROW(cfg.use(AF_INET, ""), InvalidIfaceName);
+ ASSERT_THROW(cfg.use(AF_INET, " "), InvalidIfaceName);
+ ASSERT_THROW(cfg.use(AF_INET, "bogus"), NoSuchIface);
+
+ ASSERT_NO_THROW(cfg.use(AF_INET, "eth0"));
+ ASSERT_THROW(cfg.use(AF_INET, "eth0"), DuplicateIfaceName);
+
+ ASSERT_THROW(cfg.use(AF_INET, "eth0/2001:db8:1::1"), InvalidIfaceName);
+
+ ASSERT_THROW(cfg.use(AF_INET6, "eth0/"), InvalidIfaceName);
+ ASSERT_THROW(cfg.use(AF_INET6, "/2001:db8:1::1"), InvalidIfaceName);
+ ASSERT_THROW(cfg.use(AF_INET6, "*/2001:db8:1::1"), InvalidIfaceName);
+ ASSERT_THROW(cfg.use(AF_INET6, "bogus/2001:db8:1::1"), NoSuchIface);
+ ASSERT_THROW(cfg.use(AF_INET6, "eth0/2001:db8:1::2"), NoSuchAddress);
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "*"));
+ ASSERT_THROW(cfg.use(AF_INET6, "*"), DuplicateIfaceName);
+}
+
+// This test checks that it is possible to specify the loopback interface.
+// Note that without a link-local address an unicast address is required.
+TEST_F(CfgIfaceTest, explicitLoopbackV6) {
+ CfgIface cfg;
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "lo/::1"));
+
+ // Open sockets on specified interfaces and addresses.
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+
+ EXPECT_TRUE(socketOpen("lo", AF_INET6));
+
+ // Close all sockets and make sure they are really closed.
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("lo", AF_INET6));
+
+ // Reset configuration.
+ cfg.reset();
+
+ // Retry with wildcard
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "*"));
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "lo/::1"));
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+ // The logic used to require lo to be used only on its own, not with a
+ // wildcard. That constraint was removed.
+ EXPECT_TRUE(socketOpen("lo", AF_INET6));
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("lo", AF_INET6));
+
+ // Retry with a second interface
+ cfg.reset();
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "eth0"));
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "lo/::1"));
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+ // The logic used to require lo to be used only on its own, not with a
+ // wildcard. That constraint was removed.
+ EXPECT_TRUE(socketOpen("lo", AF_INET6));
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("lo", AF_INET6));
+
+ // Finally with interfaces and addresses
+ cfg.reset();
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "eth0/2001:db8:1::1"));
+ ASSERT_NO_THROW(cfg.use(AF_INET6, "lo/::1"));
+ cfg.openSockets(AF_INET6, DHCP6_SERVER_PORT);
+ // The logic used to require lo to be used only on its own, not with a
+ // wildcard. That constraint was removed.
+ EXPECT_TRUE(socketOpen("lo", AF_INET6));
+ cfg.closeSockets();
+ ASSERT_FALSE(socketOpen("lo", AF_INET6));
+}
+
+// Test that the equality and inequality operators work fine for CfgIface.
+TEST_F(CfgIfaceTest, equality) {
+ CfgIface cfg1;
+ CfgIface cfg2;
+
+ // Initially objects must be equal.
+ EXPECT_TRUE(cfg1 == cfg2);
+ EXPECT_FALSE(cfg1 != cfg2);
+
+ // Differ by one interface.
+ cfg1.use(AF_INET, "eth0");
+ EXPECT_FALSE(cfg1 == cfg2);
+ EXPECT_TRUE(cfg1 != cfg2);
+
+ // Now interfaces should be equal.
+ cfg2.use(AF_INET, "eth0");
+ EXPECT_TRUE(cfg1 == cfg2);
+ EXPECT_FALSE(cfg1 != cfg2);
+
+ // Differ by unicast address.
+ cfg1.use(AF_INET6, "eth0/2001:db8:1::1");
+ EXPECT_FALSE(cfg1 == cfg2);
+ EXPECT_TRUE(cfg1 != cfg2);
+
+ // Differ by unicast address and one interface.
+ cfg2.use(AF_INET6, "eth1");
+ EXPECT_FALSE(cfg1 == cfg2);
+ EXPECT_TRUE(cfg1 != cfg2);
+
+ // Now, the unicast addresses are equal but still differ by one interface.
+ cfg2.use(AF_INET6, "eth0/2001:db8:1::1");
+ EXPECT_FALSE(cfg1 == cfg2);
+ EXPECT_TRUE(cfg1 != cfg2);
+
+ // They should be now back to equal.
+ cfg1.use(AF_INET6, "eth1");
+ EXPECT_TRUE(cfg1 == cfg2);
+ EXPECT_FALSE(cfg1 != cfg2);
+
+ // Even though the wildcard doesn't change anything because all interfaces
+ // are already in use, the fact that the wildcard is specified should
+ // cause them to be not equal.
+ cfg1.use(AF_INET6, "*");
+ EXPECT_FALSE(cfg1 == cfg2);
+ EXPECT_TRUE(cfg1 != cfg2);
+
+ // Finally, both are equal as they use wildcard.
+ cfg2.use(AF_INET, "*");
+ EXPECT_TRUE(cfg1 == cfg2);
+ EXPECT_FALSE(cfg1 != cfg2);
+
+ // Differ by socket type.
+ cfg1.useSocketType(AF_INET, "udp");
+ EXPECT_FALSE(cfg1 == cfg2);
+ EXPECT_TRUE(cfg1 != cfg2);
+
+ // Now, both should use the same socket type.
+ cfg2.useSocketType(AF_INET, "udp");
+ EXPECT_TRUE(cfg1 == cfg2);
+ EXPECT_FALSE(cfg1 != cfg2);
+}
+
+// This test verifies that it is possible to unparse the interface config.
+TEST_F(CfgIfaceTest, unparse) {
+ CfgIface cfg4;
+
+ // Add things in it
+ EXPECT_NO_THROW(cfg4.use(AF_INET, "*"));
+ EXPECT_NO_THROW(cfg4.use(AF_INET, "eth0"));
+ EXPECT_NO_THROW(cfg4.use(AF_INET, "eth1/192.0.2.3"));
+ std::string comment = "{ \"comment\": \"foo\", \"bar\": 1 }";
+ EXPECT_NO_THROW(cfg4.setContext(Element::fromJSON(comment)));
+
+ // Check unparse
+ std::string expected =
+ "{ "
+ "\"interfaces\": [ \"*\", \"eth0\", \"eth1/192.0.2.3\" ], "
+ "\"re-detect\": false, "
+ "\"user-context\": { \"comment\": \"foo\", \"bar\": 1 } }";
+ runToElementTest<CfgIface>(expected, cfg4);
+
+ // Now check IPv6
+ CfgIface cfg6;
+ EXPECT_NO_THROW(cfg6.use(AF_INET6, "*"));
+ EXPECT_NO_THROW(cfg6.use(AF_INET6, "eth1"));
+ EXPECT_NO_THROW(cfg6.use(AF_INET6, "eth0/2001:db8:1::1"));
+ comment = "{ \"comment\": \"bar\", \"foo\": 2 }";
+ EXPECT_NO_THROW(cfg6.setContext(Element::fromJSON(comment)));
+
+ expected =
+ "{ "
+ "\"interfaces\": [ \"*\", \"eth1\", \"eth0/2001:db8:1::1\" ], "
+ "\"re-detect\": false, "
+ "\"user-context\": { \"comment\": \"bar\", \"foo\": 2 } }";
+ runToElementTest<CfgIface>(expected, cfg6);
+}
+
+// This test verifies that it is possible to require that all
+// service sockets are opened properly. If any socket fails to
+// bind then an exception should be thrown.
+TEST_F(CfgIfaceTest, requireOpenAllServiceSockets) {
+ CfgIface cfg4;
+ CfgIface cfg6;
+
+ // Configure a fail callback
+ uint16_t fail_calls = 0;
+ CfgIface::OpenSocketsFailedCallback on_fail_callback =
+ [&fail_calls](ReconnectCtlPtr reconnect_ctl) {
+ EXPECT_TRUE(reconnect_ctl);
+ EXPECT_TRUE(reconnect_ctl->exitOnFailure());
+ fail_calls++;
+ };
+
+ CfgIface::open_sockets_failed_callback_ = on_fail_callback;
+
+ ASSERT_NO_THROW(cfg4.use(AF_INET, "eth0"));
+ ASSERT_NO_THROW(cfg4.use(AF_INET, "eth1/192.0.2.3"));
+ ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth0/2001:db8:1::1"));
+ ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth1"));
+
+ // Require all sockets bind successfully
+ cfg4.setServiceSocketsRequireAll(true);
+ cfg4.setServiceSocketsMaxRetries(0);
+ cfg6.setServiceSocketsRequireAll(true);
+ cfg6.setServiceSocketsMaxRetries(0);
+
+ // Open the available ports
+ ASSERT_NO_THROW(cfg4.openSockets(AF_INET, DHCP4_SERVER_PORT));
+ ASSERT_NO_THROW(cfg6.openSockets(AF_INET6, DHCP6_SERVER_PORT));
+ cfg4.closeSockets();
+ cfg6.closeSockets();
+
+ // Set the callback to throw an exception on open
+ auto open_callback = [](uint16_t) {
+ isc_throw(Unexpected, "CfgIfaceTest: cannot open a port");
+ };
+ boost::shared_ptr<isc::dhcp::test::PktFilterTestStub> filter(new isc::dhcp::test::PktFilterTestStub());
+ boost::shared_ptr<isc::dhcp::test::PktFilter6TestStub> filter6(new isc::dhcp::test::PktFilter6TestStub());
+ filter->setOpenSocketCallback(open_callback);
+ filter6->setOpenSocketCallback(open_callback);
+ ASSERT_TRUE(filter);
+ ASSERT_TRUE(filter6);
+ ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter));
+ ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter6));
+
+ // Open an unavailable port
+ EXPECT_NO_THROW(cfg4.openSockets(AF_INET, DHCP4_SERVER_PORT));
+ EXPECT_NO_THROW(cfg6.openSockets(AF_INET6, DHCP6_SERVER_PORT));
+
+ // Both instances should call the fail callback.
+ EXPECT_EQ(fail_calls, 2);
+}
+
+// This test verifies that if any IPv4 socket fails to bind,
+// the opening will retry.
+TEST_F(CfgIfaceTest, retryOpenServiceSockets4) {
+ CfgIface cfg4;
+
+ ASSERT_NO_THROW(cfg4.use(AF_INET, "eth0"));
+ ASSERT_NO_THROW(cfg4.use(AF_INET, "eth1/192.0.2.3"));
+
+ // Parameters
+ const uint16_t RETRIES = 5;
+ const uint16_t WAIT_TIME = 10; // miliseconds
+ // The number of sockets opened in a single retry attempt.
+ // iface: eth0 addr: 10.0.0.1 port: 67 rbcast: 0 sbcast: 0
+ // iface: eth1 addr: 192.0.2.3 port: 67 rbcast: 0 sbcast: 0
+ const uint16_t CALLS_PER_RETRY = 2;
+
+ // Require retry socket binding
+ cfg4.setServiceSocketsMaxRetries(RETRIES);
+ cfg4.setServiceSocketsRetryWaitTime(WAIT_TIME);
+
+ // Set the callback to count calls and check wait time
+ size_t total_calls = 0;
+ auto last_call_time = std::chrono::system_clock::time_point::min();
+ auto open_callback = [&total_calls, &last_call_time, WAIT_TIME](uint16_t) {
+ auto now = std::chrono::system_clock::now();
+
+ // Check waiting time only for the first call in a retry attempt.
+ if (total_calls % CALLS_PER_RETRY == 0) {
+ // Don't check the waiting time for initial call.
+ if (total_calls != 0) {
+ auto interval = now - last_call_time;
+ auto interval_ms =
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ interval
+ ).count();
+
+ EXPECT_GE(interval_ms, WAIT_TIME);
+ }
+
+ last_call_time = now;
+ }
+
+ total_calls++;
+
+ // Fail to open a socket
+ isc_throw(Unexpected, "CfgIfaceTest: cannot open a port");
+ };
+
+ boost::shared_ptr<isc::dhcp::test::PktFilterTestStub> filter(
+ new isc::dhcp::test::PktFilterTestStub()
+ );
+
+ filter->setOpenSocketCallback(open_callback);
+
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter));
+
+ // Open an unavailable port
+ ASSERT_NO_THROW(cfg4.openSockets(AF_INET, DHCP4_SERVER_PORT));
+
+ // Wait for a finish sockets binding (with a safe margin).
+ doWait(RETRIES * WAIT_TIME * 2);
+
+ // For each interface perform 1 init open and a few retries.
+ EXPECT_EQ(CALLS_PER_RETRY * (RETRIES + 1), total_calls);
+}
+
+// This test verifies that if any IPv4 socket fails to bind, the opening will
+// retry, but the opened sockets will not be re-bound.
+TEST_F(CfgIfaceTest, retryOpenServiceSockets4OmitBound) {
+ CfgIface cfg4;
+
+ ASSERT_NO_THROW(cfg4.use(AF_INET, "eth0"));
+ ASSERT_NO_THROW(cfg4.use(AF_INET, "eth1/192.0.2.3"));
+
+ // Parameters
+ const uint16_t RETRIES = 5;
+ const uint16_t WAIT_TIME = 10; // miliseconds
+
+ // Require retry socket binding
+ cfg4.setServiceSocketsMaxRetries(RETRIES);
+ cfg4.setServiceSocketsRetryWaitTime(WAIT_TIME);
+
+ // Set the callback to count calls and check wait time
+ size_t total_calls = 0;
+ auto last_call_time = std::chrono::system_clock::time_point::min();
+ auto open_callback = [&total_calls, &last_call_time, WAIT_TIME](uint16_t) {
+ auto now = std::chrono::system_clock::now();
+ bool is_eth1 = total_calls == 1;
+
+ // Skip the wait time check for the socket when two sockets are
+ // binding in a single attempt.
+
+ // Don't check the waiting time for initial calls.
+ // iface: eth0 addr: 10.0.0.1 port: 67 rbcast: 0 sbcast: 0
+ // iface: eth1 addr: 192.0.2.3 port: 67 rbcast: 0 sbcast: 0 - fails
+ if (total_calls > 1) {
+ auto interval = now - last_call_time;
+ auto interval_ms =
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ interval
+ ).count();
+
+ EXPECT_GE(interval_ms, WAIT_TIME);
+ }
+
+ last_call_time = now;
+
+ total_calls++;
+
+ // Fail to open a socket on eth0, success for eth1
+ if (!is_eth1) {
+ isc_throw(Unexpected, "CfgIfaceTest: cannot open a port");
+ }
+ };
+
+ boost::shared_ptr<isc::dhcp::test::PktFilterTestStub> filter(
+ new isc::dhcp::test::PktFilterTestStub()
+ );
+
+ filter->setOpenSocketCallback(open_callback);
+
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter));
+
+ // Open an unavailable port
+ ASSERT_NO_THROW(cfg4.openSockets(AF_INET, DHCP4_SERVER_PORT));
+
+ // Wait for a finish sockets binding (with a safe margin).
+ doWait(RETRIES * WAIT_TIME * 2);
+
+ // For eth0 interface perform 1 init open and a few retries,
+ // for eth1 interface perform only init open.
+ EXPECT_EQ((RETRIES + 1) + 1, total_calls);
+}
+
+// Test that only one reopen timer is active simultaneously. If a new opening
+// starts, then the previous should be interrupted.
+TEST_F(CfgIfaceTest, retryDoubleOpenServiceSockets4) {
+ CfgIface cfg4;
+
+ ASSERT_NO_THROW(cfg4.use(AF_INET, "eth0"));
+
+ // Initial timer has a high frequency.
+ cfg4.setServiceSocketsMaxRetries(10000);
+ cfg4.setServiceSocketsRetryWaitTime(1);
+
+ // Set the callback that interrupt the previous execution.
+ uint16_t first_port_calls = 0;
+ uint16_t second_port_calls = 0;
+ auto open_callback = [&first_port_calls, &second_port_calls](uint16_t port) {
+ // First timer must be interrupted.
+ if (second_port_calls > 0) {
+ EXPECT_TRUE(port == 2);
+ }
+
+ if (port == 1) {
+ first_port_calls++;
+ } else {
+ second_port_calls++;
+ }
+
+ // Fail to open and retry.
+ isc_throw(Unexpected, "CfgIfaceTest: cannot open a port");
+ };
+
+ boost::shared_ptr<isc::dhcp::test::PktFilterTestStub> filter(
+ new isc::dhcp::test::PktFilterTestStub()
+ );
+
+ filter->setOpenSocketCallback(open_callback);
+
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter));
+
+ // First opening.
+ ASSERT_NO_THROW(cfg4.openSockets(AF_INET, 1));
+
+ // Wait a short time.
+ doWait(10);
+
+ // Reconfigure the interface parameters.
+ cfg4.setServiceSocketsMaxRetries(1);
+ cfg4.setServiceSocketsRetryWaitTime(10);
+
+ // Second opening.
+ ASSERT_NO_THROW(cfg4.openSockets(AF_INET, 2));
+
+ doWait(50);
+
+ // The first timer should perform some calls.
+ EXPECT_GT(first_port_calls, 0);
+ // The secondary timer should make 2 calls: initial and 1 retry.
+ EXPECT_EQ(second_port_calls, 2);
+}
+
+// This test verifies that if any IPv6 socket fails to bind,
+// the opening will retry.
+TEST_F(CfgIfaceTest, retryOpenServiceSockets6) {
+ CfgIface cfg6;
+
+ ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth0/2001:db8:1::1"));
+ ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth1"));
+
+ // Parameters
+ const uint16_t RETRIES = 5;
+ const uint16_t WAIT_TIME = 10; // miliseconds
+ // The number of sockets opened in a single retry attempt.
+ // 1 unicast and 2 multicast sockets.
+ // iface: eth0 addr: 2001:db8:1::1 port: 547 multicast: 0
+ // iface: eth0 addr: fe80::3a60:77ff:fed5:cdef port: 547 multicast: 1
+ // iface: eth1 addr: fe80::3a60:77ff:fed5:abcd port: 547 multicast: 1
+ const uint16_t CALLS_PER_RETRY = 3;
+
+ // Require retry socket binding
+ cfg6.setServiceSocketsMaxRetries(RETRIES);
+ cfg6.setServiceSocketsRetryWaitTime(WAIT_TIME);
+
+ // Set the callback to count calls and check wait time
+ size_t total_calls = 0;
+ auto last_call_time = std::chrono::system_clock::time_point::min();
+ auto open_callback = [&total_calls, &last_call_time, WAIT_TIME](uint16_t) {
+ auto now = std::chrono::system_clock::now();
+
+ // Check waiting time only for the first call in a retry attempt.
+ if (total_calls % CALLS_PER_RETRY == 0) {
+ // Don't check the waiting time for initial call.
+ if (total_calls != 0) {
+ auto interval = now - last_call_time;
+ auto interval_ms =
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ interval
+ ).count();
+
+ EXPECT_GE(interval_ms, WAIT_TIME);
+ }
+
+ last_call_time = now;
+ }
+
+ total_calls++;
+
+ // Fail to open a socket
+ isc_throw(Unexpected, "CfgIfaceTest: cannot open a port");
+ };
+
+ boost::shared_ptr<isc::dhcp::test::PktFilter6TestStub> filter(
+ new isc::dhcp::test::PktFilter6TestStub()
+ );
+ filter->setOpenSocketCallback(open_callback);
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter));
+
+ // Open an unavailable port
+ ASSERT_NO_THROW(cfg6.openSockets(AF_INET6, DHCP6_SERVER_PORT));
+
+ // Wait for a finish sockets binding (with a safe margin).
+ doWait(RETRIES * WAIT_TIME * 2);
+
+ // For each interface perform 1 init open and a few retries.
+ EXPECT_EQ(CALLS_PER_RETRY * (RETRIES + 1), total_calls);
+}
+
+// This test verifies that if any IPv6 socket fails to bind, the opening will
+// retry, but the opened sockets will not be re-bound.
+TEST_F(CfgIfaceTest, retryOpenServiceSockets6OmitBound) {
+ CfgIface cfg6;
+
+ ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth0/2001:db8:1::1"));
+ ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth1"));
+
+ // Parameters
+ const uint16_t RETRIES = 5;
+ const uint16_t WAIT_TIME = 10; // miliseconds
+
+ // Require retry socket binding
+ cfg6.setServiceSocketsMaxRetries(RETRIES);
+ cfg6.setServiceSocketsRetryWaitTime(WAIT_TIME);
+
+#if defined OS_LINUX
+ const uint32_t opened_by_eth0 = 3;
+#else
+ const uint32_t opened_by_eth0 = 2;
+#endif
+
+ // Set the callback to count calls and check wait time
+ size_t total_calls = 0;
+ auto last_call_time = std::chrono::system_clock::time_point::min();
+ auto open_callback = [&total_calls, &last_call_time, WAIT_TIME](uint16_t) {
+ auto now = std::chrono::system_clock::now();
+ bool is_eth0 = total_calls < opened_by_eth0;
+
+ // Skip the wait time check for the socket when two sockets are
+ // binding in a single attempt.
+
+ // Don't check the waiting time for initial calls.
+ // iface: eth0 addr: 2001:db8:1::1 port: 547 multicast: 0
+ // iface: eth0 addr: fe80::3a60:77ff:fed5:cdef port: 547 multicast: 1
+ // iface: eth0 addr: ff02::1:2 port: 547 multicast: 0 --- only on Linux systems
+ // iface: eth1 addr: fe80::3a60:77ff:fed5:abcd port: 547 multicast: 1 - fails
+
+ if (total_calls > (opened_by_eth0 + 1)) {
+ auto interval = now - last_call_time;
+ auto interval_ms =
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ interval
+ ).count();
+
+ EXPECT_GE(interval_ms, WAIT_TIME);
+ }
+
+ last_call_time = now;
+
+ total_calls++;
+
+ // Fail to open a socket on eth1, success for eth0
+ if (!is_eth0) {
+ isc_throw(Unexpected, "CfgIfaceTest: cannot open a port");
+ }
+ };
+
+ boost::shared_ptr<isc::dhcp::test::PktFilter6TestStub> filter(
+ new isc::dhcp::test::PktFilter6TestStub()
+ );
+
+ filter->setOpenSocketCallback(open_callback);
+
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter));
+
+ // Open an unavailable port
+ ASSERT_NO_THROW(cfg6.openSockets(AF_INET6, DHCP6_SERVER_PORT));
+
+ // Wait for a finish sockets binding (with a safe margin).
+ doWait(RETRIES * WAIT_TIME * 2);
+
+ // For eth0 interface perform only 3 (on Linux Systems or 2 otherwise) init open,
+ // for eth1 interface perform 1 init open and a few retries.
+ EXPECT_EQ((RETRIES + 1) + opened_by_eth0, total_calls);
+}
+
+// Test that only one reopen timer is active simultaneously. If a new opening
+// starts, then the previous should be interrupted.
+TEST_F(CfgIfaceTest, retryDoubleOpenServiceSockets6) {
+ CfgIface cfg6;
+
+ ASSERT_NO_THROW(cfg6.use(AF_INET6, "eth0"));
+
+ // Initial timer has a high frequency.
+ cfg6.setServiceSocketsMaxRetries(10000);
+ cfg6.setServiceSocketsRetryWaitTime(1);
+
+ // Set the callback that interrupt the previous execution.
+ uint16_t first_port_calls = 0;
+ uint16_t second_port_calls = 0;
+ auto open_callback = [&first_port_calls, &second_port_calls](uint16_t port) {
+ // First timer must be interrupted.
+ if (second_port_calls > 0) {
+ EXPECT_TRUE(port == 2);
+ }
+
+ if (port == 1) {
+ first_port_calls++;
+ } else {
+ second_port_calls++;
+ }
+
+ // Fail to open and retry.
+ isc_throw(Unexpected, "CfgIfaceTest: cannot open a port");
+ };
+
+ boost::shared_ptr<isc::dhcp::test::PktFilter6TestStub> filter(
+ new isc::dhcp::test::PktFilter6TestStub()
+ );
+
+ filter->setOpenSocketCallback(open_callback);
+
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(IfaceMgr::instance().setPacketFilter(filter));
+
+ // First opening.
+ ASSERT_NO_THROW(cfg6.openSockets(AF_INET6, 1));
+
+ // Wait a short time.
+ doWait(10);
+
+ // Reconfigure the interface parameters.
+ cfg6.setServiceSocketsMaxRetries(1);
+ cfg6.setServiceSocketsRetryWaitTime(10);
+
+ // Second opening.
+ ASSERT_NO_THROW(cfg6.openSockets(AF_INET6, 2));
+
+ doWait(50);
+
+ // The first timer should perform some calls.
+ EXPECT_GT(first_port_calls, 0);
+ // The secondary timer should make 2 calls: initial and 1 retry.
+ EXPECT_EQ(second_port_calls, 2);
+}
+
+// This test verifies that it is possible to specify the socket
+// type to be used by the DHCPv4 server.
+// This test is enabled on LINUX and BSD only, because the
+// direct traffic is only supported on those systems.
+#if defined OS_LINUX || defined OS_BSD
+TEST(CfgIfaceNoStubTest, useSocketType) {
+ CfgIface cfg;
+ // Select datagram sockets.
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, "udp"));
+ EXPECT_EQ("udp", cfg.socketTypeToText());
+ ASSERT_NO_THROW(cfg.openSockets(AF_INET, 10067, true));
+ // For datagram sockets, the direct traffic is not supported.
+ ASSERT_TRUE(!IfaceMgr::instance().isDirectResponseSupported());
+
+ // Check unparse
+ std::string expected = "{\n"
+ " \"interfaces\": [ ],\n"
+ " \"dhcp-socket-type\": \"udp\",\n"
+ " \"re-detect\": false }";
+ runToElementTest<CfgIface>(expected, cfg);
+
+ // Select raw sockets.
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, "raw"));
+ EXPECT_EQ("raw", cfg.socketTypeToText());
+ ASSERT_NO_THROW(cfg.openSockets(AF_INET, 10067, true));
+ // For raw sockets, the direct traffic is supported.
+ ASSERT_TRUE(IfaceMgr::instance().isDirectResponseSupported());
+
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, CfgIface::SOCKET_UDP));
+ EXPECT_EQ("udp", cfg.socketTypeToText());
+ ASSERT_NO_THROW(cfg.openSockets(AF_INET, 10067, true));
+ ASSERT_TRUE(!IfaceMgr::instance().isDirectResponseSupported());
+
+ ASSERT_NO_THROW(cfg.useSocketType(AF_INET, CfgIface::SOCKET_RAW));
+ EXPECT_EQ("raw", cfg.socketTypeToText());
+ ASSERT_NO_THROW(cfg.openSockets(AF_INET, 10067, true));
+ ASSERT_TRUE(IfaceMgr::instance().isDirectResponseSupported());
+
+ // Test invalid values.
+ EXPECT_THROW(cfg.useSocketType(AF_INET, "default"),
+ InvalidSocketType);
+ EXPECT_THROW(cfg.useSocketType(AF_INET6, "udp"),
+ InvalidSocketType);
+}
+#endif
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/cfg_mac_source_unittest.cc b/src/lib/dhcpsrv/tests/cfg_mac_source_unittest.cc
new file mode 100644
index 0000000..b02c791
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_mac_source_unittest.cc
@@ -0,0 +1,84 @@
+// Copyright (C) 2014-2015,2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcpsrv/cfg_mac_source.h>
+#include <dhcp/hwaddr.h>
+#include <exceptions/exceptions.h>
+#include <testutils/test_to_element.h>
+#include <gtest/gtest.h>
+#include <string>
+
+namespace {
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::test;
+
+// Checks whether CfgMACSource::MACSourceFromText is working correctly.
+// Technically, this is a Pkt, not Pkt6 test, but since there is no separate
+// unit-tests for Pkt and it is abstract, so it would be tricky to test it
+// directly. Hence test is being run in Pkt6.
+TEST(CfgMACSourceTest, MACSourceFromText) {
+ EXPECT_THROW(CfgMACSource::MACSourceFromText("unknown"), BadValue);
+
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_ANY, CfgMACSource::MACSourceFromText("any"));
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_RAW, CfgMACSource::MACSourceFromText("raw"));
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_DUID, CfgMACSource::MACSourceFromText("duid"));
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_IPV6_LINK_LOCAL,
+ CfgMACSource::MACSourceFromText("ipv6-link-local"));
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_CLIENT_ADDR_RELAY_OPTION,
+ CfgMACSource::MACSourceFromText("client-link-addr-option"));
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_CLIENT_ADDR_RELAY_OPTION,
+ CfgMACSource::MACSourceFromText("rfc6939"));
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_REMOTE_ID,
+ CfgMACSource::MACSourceFromText("remote-id"));
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_REMOTE_ID,
+ CfgMACSource::MACSourceFromText("rfc4649"));
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_SUBSCRIBER_ID,
+ CfgMACSource::MACSourceFromText("subscriber-id"));
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_SUBSCRIBER_ID,
+ CfgMACSource::MACSourceFromText("rfc4580"));
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_DOCSIS_CMTS,
+ CfgMACSource::MACSourceFromText("docsis-cmts"));
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_DOCSIS_MODEM,
+ CfgMACSource::MACSourceFromText("docsis-modem"));
+}
+
+// Checks whether the opposite operation is working correctly.
+TEST(CfgMACSourceTest, unparse) {
+ CfgMACSource cfg;
+ // any was added by the constructor
+ cfg.add(HWAddr::HWADDR_SOURCE_RAW);
+ cfg.add(HWAddr::HWADDR_SOURCE_DUID);
+ cfg.add(HWAddr::HWADDR_SOURCE_IPV6_LINK_LOCAL);
+ cfg.add(HWAddr::HWADDR_SOURCE_CLIENT_ADDR_RELAY_OPTION);
+ cfg.add(HWAddr::HWADDR_SOURCE_REMOTE_ID);
+ cfg.add(HWAddr::HWADDR_SOURCE_SUBSCRIBER_ID);
+ cfg.add(HWAddr::HWADDR_SOURCE_DOCSIS_CMTS);
+ cfg.add(HWAddr::HWADDR_SOURCE_DOCSIS_MODEM);
+
+ // Unparse
+ std::string expected = "["
+ "\"any\","
+ "\"raw\","
+ "\"duid\","
+ "\"ipv6-link-local\","
+ "\"client-link-addr-option\","
+ "\"remote-id\","
+ "\"subscriber-id\","
+ "\"docsis-cmts\","
+ "\"docsis-modem\""
+ "]";
+ runToElementTest<CfgMACSource>(expected, cfg);
+
+ // Add an unknown type
+ cfg.add(0x12345678);
+ ASSERT_THROW(cfg.toElement(), ToElementError);
+}
+
+};
diff --git a/src/lib/dhcpsrv/tests/cfg_multi_threading_unittest.cc b/src/lib/dhcpsrv/tests/cfg_multi_threading_unittest.cc
new file mode 100644
index 0000000..3d9de82
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_multi_threading_unittest.cc
@@ -0,0 +1,117 @@
+// Copyright (C) 2020-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <cc/data.h>
+#include <dhcpsrv/cfg_multi_threading.h>
+#include <util/multi_threading_mgr.h>
+
+#include <gtest/gtest.h>
+
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Test fixture class for @c MultiThreadingConfigParser
+class CfgMultiThreadingTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor
+ CfgMultiThreadingTest() = default;
+
+ /// @brief Destructor
+ virtual ~CfgMultiThreadingTest() = default;
+
+protected:
+
+ /// @brief Setup for each test.
+ ///
+ /// Clears the configuration in the @c MultiThreadingMgr.
+ virtual void SetUp();
+
+ /// @brief Cleans up after each test.
+ ///
+ /// Clears the configuration in the @c MultiThreadingMgr.
+ virtual void TearDown();
+};
+
+void
+CfgMultiThreadingTest::SetUp() {
+ MultiThreadingMgr::instance().apply(false, 0, 0);
+}
+
+void
+CfgMultiThreadingTest::TearDown() {
+ MultiThreadingMgr::instance().apply(false, 0 , 0);
+}
+
+/// @brief Verifies that extracting multi threading settings works
+TEST_F(CfgMultiThreadingTest, extract) {
+ bool enabled = false;
+ uint32_t thread_count = 0;
+ uint32_t queue_size = 0;
+ std::string content_json =
+ "{"
+ " \"enable-multi-threading\": true,\n"
+ " \"thread-pool-size\": 4,\n"
+ " \"packet-queue-size\": 64\n"
+ "}";
+ ConstElementPtr param;
+ ASSERT_NO_THROW(param = Element::fromJSON(content_json))
+ << "invalid context_json, test is broken";
+ ASSERT_NO_THROW(CfgMultiThreading::extract(param, enabled, thread_count,
+ queue_size));
+ EXPECT_EQ(enabled, true);
+ EXPECT_EQ(thread_count, 4);
+ EXPECT_EQ(queue_size, 64);
+
+ content_json = "{}";
+ ASSERT_NO_THROW(param = Element::fromJSON(content_json))
+ << "invalid context_json, test is broken";
+ //check empty config
+ ASSERT_NO_THROW(CfgMultiThreading::extract(param, enabled, thread_count,
+ queue_size));
+ EXPECT_EQ(enabled, false);
+ EXPECT_EQ(thread_count, 0);
+ EXPECT_EQ(queue_size, 0);
+
+ enabled = true;
+ thread_count = 4;
+ queue_size = 64;
+ // check empty data
+ ASSERT_NO_THROW(CfgMultiThreading::extract(ConstElementPtr(), enabled,
+ thread_count, queue_size));
+ EXPECT_EQ(enabled, false);
+ EXPECT_EQ(thread_count, 0);
+ EXPECT_EQ(queue_size, 0);
+}
+
+/// @brief Verifies that applying multi threading settings works
+TEST_F(CfgMultiThreadingTest, apply) {
+ EXPECT_FALSE(MultiThreadingMgr::instance().getMode());
+ EXPECT_EQ(MultiThreadingMgr::instance().getThreadPoolSize(), 0);
+ EXPECT_EQ(MultiThreadingMgr::instance().getPacketQueueSize(), 0);
+ EXPECT_EQ(MultiThreadingMgr::instance().getThreadPool().getMaxQueueSize(), 0);
+ std::string content_json =
+ "{"
+ " \"enable-multi-threading\": true,\n"
+ " \"thread-pool-size\": 4,\n"
+ " \"packet-queue-size\": 64\n"
+ "}";
+ ConstElementPtr param;
+ ASSERT_NO_THROW(param = Element::fromJSON(content_json))
+ << "invalid context_json, test is broken";
+ CfgMultiThreading::apply(param);
+ EXPECT_TRUE(MultiThreadingMgr::instance().getMode());
+ EXPECT_EQ(MultiThreadingMgr::instance().getThreadPoolSize(), 4);
+ EXPECT_EQ(MultiThreadingMgr::instance().getPacketQueueSize(), 64);
+ EXPECT_EQ(MultiThreadingMgr::instance().getThreadPool().getMaxQueueSize(), 64);
+}
+
+} // namespace
diff --git a/src/lib/dhcpsrv/tests/cfg_option_def_unittest.cc b/src/lib/dhcpsrv/tests/cfg_option_def_unittest.cc
new file mode 100644
index 0000000..e798173
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_option_def_unittest.cc
@@ -0,0 +1,400 @@
+// Copyright (C) 2014-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/option_space.h>
+#include <dhcpsrv/cfg_option_def.h>
+#include <testutils/test_to_element.h>
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+
+namespace {
+
+// This test checks that two option definition configurations can be
+// compared for equality.
+TEST(CfgOptionDefTest, equal) {
+ CfgOptionDef cfg1;
+ CfgOptionDef cfg2;
+
+ // Default objects must be equal.
+ ASSERT_TRUE(cfg1 == cfg2);
+ ASSERT_FALSE(cfg1 != cfg2);
+
+ // Let's add the same option but to two different option spaces.
+ cfg1.add(OptionDefinitionPtr(new OptionDefinition("option-foo", 5, "isc",
+ "uint16")));
+ cfg2.add(OptionDefinitionPtr(new OptionDefinition("option-foo", 5, "dns",
+ "uint16")));
+ // Configurations must be unequal.
+ ASSERT_FALSE(cfg1 == cfg2);
+ ASSERT_TRUE(cfg1 != cfg2);
+
+ // Now, let's add them again but to original option spaces. Both objects
+ // should now contain the same options under two option spaces. The
+ // order should not matter so configurations should be equal.
+ cfg1.add(OptionDefinitionPtr(new OptionDefinition("option-foo", 5, "dns",
+ "uint16")));
+ cfg2.add(OptionDefinitionPtr(new OptionDefinition("option-foo", 5, "isc",
+ "uint16")));
+
+ EXPECT_TRUE(cfg1 == cfg2);
+ EXPECT_FALSE(cfg1 != cfg2);
+
+}
+
+// This test verifies that multiple option definitions can be added
+// under different option spaces and then removed.
+TEST(CfgOptionDefTest, getAllThenDelete) {
+ CfgOptionDef cfg;
+
+ // Create a set of option definitions with codes between 100 and 109.
+ for (uint16_t code = 100; code < 110; ++code) {
+ std::ostringstream option_name;
+ // Option name is unique, e.g. option-100, option-101 etc.
+ option_name << "option-" << code;
+ OptionDefinitionPtr def(new OptionDefinition(option_name.str(), code,
+ "isc", "uint16"));
+ // We're setting id of 123 for all option definitions in this
+ // code range.
+ def->setId(123);
+
+ // Add option definition to "isc" option space.
+ // Option codes are not duplicated so expect no error
+ // when adding them.
+ ASSERT_NO_THROW(cfg.add(def));
+ }
+
+ // Create a set of option definitions with codes between 105 and 114 and
+ // add them to the different option space.
+ for (uint16_t code = 105; code < 115; ++code) {
+ std::ostringstream option_name;
+ option_name << "option-" << code;
+ OptionDefinitionPtr def(new OptionDefinition(option_name.str(), code,
+ "abcde", "uint16"));
+ // We're setting id of 234 for all option definitions in this
+ // code range.
+ def->setId(234);
+
+ ASSERT_NO_THROW(cfg.add(def));
+ }
+
+ // Sanity check that all 10 option definitions are there.
+ OptionDefContainerPtr option_defs1 = cfg.getAll("isc");
+ ASSERT_TRUE(option_defs1);
+ ASSERT_EQ(10, option_defs1->size());
+
+ // Iterate over all option definitions and check that they have
+ // valid codes. Also, their order should be the same as they
+ // were added (codes 100-109).
+ uint16_t code = 100;
+ for (OptionDefContainer::const_iterator it = option_defs1->begin();
+ it != option_defs1->end(); ++it, ++code) {
+ OptionDefinitionPtr def(*it);
+ ASSERT_TRUE(def);
+ EXPECT_EQ(code, def->getCode());
+ }
+
+ // Sanity check that all 10 option definitions are there.
+ OptionDefContainerPtr option_defs2 = cfg.getAll("abcde");
+ ASSERT_TRUE(option_defs2);
+ ASSERT_EQ(10, option_defs2->size());
+
+ // Check that the option codes are valid.
+ code = 105;
+ for (OptionDefContainer::const_iterator it = option_defs2->begin();
+ it != option_defs2->end(); ++it, ++code) {
+ OptionDefinitionPtr def(*it);
+ ASSERT_TRUE(def);
+ EXPECT_EQ(code, def->getCode());
+ }
+
+ // Let's make one more check that the empty set is returned when
+ // invalid option space is used.
+ OptionDefContainerPtr option_defs3 = cfg.getAll("non-existing");
+ ASSERT_TRUE(option_defs3);
+ EXPECT_TRUE(option_defs3->empty());
+
+ // Check that we can delete option definitions by id.
+ uint64_t num_deleted = 0;
+ ASSERT_NO_THROW(num_deleted = cfg.del(123));
+ EXPECT_EQ(10, num_deleted);
+
+ option_defs1 = cfg.getAll("isc");
+ ASSERT_TRUE(option_defs1);
+ ASSERT_EQ(0, option_defs1->size());
+
+ option_defs2 = cfg.getAll("abcde");
+ ASSERT_TRUE(option_defs2);
+ ASSERT_EQ(10, option_defs2->size());
+
+ // Second attempt to delete the same option definitions should
+ // result in 0 deletions.
+ ASSERT_NO_THROW(num_deleted = cfg.del(123));
+ EXPECT_EQ(0, num_deleted);
+
+ // Delete all other option definitions.
+ ASSERT_NO_THROW(num_deleted = cfg.del(234));
+ EXPECT_EQ(10, num_deleted);
+
+ option_defs2 = cfg.getAll("abcde");
+ ASSERT_TRUE(option_defs2);
+ ASSERT_EQ(0, option_defs2->size());
+}
+
+// This test verifies that single option definition is correctly
+// returned with get function.
+TEST(CfgOptionDefTest, get) {
+ CfgOptionDef cfg;
+ // Create a set of option definitions with codes between 100 and 109.
+ for (uint16_t code = 100; code < 110; ++code) {
+ std::ostringstream option_name;
+ // Option name is unique, e.g. option-100, option-101 etc.
+ option_name << "option-" << code;
+ OptionDefinitionPtr def(new OptionDefinition(option_name.str(), code,
+ "isc", "uint16"));
+ // Add option definition to "isc" option space.
+ // Option codes are not duplicated so expect no error
+ // when adding them.
+ ASSERT_NO_THROW(cfg.add(def));
+ }
+
+ // Create a set of option definitions with codes between 105 and 114 and
+ // add them to the different option space.
+ for (uint16_t code = 105; code < 115; ++code) {
+ std::ostringstream option_name;
+ option_name << "option-other-" << code;
+ OptionDefinitionPtr def(new OptionDefinition(option_name.str(), code,
+ "abcde", "uint16"));
+ ASSERT_NO_THROW(cfg.add(def));
+ }
+
+ // Try to get option definitions one by one using all codes
+ // that we expect to be there.
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionDefinitionPtr def = cfg.get("isc", code);
+ ASSERT_TRUE(def);
+ // Check that the option name is in the format of 'option-[code]'.
+ // That way we make sure that the options that have the same codes
+ // within different option spaces are different.
+ std::ostringstream option_name;
+ option_name << "option-" << code;
+ EXPECT_EQ(option_name.str(), def->getName());
+ EXPECT_EQ(code, def->getCode());
+
+ // Try to get the same option definition using an option name as
+ // a key.
+ def = cfg.get("isc", option_name.str());
+ ASSERT_TRUE(def);
+ EXPECT_EQ(code, def->getCode());
+ }
+
+ // Check that the option codes are valid.
+ for (uint16_t code = 105; code < 115; ++code) {
+ OptionDefinitionPtr def = cfg.get("abcde", code);
+ ASSERT_TRUE(def);
+ // Check that the option name is in the format of 'option-other-[code]'.
+ // That way we make sure that the options that have the same codes
+ // within different option spaces are different.
+ std::ostringstream option_name;
+ option_name << "option-other-" << code;
+ EXPECT_EQ(option_name.str(), def->getName());
+ EXPECT_EQ(code, def->getCode());
+
+ // Try to get the same option definition using an option name as
+ // a key.
+ def = cfg.get("abcde", option_name.str());
+ ASSERT_TRUE(def);
+ EXPECT_EQ(code, def->getCode());
+ }
+
+ // Check that an option definition can be added to the standard
+ // (dhcp4 and dhcp6) option spaces when the option code is not
+ // reserved by the standard option.
+ OptionDefinitionPtr def6(new OptionDefinition("option-foo", 1000,
+ DHCP6_OPTION_SPACE,
+ "uint16"));
+ EXPECT_NO_THROW(cfg.add(def6));
+
+ OptionDefinitionPtr def4(new OptionDefinition("option-foo", 222,
+ DHCP4_OPTION_SPACE,
+ "uint16"));
+ EXPECT_NO_THROW(cfg.add(def4));
+
+ // Try to query the option definition from an non-existing
+ // option space and expect NULL pointer.
+ OptionDefinitionPtr def = cfg.get("non-existing", 56);
+ EXPECT_FALSE(def);
+
+ // Try to get the non-existing option definition from an
+ // existing option space.
+ EXPECT_FALSE(cfg.get("isc", 56));
+}
+
+// This test verifies that it is not allowed to override a definition of the
+// standard option which has its definition defined in libdhcp++, but it is
+// allowed to create a definition for the standard option which doesn't have
+// its definition in libdhcp++.
+TEST(CfgOptionDefTest, overrideStdOptionDef) {
+ CfgOptionDef cfg;
+ OptionDefinitionPtr def;
+ // There is a definition for routers option in libdhcp++, so an attempt
+ // to add (override) another definition for this option should fail.
+ def.reset(new OptionDefinition("routers", DHO_ROUTERS,
+ DHCP4_OPTION_SPACE, "uint32"));
+ EXPECT_THROW(cfg.add(def), isc::BadValue);
+
+ // Check code duplicate (same code, different name).
+ def.reset(new OptionDefinition("routers-bis", DHO_ROUTERS,
+ DHCP4_OPTION_SPACE, "uint32"));
+ EXPECT_THROW(cfg.add(def), isc::BadValue);
+
+ // Check name duplicate (different code, same name).
+ def.reset(new OptionDefinition("routers", 170, DHCP4_OPTION_SPACE,
+ "uint32"));
+ EXPECT_THROW(cfg.add(def), isc::BadValue);
+
+ /// There is no definition for unassigned option 170.
+ def.reset(new OptionDefinition("unassigned-option-170", 170,
+ DHCP4_OPTION_SPACE, "string"));
+ EXPECT_NO_THROW(cfg.add(def));
+
+ // It is not allowed to override the definition of the option which
+ // has its definition in the libdhcp++.
+ def.reset(new OptionDefinition("sntp-servers", D6O_SNTP_SERVERS,
+ DHCP6_OPTION_SPACE, "ipv4-address"));
+ EXPECT_THROW(cfg.add(def), isc::BadValue);
+ // There is no definition for option 163 in libdhcp++ yet, so it should
+ // be possible provide a custom definition.
+ def.reset(new OptionDefinition("geolocation", 163, DHCP6_OPTION_SPACE,
+ "uint32"));
+ EXPECT_NO_THROW(cfg.add(def));
+}
+
+// This test verifies that the function that adds new option definition
+// throws exceptions when arguments are invalid.
+TEST(CfgOptionDefTest, addNegative) {
+ CfgOptionDef cfg;
+
+ OptionDefinitionPtr def(new OptionDefinition("option-foo", 1000, "isc",
+ "uint16"));
+
+ // Try NULL option definition.
+ ASSERT_THROW(cfg.add(OptionDefinitionPtr()),
+ isc::dhcp::MalformedOptionDefinition);
+ // Try adding option definition twice and make sure that it
+ // fails on the second attempt.
+ ASSERT_NO_THROW(cfg.add(def));
+ EXPECT_THROW(cfg.add(def), DuplicateOptionDefinition);
+}
+
+// This test verifies that the function that unparses configuration
+// works as expected.
+TEST(CfgOptionDefTest, unparse) {
+ CfgOptionDef cfg;
+
+ // Add some options.
+ cfg.add(OptionDefinitionPtr(new
+ OptionDefinition("option-foo", 5, "isc", "uint16")));
+ cfg.add(OptionDefinitionPtr(new
+ OptionDefinition("option-bar", 5, "dns", "uint16", true)));
+ cfg.add(OptionDefinitionPtr(new
+ OptionDefinition("option-baz", 6, "isc", "uint16", "dns")));
+ OptionDefinitionPtr rec(new OptionDefinition("option-rec", 6, "dns", "record"));
+ std::string json = "{ \"comment\": \"foo\", \"bar\": 1 }";
+ rec->setContext(data::Element::fromJSON(json));
+ rec->addRecordField("uint16");
+ rec->addRecordField("uint16");
+ cfg.add(rec);
+
+ // Unparse
+ std::string expected = "[\n"
+ "{\n"
+ " \"name\": \"option-bar\",\n"
+ " \"code\": 5,\n"
+ " \"type\": \"uint16\",\n"
+ " \"array\": true,\n"
+ " \"record-types\": \"\",\n"
+ " \"encapsulate\": \"\",\n"
+ " \"space\": \"dns\"\n"
+ "},{\n"
+ " \"name\": \"option-rec\",\n"
+ " \"code\": 6,\n"
+ " \"type\": \"record\",\n"
+ " \"array\": false,\n"
+ " \"record-types\": \"uint16, uint16\",\n"
+ " \"encapsulate\": \"\",\n"
+ " \"space\": \"dns\",\n"
+ " \"user-context\": { \"comment\": \"foo\", \"bar\": 1 }\n"
+ "},{\n"
+ " \"name\": \"option-foo\",\n"
+ " \"code\": 5,\n"
+ " \"type\": \"uint16\",\n"
+ " \"array\": false,\n"
+ " \"record-types\": \"\",\n"
+ " \"encapsulate\": \"\",\n"
+ " \"space\": \"isc\"\n"
+ "},{\n"
+ " \"name\": \"option-baz\",\n"
+ " \"code\": 6,\n"
+ " \"type\": \"uint16\",\n"
+ " \"array\": false,\n"
+ " \"record-types\": \"\",\n"
+ " \"encapsulate\": \"dns\",\n"
+ " \"space\": \"isc\"\n"
+ "}]\n";
+ isc::test::runToElementTest<CfgOptionDef>(expected, cfg);
+}
+
+// This test verifies that configured option definitions can be merged
+// correctly.
+TEST(CfgOptionDefTest, merge) {
+ CfgOptionDef to; // Configuration we are merging to.
+
+ // Add some options to the "to" config.
+ to.add((OptionDefinitionPtr(new OptionDefinition("one", 1, "isc", "uint16"))));
+ to.add((OptionDefinitionPtr(new OptionDefinition("two", 2, "isc", "uint16"))));
+ to.add((OptionDefinitionPtr(new OptionDefinition("three", 3, "fluff", "uint16"))));
+ to.add((OptionDefinitionPtr(new OptionDefinition("four", 4, "fluff", "uint16"))));
+
+ // Clone the "to" config and use that for merging.
+ CfgOptionDef to_clone;
+ to.copyTo(to_clone);
+
+ // Ensure they are equal before we do anything.
+ ASSERT_TRUE(to.equals(to_clone));
+
+ // Merge from an empty config.
+ CfgOptionDef empty_from;
+ ASSERT_NO_THROW(to_clone.merge(empty_from));
+
+ // Should have the same content as before.
+ ASSERT_TRUE(to.equals(to_clone));
+
+ // Construct a non-empty "from" config.
+ // Options "two" and "three" will be updated definitions and "five" will be new.
+ CfgOptionDef from;
+ from.add((OptionDefinitionPtr(new OptionDefinition("two", 22, "isc", "uint16"))));
+ from.add((OptionDefinitionPtr(new OptionDefinition("three", 3, "fluff", "string"))));
+ from.add((OptionDefinitionPtr(new OptionDefinition("five", 5, "fluff", "uint16"))));
+
+ // Now let's clone "from" config and use that manually construct the expected config.
+ CfgOptionDef expected;
+ from.copyTo(expected);
+ expected.add((OptionDefinitionPtr(new OptionDefinition("one", 1, "isc", "uint16"))));
+ expected.add((OptionDefinitionPtr(new OptionDefinition("four", 4, "fluff", "uint16"))));
+
+ // Do the merge.
+ ASSERT_NO_THROW(to_clone.merge(from));
+
+ // Verify we have the expected content.
+ ASSERT_TRUE(expected.equals(to_clone));
+}
+
+}
diff --git a/src/lib/dhcpsrv/tests/cfg_option_unittest.cc b/src/lib/dhcpsrv/tests/cfg_option_unittest.cc
new file mode 100644
index 0000000..2e1acb2
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_option_unittest.cc
@@ -0,0 +1,1148 @@
+// Copyright (C) 2014-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <cc/data.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/option.h>
+#include <dhcp/option_custom.h>
+#include <dhcp/option_int.h>
+#include <dhcp/option_int_array.h>
+#include <dhcp/option_space.h>
+#include <dhcp/option_string.h>
+#include <dhcp/option4_addrlst.h>
+#include <dhcpsrv/cfg_option.h>
+#include <testutils/gtest_utils.h>
+#include <testutils/test_to_element.h>
+#include <boost/foreach.hpp>
+#include <boost/pointer_cast.hpp>
+#include <gtest/gtest.h>
+#include <iterator>
+#include <limits>
+#include <list>
+#include <sstream>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+
+namespace {
+
+// This test verifies that the OptionDescriptor factory function creates a
+// valid instance.
+TEST(OptionDescriptorTest, create) {
+ OptionPtr option = Option::create(Option::V4, 234);
+ ElementPtr context = Element::createMap();
+ context->set("name", Element::create("value"));
+ auto desc = OptionDescriptor::create(option, true, "value", context);
+
+ ASSERT_TRUE(desc);
+ EXPECT_EQ(option, desc->option_);
+ EXPECT_TRUE(desc->persistent_);
+ EXPECT_EQ("value", desc->formatted_value_);
+ EXPECT_EQ(context, desc->getContext());
+}
+
+// This test verifies that the OptionDescriptor factory function variant
+// taking persistent flag as an argument creates valid instance.
+TEST(OptionDescriptorTest, createPersistent) {
+ auto desc = OptionDescriptor::create(true);
+ ASSERT_TRUE(desc);
+
+ EXPECT_FALSE(desc->option_);
+ EXPECT_TRUE(desc->persistent_);
+ EXPECT_TRUE(desc->formatted_value_.empty());
+ EXPECT_FALSE(desc->getContext());
+}
+
+// This test verifies that the OptionDescriptor factory function variant
+// copying a descriptor provided as an argument creates valid instance.
+TEST(OptionDescriptorTest, createCopy) {
+ OptionPtr option = Option::create(Option::V4, 234);
+ ElementPtr context = Element::createMap();
+ context->set("name", Element::create("value"));
+ auto desc = OptionDescriptor::create(option, true, "value", context);
+
+ auto desc_copy = OptionDescriptor::create(*desc);
+ ASSERT_TRUE(desc_copy);
+
+ ASSERT_TRUE(desc_copy);
+ EXPECT_EQ(option, desc_copy->option_);
+ EXPECT_TRUE(desc_copy->persistent_);
+ EXPECT_EQ("value", desc_copy->formatted_value_);
+ EXPECT_EQ(context, desc_copy->getContext());
+}
+
+// This test verifies that the OptionDescriptor assignment operator
+// does the shallow copy.
+TEST(OptionDescriptorTest, assign) {
+ // Create a persistent option descriptor.
+ auto desc = OptionDescriptor::create(true);
+ ASSERT_TRUE(desc);
+
+ // Create another option descriptor.
+ OptionPtr option = Option::create(Option::V4, 234);
+ ElementPtr context = Element::createMap();
+ context->set("name", Element::create("value"));
+ auto desc1 = OptionDescriptor::create(option, true, "value", context);
+ ASSERT_TRUE(desc1);
+
+ // Assign the option descriptor.
+ desc = desc1;
+
+ // Check it.
+ ASSERT_TRUE(desc);
+ EXPECT_EQ(option, desc->option_);
+ EXPECT_TRUE(desc->persistent_);
+ EXPECT_EQ("value", desc->formatted_value_);
+ EXPECT_EQ(context, desc->getContext());
+}
+
+/// This class fixture for testing @c CfgOption class, holding option
+/// configuration.
+class CfgOptionTest : public ::testing::Test {
+public:
+
+ /// @brief Generates encapsulated options and adds them to CfgOption
+ ///
+ /// This method generates the following options:
+ /// - 1000-1019 options: uint16 with value 1234, encapsulate "foo"
+ /// - 1-19 options: uint8 with value 1, encapsulate "foo-subs"
+ /// - 1-9 options: uint8 with value 3
+ /// - 1020-1039 options: uint16 with value 2345, encapsulate "bar"
+ /// - 100-119 options: uint8 with value 2, encapsulate "bar-subs"
+ /// - 501-509 options: uint8 with value 4
+ void generateEncapsulatedOptions(CfgOption& cfg) {
+ // Create top-level options encapsulating "foo" option space.
+ for (uint16_t code = 1000; code < 1020; ++code) {
+ OptionUint16Ptr option = OptionUint16Ptr(new OptionUint16(Option::V6,
+ code, 1234));
+ option->setEncapsulatedSpace("foo");
+
+ // In order to easier identify the options by id, let's use the option
+ // code as the id.
+ ASSERT_NO_THROW(cfg.add(option, false, DHCP6_OPTION_SPACE,
+ static_cast<uint64_t>(code)));
+ }
+
+ // Create top level options encapsulating "bar" option space.
+ for (uint16_t code = 1020; code < 1040; ++code) {
+ OptionUint16Ptr option = OptionUint16Ptr(new OptionUint16(Option::V6,
+ code, 2345));
+ option->setEncapsulatedSpace("bar");
+ ASSERT_NO_THROW(cfg.add(option, false, DHCP6_OPTION_SPACE,
+ static_cast<uint64_t>(code)));
+ }
+
+ // Create sub-options belonging to "foo" option space and encapsulating
+ // foo-subs option space.
+ for (uint16_t code = 1; code < 20; ++code) {
+ OptionUint8Ptr option = OptionUint8Ptr(new OptionUint8(Option::V6, code,
+ 0x01));
+ option->setEncapsulatedSpace("foo-subs");
+ ASSERT_NO_THROW(cfg.add(option, false, "foo", static_cast<uint64_t>(code)));
+ }
+
+ // Create sub-options belonging to "bar" option space and encapsulating
+ // bar-subs option space.
+ for (uint16_t code = 100; code < 119; ++code) {
+ OptionUint8Ptr option = OptionUint8Ptr(new OptionUint8(Option::V6,
+ code, 0x02));
+ option->setEncapsulatedSpace("bar-subs");
+ ASSERT_NO_THROW(cfg.add(option, false, "bar", static_cast<uint64_t>(code)));
+ }
+
+ // Create sub-options belonging to "foo-subs" option space.
+ for (uint16_t code = 1; code < 10; ++code) {
+ OptionUint8Ptr option = OptionUint8Ptr(new OptionUint8(Option::V6, code,
+ 0x03));
+ ASSERT_NO_THROW(cfg.add(option, false, "foo-subs",
+ static_cast<uint64_t>(code)));
+ }
+
+ // Create sub-options belonging to "bar-subs" option space.
+ for (uint16_t code = 501; code < 510; ++code) {
+ OptionUint8Ptr option = OptionUint8Ptr(new OptionUint8(Option::V6,
+ code, 0x04));
+ ASSERT_NO_THROW(cfg.add(option, false, "bar-subs",
+ static_cast<uint64_t>(code)));
+ }
+ }
+};
+
+// This test verifies the empty predicate.
+TEST_F(CfgOptionTest, empty) {
+ CfgOption cfg1;
+ CfgOption cfg2;
+
+ // Initially the option configurations should be empty.
+ ASSERT_TRUE(cfg1.empty());
+ ASSERT_TRUE(cfg2.empty());
+
+ // Add an option to each configuration
+ OptionPtr option(new Option(Option::V6, 1));
+ ASSERT_NO_THROW(cfg1.add(option, false, DHCP6_OPTION_SPACE));
+ ASSERT_NO_THROW(cfg2.add(option, true, "isc"));
+
+ // The first option configuration has an option
+ ASSERT_FALSE(cfg1.empty());
+
+ // The second option configuration has a vendor option
+ ASSERT_FALSE(cfg2.empty());
+}
+
+// This test verifies that the option configurations can be compared.
+TEST_F(CfgOptionTest, equals) {
+ CfgOption cfg1;
+ CfgOption cfg2;
+
+ // Initially the configurations should be equal.
+ ASSERT_TRUE(cfg1 == cfg2);
+ ASSERT_FALSE(cfg1 != cfg2);
+
+ // Add 9 options to two different option spaces. Each option have different
+ // option code and content.
+ for (uint16_t code = 1; code < 10; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, code)));
+ ASSERT_NO_THROW(cfg1.add(option, false, "isc"));
+ ASSERT_NO_THROW(cfg1.add(option, true, "vendor-123"));
+ }
+
+ // Configurations should now be different.
+ ASSERT_FALSE(cfg1 == cfg2);
+ ASSERT_TRUE(cfg1 != cfg2);
+
+ // Add 8 options (excluding the option with code 1) to the same option
+ // spaces.
+ for (uint16_t code = 2; code < 10; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, code)));
+ ASSERT_NO_THROW(cfg2.add(option, false, "isc"));
+ ASSERT_NO_THROW(cfg2.add(option, true, "vendor-123"));
+ }
+
+ // Configurations should still be unequal.
+ ASSERT_FALSE(cfg1 == cfg2);
+ ASSERT_TRUE(cfg1 != cfg2);
+
+ // Add missing option to the option space isc.
+ ASSERT_NO_THROW(cfg2.add(OptionPtr(new Option(Option::V6, 1,
+ OptionBuffer(10, 0x01))),
+ false, "isc"));
+ // Configurations should still be unequal because option with code 1
+ // is missing in the option space vendor-123.
+ ASSERT_FALSE(cfg1 == cfg2);
+ ASSERT_TRUE(cfg1 != cfg2);
+
+ // Add missing option.
+ ASSERT_NO_THROW(cfg2.add(OptionPtr(new Option(Option::V6, 1,
+ OptionBuffer(10, 0x01))),
+ true, "vendor-123"));
+ // Configurations should now be equal.
+ ASSERT_TRUE(cfg1 == cfg2);
+ ASSERT_FALSE(cfg1 != cfg2);
+
+}
+
+// This test verifies that multiple options can be added to the configuration
+// and that they can be retrieved using the option space name.
+TEST_F(CfgOptionTest, add) {
+ CfgOption cfg;
+
+ // Differentiate options by their codes (100-109)
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(cfg.add(option, false, DHCP6_OPTION_SPACE));
+ }
+
+ // Add 7 options to another option space. The option codes partially overlap
+ // with option codes that we have added to dhcp6 option space.
+ for (uint16_t code = 105; code < 112; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(cfg.add(option, false, "isc"));
+ }
+
+ // Get options from the Subnet and check if all 10 are there.
+ OptionContainerPtr options = cfg.getAll(DHCP6_OPTION_SPACE);
+ ASSERT_TRUE(options);
+ ASSERT_EQ(10, options->size());
+
+ // Validate codes of options added to dhcp6 option space.
+ uint16_t expected_code = 100;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ options = cfg.getAll("isc");
+ ASSERT_TRUE(options);
+ ASSERT_EQ(7, options->size());
+
+ // Validate codes of options added to isc option space.
+ expected_code = 105;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ // Try to get options from a non-existing option space.
+ options = cfg.getAll("abcd");
+ ASSERT_TRUE(options);
+ EXPECT_TRUE(options->empty());
+}
+
+// This test verifies that options can be replaced with updated content.
+TEST_F(CfgOptionTest, replace) {
+ CfgOption cfg;
+
+ // Let's add some options to the config to the config.
+ OptionStringPtr option(new OptionString(Option::V6, 1, "one"));
+ ASSERT_NO_THROW(cfg.add(option, false, "isc"));
+
+ option.reset(new OptionString(Option::V6, 2, "two"));
+ ASSERT_NO_THROW(cfg.add(option, false, "isc"));
+
+ option.reset(new OptionString(Option::V6, 3, "three"));
+ ASSERT_NO_THROW(cfg.add(option, false, "isc"));
+
+ // Now let's make sure we can find them and they are as expected.
+ OptionDescriptor desc = cfg.get("isc", 1);
+ ASSERT_TRUE(desc.option_);
+ OptionStringPtr opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("one", opstr->getValue());
+
+ desc = cfg.get("isc", 2);
+ ASSERT_TRUE(desc.option_);
+ opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("two", opstr->getValue());
+
+ desc = cfg.get("isc", 3);
+ ASSERT_TRUE(desc.option_);
+ opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("three", opstr->getValue());
+
+ // Now let's replace one and three.
+ desc.option_.reset(new OptionString(Option::V6, 1, "new one"));
+ ASSERT_NO_THROW(cfg.replace(desc, "isc"));
+
+ desc.option_.reset(new OptionString(Option::V6, 3, "new three"));
+ ASSERT_NO_THROW(cfg.replace(desc, "isc"));
+
+ // Now let's make sure we can find them again and they are as expected.
+ desc = cfg.get("isc", 1);
+ ASSERT_TRUE(desc.option_);
+ opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("new one", opstr->getValue());
+
+ desc = cfg.get("isc", 2);
+ ASSERT_TRUE(desc.option_);
+ opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("two", opstr->getValue());
+
+ desc = cfg.get("isc", 3);
+ ASSERT_TRUE(desc.option_);
+ opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("new three", opstr->getValue());
+}
+
+// This test verifies that one configuration can be merged into another.
+TEST_F(CfgOptionTest, mergeTo) {
+ CfgOption cfg_src;
+ CfgOption cfg_dst;
+
+ // Create collection of options in option space dhcp6, with option codes
+ // from the range of 100 to 109 and holding one byte of data equal to 0xFF.
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0xFF)));
+ ASSERT_NO_THROW(cfg_src.add(option, false, DHCP6_OPTION_SPACE));
+ }
+
+ // Create collection of options in vendor space 123, with option codes
+ // from the range of 100 to 109 and holding one byte of data equal to 0xFF.
+ for (uint16_t code = 100; code < 110; code += 2) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0xFF)));
+ ASSERT_NO_THROW(cfg_src.add(option, false, "vendor-123"));
+ }
+
+ // Create destination configuration (configuration that we merge the
+ // other configuration to).
+
+ // Create collection of options having even option codes in the range of
+ // 100 to 108.
+ for (uint16_t code = 100; code < 110; code += 2) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0x01)));
+ ASSERT_NO_THROW(cfg_dst.add(option, false, DHCP6_OPTION_SPACE));
+ }
+
+ // Create collection of options having odd option codes in the range of
+ // 101 to 109.
+ for (uint16_t code = 101; code < 110; code += 2) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0x01)));
+ ASSERT_NO_THROW(cfg_dst.add(option, false, "vendor-123"));
+ }
+
+ // Merge source configuration to the destination configuration. The options
+ // in the destination should be preserved. The options from the source
+ // configuration should be added.
+ ASSERT_NO_THROW(cfg_src.mergeTo(cfg_dst));
+
+ // Validate the options in the dhcp6 option space in the destination.
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionDescriptor desc = cfg_dst.get(DHCP6_OPTION_SPACE, code);
+ ASSERT_TRUE(desc.option_);
+ ASSERT_EQ(1, desc.option_->getData().size());
+ // The options with even option codes should hold one byte of data
+ // equal to 0x1. These are the ones that we have initially added to
+ // the destination configuration. The other options should hold the
+ // values of 0xFF which indicates that they have been merged from the
+ // source configuration.
+ if ((code % 2) == 0) {
+ EXPECT_EQ(0x01, desc.option_->getData()[0]);
+ } else {
+ EXPECT_EQ(0xFF, desc.option_->getData()[0]);
+ }
+ }
+
+ // Validate the options in the vendor space.
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionDescriptor desc = cfg_dst.get(123, code);
+ ASSERT_TRUE(desc.option_);
+ ASSERT_EQ(1, desc.option_->getData().size());
+ // This time, the options with even option codes should hold a byte
+ // of data equal to 0xFF. The other options should hold the byte of
+ // data equal to 0x01.
+ if ((code % 2) == 0) {
+ EXPECT_EQ(0xFF, desc.option_->getData()[0]);
+ } else {
+ EXPECT_EQ(0x01, desc.option_->getData()[0]);
+ }
+ }
+}
+
+// This test verifies that the options configuration can be copied between
+// objects.
+TEST_F(CfgOptionTest, copy) {
+ CfgOption cfg_src;
+ // Add 10 options to the custom option space in the source configuration.
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0x01)));
+ ASSERT_NO_THROW(cfg_src.add(option, false, "foo"));
+ }
+
+ CfgOption cfg_dst;
+ // Add 20 options to the custom option space in destination configuration.
+ for (uint16_t code = 100; code < 120; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0xFF)));
+ ASSERT_NO_THROW(cfg_dst.add(option, false, "isc"));
+ }
+
+ // Copy entire configuration to the destination. This should override any
+ // existing data.
+ ASSERT_NO_THROW(cfg_src.copyTo(cfg_dst));
+
+ // Validate options in the destination configuration.
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionDescriptor desc = cfg_dst.get("foo", code);
+ ASSERT_TRUE(desc.option_);
+ ASSERT_EQ(1, desc.option_->getData().size());
+ EXPECT_EQ(0x01, desc.option_->getData()[0]);
+ }
+
+ // Any existing options should be removed.
+ OptionContainerPtr container = cfg_dst.getAll("isc");
+ ASSERT_TRUE(container);
+ EXPECT_TRUE(container->empty());
+
+ // The option space "foo" should contain exactly 10 options.
+ container = cfg_dst.getAll("foo");
+ ASSERT_TRUE(container);
+ EXPECT_EQ(10, container->size());
+}
+
+// This test verifies that DHCP options from one configuration
+// can be used to update options in another configuration.
+// In other words, options from an "other" configuration
+// can be merged into an existing configuration, with any
+// duplicates in other overwriting those in the existing
+// configuration.
+TEST_F(CfgOptionTest, validMerge) {
+ CfgOption this_cfg;
+ CfgOption other_cfg;
+
+ // We need to create a dictionary of definitions pass into option merge.
+ CfgOptionDefPtr defs(new CfgOptionDef());
+ defs->add((OptionDefinitionPtr(new OptionDefinition("one", 1, "isc", "uint8"))));
+ defs->add((OptionDefinitionPtr(new OptionDefinition("two", 2, "isc", "uint8"))));
+ defs->add((OptionDefinitionPtr(new OptionDefinition("four", 4, "isc", "uint8"))));
+ defs->add((OptionDefinitionPtr(new OptionDefinition("three", 3, "fluff", "uint8"))));
+ defs->add((OptionDefinitionPtr(new OptionDefinition("four", 4, "fluff", "uint8"))));
+
+ // Create our existing config, that gets merged into.
+ OptionPtr option(new Option(Option::V4, 1, OptionBuffer(1, 0x01)));
+ ASSERT_NO_THROW(this_cfg.add(option, false, "isc"));
+
+ // Add option 3 to "fluff"
+ option.reset(new Option(Option::V4, 3, OptionBuffer(1, 0x03)));
+ ASSERT_NO_THROW(this_cfg.add(option, false, "fluff"));
+
+ // Add option 4 to "fluff".
+ option.reset(new Option(Option::V4, 4, OptionBuffer(1, 0x04)));
+ ASSERT_NO_THROW(this_cfg.add(option, false, "fluff"));
+
+ // Create our other config that will be merged from.
+ // Add Option 1 to "isc", this should "overwrite" the original.
+ option.reset(new Option(Option::V4, 1, OptionBuffer(1, 0x10)));
+ ASSERT_NO_THROW(other_cfg.add(option, false, "isc"));
+
+ // Add option 2 to "isc".
+ option.reset(new Option(Option::V4, 2, OptionBuffer(1, 0x20)));
+ ASSERT_NO_THROW(other_cfg.add(option, false, "isc"));
+
+ // Add option 4 to "isc".
+ option.reset(new Option(Option::V4, 4, OptionBuffer(1, 0x40)));
+ ASSERT_NO_THROW(other_cfg.add(option, false, "isc"));
+
+ // Merge source configuration to the destination configuration. The options
+ // in the destination should be preserved. The options from the source
+ // configuration should be added.
+ ASSERT_NO_THROW(this_cfg.merge(defs, other_cfg));
+
+ // isc:1 should come from "other" config.
+ OptionDescriptor desc = this_cfg.get("isc", 1);
+ ASSERT_TRUE(desc.option_);
+ OptionUint8Ptr opint = boost::dynamic_pointer_cast<OptionUint8>(desc.option_);
+ ASSERT_TRUE(opint);
+ EXPECT_EQ(16, opint->getValue());
+
+ // isc:2 should come from "other" config.
+ desc = this_cfg.get("isc", 2);
+ ASSERT_TRUE(desc.option_);
+ opint = boost::dynamic_pointer_cast<OptionUint8>(desc.option_);
+ ASSERT_TRUE(opint);
+ EXPECT_EQ(32, opint->getValue());
+
+ // isc:4 should come from "other" config.
+ desc = this_cfg.get("isc", 4);
+ ASSERT_TRUE(desc.option_);
+ opint = boost::dynamic_pointer_cast<OptionUint8>(desc.option_);
+ ASSERT_TRUE(opint);
+ EXPECT_EQ(64, opint->getValue());
+
+ // fluff:3 should come from "this" config.
+ desc = this_cfg.get("fluff", 3);
+ ASSERT_TRUE(desc.option_);
+ opint = boost::dynamic_pointer_cast<OptionUint8>(desc.option_);
+ ASSERT_TRUE(opint);
+ EXPECT_EQ(3, opint->getValue());
+
+ // fluff:4 should come from "this" config.
+ desc = this_cfg.get("fluff", 4);
+ ASSERT_TRUE(desc.option_);
+ opint = boost::dynamic_pointer_cast<OptionUint8>(desc.option_);
+ ASSERT_TRUE(opint);
+ EXPECT_EQ(4, opint->getValue());
+}
+
+// This test verifies that attempting to merge options
+// which are by incompatible with "known" option definitions
+// are detected.
+TEST_F(CfgOptionTest, mergeInvalid) {
+ CfgOption this_cfg;
+ CfgOption other_cfg;
+
+ // Create an empty dictionary of defintions pass into option merge.
+ CfgOptionDefPtr defs(new CfgOptionDef());
+
+ // Create our other config that will be merged from.
+ // Add an option without a formatted value.
+ OptionPtr option(new Option(Option::V4, 1, OptionBuffer(1, 0x01)));
+ ASSERT_NO_THROW(other_cfg.add(option, false, "isc"));
+
+ // Add an option with a formatted value.
+ option.reset(new Option(Option::V4, 2));
+ OptionDescriptor desc(option, false, "one,two,three");
+ ASSERT_NO_THROW(other_cfg.add(desc, "isc"));
+
+ // When we attempt to merge, it should fail, recognizing that
+ // option 2, which has a formatted value, has no definition.
+ ASSERT_THROW_MSG(this_cfg.merge(defs, other_cfg), InvalidOperation,
+ "option: isc.2 has a formatted value: "
+ "'one,two,three' but no option definition");
+
+ // Now let's add an option definition that will force data truncated
+ // error for option 1.
+ defs->add((OptionDefinitionPtr(new OptionDefinition("one", 1, "isc", "uint16"))));
+
+ // When we attempt to merge, it should fail because option 1's data
+ // is not valid per its definition.
+ EXPECT_THROW_MSG(this_cfg.merge(defs, other_cfg), InvalidOperation,
+ "could not create option: isc.1"
+ " from data specified, reason:"
+ " OptionInt 1 truncated");
+}
+
+// This test verifies the all of the valid option cases
+// in createDescriptorOption():
+// 1. standard option
+// 2. vendor option
+// 3. user-defined, unformatted option
+// 4. user-defined, formatted option
+// 5. undefined, unformatted option
+TEST_F(CfgOptionTest, createDescriptorOptionValid) {
+ // First we'll create our "known" user definitions
+ CfgOptionDefPtr defs(new CfgOptionDef());
+ defs->add((OptionDefinitionPtr(new OptionDefinition("one", 1, "isc", "uint8"))));
+ defs->add((OptionDefinitionPtr(new OptionDefinition("two", 2, "isc", "uint8", true))));
+
+ // We'll try a standard V4 option first.
+ std::string space = DHCP4_OPTION_SPACE;
+ std::string value = "v4.example.com";
+ OptionPtr option(new Option(Option::V6, DHO_HOST_NAME));
+ option->setData(value.begin(), value.end());
+ OptionDescriptorPtr desc(new OptionDescriptor(option, false));
+
+ bool updated = false;
+ ASSERT_NO_THROW(updated = CfgOption::createDescriptorOption(defs, space, *desc));
+ ASSERT_TRUE(updated);
+ OptionStringPtr opstr = boost::dynamic_pointer_cast<OptionString>(desc->option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("v4.example.com", opstr->getValue());
+
+ // Next we'll try a standard V6 option.
+ space = DHCP6_OPTION_SPACE;
+ std::vector<uint8_t> fqdn =
+ { 2, 'v', '6', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0 };
+ option.reset(new Option(Option::V6, D6O_AFTR_NAME));
+ option->setData(fqdn.begin(), fqdn.end());
+ desc.reset(new OptionDescriptor(option, false));
+
+ ASSERT_NO_THROW(updated = CfgOption::createDescriptorOption(defs, space, *desc));
+ ASSERT_TRUE(updated);
+ OptionCustomPtr opcustom = boost::dynamic_pointer_cast<OptionCustom>(desc->option_);
+ ASSERT_TRUE(opcustom);
+ EXPECT_EQ("v6.example.com.", opcustom->readFqdn());
+
+ // Next we'll try a vendor option with a formatted value
+ space = "vendor-4491";
+ value = "192.0.2.1, 192.0.2.2";
+ option.reset(new Option(Option::V4, 2));
+ desc.reset(new OptionDescriptor(option, false, value));
+
+ ASSERT_NO_THROW(updated = CfgOption::createDescriptorOption(defs, space, *desc));
+ ASSERT_TRUE(updated);
+ Option4AddrLstPtr opaddrs = boost::dynamic_pointer_cast<Option4AddrLst>(desc->option_);
+ ASSERT_TRUE(opaddrs);
+ Option4AddrLst::AddressContainer exp_addresses = { IOAddress("192.0.2.1"), IOAddress("192.0.2.2") };
+ EXPECT_EQ(exp_addresses, opaddrs->getAddresses());
+
+ // Now, a user defined uint8 option
+ space = "isc";
+ option.reset(new Option(Option::V4, 1, OptionBuffer(1, 0x77)));
+ desc.reset(new OptionDescriptor(option, false));
+
+ ASSERT_NO_THROW(updated = CfgOption::createDescriptorOption(defs, space, *desc));
+ ASSERT_TRUE(updated);
+ OptionUint8Ptr opint = boost::dynamic_pointer_cast<OptionUint8>(desc->option_);
+ ASSERT_TRUE(opint);
+ EXPECT_EQ(119, opint->getValue());
+
+ // Now, a user defined array of ints from a formatted value
+ option.reset(new Option(Option::V4, 2));
+ desc.reset(new OptionDescriptor(option, false, "1,2,3"));
+
+ ASSERT_NO_THROW(updated = CfgOption::createDescriptorOption(defs, space, *desc));
+ ASSERT_TRUE(updated);
+ OptionUint8ArrayPtr oparray = boost::dynamic_pointer_cast<OptionUint8Array>(desc->option_);
+ ASSERT_TRUE(oparray);
+ std::vector<uint8_t> exp_ints = { 1, 2, 3 };
+ EXPECT_EQ(exp_ints, oparray->getValues());
+
+ // Finally, a generic, undefined option
+ option.reset(new Option(Option::V4, 199, OptionBuffer(1, 0x77)));
+ desc.reset(new OptionDescriptor(option, false));
+
+ ASSERT_NO_THROW(updated = CfgOption::createDescriptorOption(defs, space, *desc));
+ ASSERT_FALSE(updated);
+ ASSERT_EQ(1, desc->option_->getData().size());
+ EXPECT_EQ(0x77, desc->option_->getData()[0]);
+}
+
+// This test verifies that encapsulated options are added as sub-options
+// to the top level options on request.
+TEST_F(CfgOptionTest, encapsulate) {
+ CfgOption cfg;
+
+ generateEncapsulatedOptions(cfg);
+
+ // Append options from "foo" and "bar" space as sub-options and options
+ // from "foo-subs" and "bar-subs" as sub-options of "foo" and "bar"
+ // options.
+ ASSERT_NO_THROW(cfg.encapsulate());
+
+ // Verify that we have 40 top-level options.
+ OptionContainerPtr options = cfg.getAll(DHCP6_OPTION_SPACE);
+ ASSERT_EQ(40, options->size());
+
+ // Iterate over top level options.
+ for (uint16_t code = 1000; code < 1040; ++code) {
+
+ OptionUint16Ptr option = boost::dynamic_pointer_cast<
+ OptionUint16>(cfg.get(DHCP6_OPTION_SPACE, code).option_);
+ ASSERT_TRUE(option) << "option with code " << code << " not found";
+
+ // First level sub options. There are 19 sub-options for each top
+ // level option.
+ const OptionCollection& first_level = option->getOptions();
+ ASSERT_EQ(19, first_level.size());
+
+ // Iterate over all first level sub-options.
+ std::pair<unsigned int, OptionPtr> first_level_opt;
+ BOOST_FOREACH(first_level_opt, first_level) {
+ // Each option in this test comprises a single one byte field and
+ // should cast to OptionUint8 type.
+ OptionUint8Ptr first_level_uint8 = boost::dynamic_pointer_cast<
+ OptionUint8>(first_level_opt.second);
+ ASSERT_TRUE(first_level_uint8);
+
+ const unsigned int value = static_cast<unsigned int>(first_level_uint8->getValue());
+ // There are two sets of first level sub-options. Those that include
+ // a value of 1 and those that include a value of 2.
+ if (first_level_uint8->getType() < 20) {
+ EXPECT_EQ(1, value);
+ } else {
+ EXPECT_EQ(2, value);
+ }
+
+ // Each first level sub-option should include 9 second level
+ // sub options.
+ const OptionCollection& second_level = first_level_uint8->getOptions();
+ ASSERT_EQ(9, second_level.size());
+
+ // Iterate over sub-options and make sure they include the expected
+ // values.
+ std::pair<unsigned int, OptionPtr> second_level_opt;
+ BOOST_FOREACH(second_level_opt, second_level) {
+ OptionUint8Ptr second_level_uint8 = boost::dynamic_pointer_cast<
+ OptionUint8>(second_level_opt.second);
+ ASSERT_TRUE(second_level_uint8);
+ const unsigned value = static_cast<
+ unsigned>(second_level_uint8->getValue());
+ // Certain sub-options should have a value of 3, other the values
+ // of 4.
+ if (second_level_uint8->getType() < 20) {
+ EXPECT_EQ(3, value);
+ } else {
+ EXPECT_EQ(4, value);
+ }
+ }
+ }
+ }
+}
+
+// This test verifies that an option can be deleted from the configuration.
+TEST_F(CfgOptionTest, deleteOptions) {
+ CfgOption cfg;
+
+ generateEncapsulatedOptions(cfg);
+
+ // Append options from "foo" and "bar" space as sub-options and options
+ // from "foo-subs" and "bar-subs" as sub-options of "foo" and "bar"
+ // options.
+ ASSERT_NO_THROW(cfg.encapsulate());
+
+ // The option with the code 5 should exist in the option space "foo".
+ ASSERT_TRUE(cfg.get("foo", 5).option_);
+
+ // Because we called "encapsulate", this option should have been
+ // propagated to the options encapsulating option space "foo".
+ for (uint16_t code = 1000; code < 1020; ++code) {
+ OptionDescriptor top_level_option(false);
+ ASSERT_NO_THROW(top_level_option = cfg.get(DHCP6_OPTION_SPACE, code));
+ // Make sure that the option with code 5 is there.
+ ASSERT_TRUE(top_level_option.option_);
+ ASSERT_TRUE(top_level_option.option_->getOption(5));
+
+ // Make sure that options belonging to space "foo-subs" should contain
+ // options with the code of 5.
+ for (uint16_t code_subs = 1; code_subs != 19; ++code_subs) {
+ OptionPtr sub_option;
+ ASSERT_NO_THROW(sub_option = top_level_option.option_->getOption(code_subs));
+ ASSERT_TRUE(sub_option);
+ ASSERT_TRUE(sub_option->getOption(5));
+ }
+ }
+
+ // Delete option with the code 5 and belonging to option space "foo".
+ uint64_t deleted_num;
+ ASSERT_NO_THROW(deleted_num = cfg.del("foo", 5));
+ EXPECT_EQ(1, deleted_num);
+
+ // The option should now be gone from options config.
+ EXPECT_FALSE(cfg.get("foo", 5).option_);
+ // Option with the code of 5 in other option spaces should remain.
+ EXPECT_TRUE(cfg.get("foo-subs", 5).option_);
+
+ // Iterate over the options encapsulating "foo" option space. Make sure
+ // that the option with code 5 is no longer encapsulated by these options.
+ for (uint16_t code = 1000; code < 1020; ++code) {
+ OptionDescriptor top_level_option(false);
+ ASSERT_NO_THROW(top_level_option = cfg.get(DHCP6_OPTION_SPACE, code));
+ ASSERT_TRUE(top_level_option.option_);
+ EXPECT_FALSE(top_level_option.option_->getOption(5));
+ // Other options should remain.
+ EXPECT_TRUE(top_level_option.option_->getOption(1));
+
+ // Iterate over options within foo-subs option space and make sure
+ // that they still contain options with the code of 5.
+ for (uint16_t code_subs = 1; code_subs != 19; ++code_subs) {
+ // There shouldn't be option with the code of 5 in the "foo" space.
+ if (code_subs == 5) {
+ continue;
+ }
+
+ OptionPtr sub_option;
+ ASSERT_NO_THROW(sub_option = top_level_option.option_->getOption(code_subs));
+ // Options belonging to option space "foo-subs" should include option
+ // with the code of 5.
+ ASSERT_TRUE(sub_option);
+ ASSERT_TRUE(sub_option->getOption(5));
+ }
+ }
+}
+
+// This test verifies that an option can be deleted from the configuration
+// by database id.
+TEST_F(CfgOptionTest, deleteOptionsById) {
+ CfgOption cfg;
+
+ generateEncapsulatedOptions(cfg);
+
+ // Append options from "foo" and "bar" space as sub-options and options
+ // from "foo-subs" and "bar-subs" as sub-options of "foo" and "bar"
+ // options.
+ ASSERT_NO_THROW(cfg.encapsulate());
+
+ // Create multiple vendor options for vendor id 123.
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0xFF)));
+ ASSERT_NO_THROW(cfg.add(option, false, "vendor-123", static_cast<uint64_t>(code)));
+ }
+
+ // Delete options with id of 100. It includes both regular options and
+ // the vendor options. There are two options with id of 100.
+ EXPECT_EQ(2, cfg.del(100));
+
+ // Make sure that the option 100 was deleted but another option
+ // in the same option space was not.
+ EXPECT_FALSE(cfg.get("bar", 100).option_);
+ EXPECT_TRUE(cfg.get("bar", 101).option_);
+
+ // Make sure that the deleted option was dereferenced from the
+ // top level options but that does not affect encapsulation of
+ // other options.
+ for (uint16_t option_code = 1020; option_code < 1040; ++option_code) {
+ auto top_level_option = cfg.get(DHCP6_OPTION_SPACE, option_code);
+ ASSERT_TRUE(top_level_option.option_);
+ EXPECT_FALSE(top_level_option.option_->getOption(100));
+
+ // The second level encapsulation should have been preserved.
+ auto second_level_option = top_level_option.option_->getOption(101);
+ ASSERT_TRUE(second_level_option);
+ EXPECT_TRUE(second_level_option->getOption(501));
+ }
+
+ // Vendor option with id of 100 should have been deleted too.
+ EXPECT_FALSE(cfg.get(123, 100).option_);
+ EXPECT_TRUE(cfg.get(123, 101).option_);
+}
+
+// This test verifies that a vendor option can be deleted from the configuration.
+TEST_F(CfgOptionTest, delVendorOption) {
+ CfgOption cfg;
+
+ // Create multiple vendor options for vendor id 123.
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0xFF)));
+ ASSERT_NO_THROW(cfg.add(option, false, "vendor-123"));
+ }
+
+ // Create multiple vendor options for vendor id 234.
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0xFF)));
+ ASSERT_NO_THROW(cfg.add(option, false, "vendor-234"));
+ }
+
+ // Make sure that the option we're trying to delete is there.
+ ASSERT_TRUE(cfg.get(123, 105).option_);
+ // There should also be an option 105 for vendor id 234.
+ ASSERT_TRUE(cfg.get(234, 105).option_);
+
+ // Delete the option for vendor id 123.
+ uint64_t deleted_num;
+ ASSERT_NO_THROW(deleted_num = cfg.del(123, 105));
+ EXPECT_EQ(1, deleted_num);
+
+ // Make sure the option is gone.
+ EXPECT_FALSE(cfg.get(123, 105).option_);
+ // Make sure that the option with code 105 is not affected for vendor id 234.
+ EXPECT_TRUE(cfg.get(234, 105).option_);
+
+ // Other options, like 107, shouldn't be gone.
+ EXPECT_TRUE(cfg.get(123, 107).option_);
+}
+
+// This test verifies that single option can be retrieved from the configuration
+// using option code and option space.
+TEST_F(CfgOptionTest, get) {
+ CfgOption cfg;
+
+ // Add 10 options to a "dhcp6" option space in the subnet.
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(cfg.add(option, false, DHCP6_OPTION_SPACE));
+ }
+
+ // Check that we can get each added option descriptor using
+ // individually.
+ for (uint16_t code = 100; code < 110; ++code) {
+ std::ostringstream stream;
+ // First, try the invalid option space name.
+ OptionDescriptor desc = cfg.get("isc", code);
+ // Returned descriptor should contain NULL option ptr.
+ EXPECT_FALSE(desc.option_);
+ // Now, try the valid option space.
+ desc = cfg.get(DHCP6_OPTION_SPACE, code);
+ // Test that the option code matches the expected code.
+ ASSERT_TRUE(desc.option_);
+ EXPECT_EQ(code, desc.option_->getType());
+ }
+}
+
+// This test verifies that the same options can be added to the configuration
+// under different option space.
+TEST_F(CfgOptionTest, addNonUniqueOptions) {
+ CfgOption cfg;
+
+ // Create a set of options with non-unique codes.
+ for (int i = 0; i < 2; ++i) {
+ // In the inner loop we create options with unique codes (100-109).
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(cfg.add(option, false, DHCP6_OPTION_SPACE));
+ }
+ }
+
+ // Sanity check that all options are there.
+ OptionContainerPtr options = cfg.getAll(DHCP6_OPTION_SPACE);
+ ASSERT_EQ(20, options->size());
+
+ // Use container index #1 to get the options by their codes.
+ OptionContainerTypeIndex& idx = options->get<1>();
+ // Look for the codes 100-109.
+ for (uint16_t code = 100; code < 110; ++ code) {
+ // For each code we should get two instances of options->
+ OptionContainerTypeRange range = idx.equal_range(code);
+ // Distance between iterators indicates how many options
+ // have been returned for the particular code.
+ ASSERT_EQ(2, distance(range.first, range.second));
+ // Check that returned options actually have the expected option code.
+ for (OptionContainerTypeIndex::const_iterator option_desc = range.first;
+ option_desc != range.second; ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(code, option_desc->option_->getType());
+ }
+ }
+
+ // Let's try to find some non-exiting option.
+ const uint16_t non_existing_code = 150;
+ OptionContainerTypeRange range = idx.equal_range(non_existing_code);
+ // Empty set is expected.
+ EXPECT_EQ(0, distance(range.first, range.second));
+}
+
+// This test verifies that the option with the persistency flag can be
+// added to the configuration and that options with the persistency flags
+// can be retrieved.
+TEST(Subnet6Test, addPersistentOption) {
+ CfgOption cfg;
+
+ // Add 10 options to the subnet with option codes 100 - 109.
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ // We create 10 options and want some of them to be flagged
+ // persistent and some non-persistent. Persistent options are
+ // those that server sends to clients regardless if they ask
+ // for them or not. We pick 3 out of 10 options and mark them
+ // non-persistent and 7 other options persistent.
+ // Code values: 102, 105 and 108 are divisible by 3
+ // and options with these codes will be flagged non-persistent.
+ // Options with other codes will be flagged persistent.
+ bool persistent = (code % 3) ? true : false;
+ ASSERT_NO_THROW(cfg.add(option, persistent, DHCP6_OPTION_SPACE));
+ }
+
+ // Get added options from the subnet.
+ OptionContainerPtr options = cfg.getAll(DHCP6_OPTION_SPACE);
+
+ // options->get<2> returns reference to container index #2. This
+ // index is used to access options by the 'persistent' flag.
+ OptionContainerPersistIndex& idx = options->get<2>();
+
+ // Get all persistent options->
+ OptionContainerPersistRange range_persistent = idx.equal_range(true);
+ // 7 out of 10 options have been flagged persistent.
+ ASSERT_EQ(7, distance(range_persistent.first, range_persistent.second));
+
+ // Get all non-persistent options->
+ OptionContainerPersistRange range_non_persistent = idx.equal_range(false);
+ // 3 out of 10 options have been flagged not persistent.
+ ASSERT_EQ(3, distance(range_non_persistent.first, range_non_persistent.second));
+}
+
+// This test verifies that the vendor option can be added to the configuration.
+TEST_F(CfgOptionTest, addVendorOptions) {
+ CfgOption cfg;
+
+ // Differentiate options by their codes (100-109)
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(cfg.add(option, false, "vendor-12345678"));
+ }
+
+ // Second option space uses corner case value for vendor id = max uint8.
+ uint32_t vendor_id = std::numeric_limits<uint32_t>::max();
+ std::ostringstream option_space;
+ option_space << "vendor-" << vendor_id;
+
+ // Add 7 options to another option space. The option codes partially overlap
+ // with option codes that we have added to dhcp6 option space.
+ for (uint16_t code = 105; code < 112; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(cfg.add(option, false, option_space.str()));
+ }
+
+ // Get options from the Subnet and check if all 10 are there.
+ OptionContainerPtr options = cfg.getAll(12345678);
+ ASSERT_TRUE(options);
+ ASSERT_EQ(10, options->size());
+
+ // Validate codes of options added to dhcp6 option space.
+ uint16_t expected_code = 100;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ options = cfg.getAll(vendor_id);
+ ASSERT_TRUE(options);
+ ASSERT_EQ(7, options->size());
+
+ // Validate codes of options added to isc option space.
+ expected_code = 105;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ // Try to get options from a non-existing option space.
+ options = cfg.getAll(1111111);
+ ASSERT_TRUE(options);
+ EXPECT_TRUE(options->empty());
+}
+
+// This test verifies that option space names for the vendor options are
+// correct.
+TEST_F(CfgOptionTest, getVendorIdsSpaceNames) {
+ CfgOption cfg;
+
+ // Create 10 options, each goes under a different vendor id.
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ // Generate space name for a unique vendor id.
+ std::ostringstream s;
+ s << "vendor-" << code;
+ ASSERT_NO_THROW(cfg.add(option, false, s.str()));
+ }
+
+ // We should now have 10 different vendor ids.
+ std::list<std::string> space_names = cfg.getVendorIdsSpaceNames();
+ ASSERT_EQ(10, space_names.size());
+
+ // Check that the option space names for those vendor ids are correct.
+ for (std::list<std::string>::iterator name = space_names.begin();
+ name != space_names.end(); ++name) {
+ uint16_t id = static_cast<uint16_t>(std::distance(space_names.begin(),
+ name));
+ std::ostringstream s;
+ s << "vendor-" << (100 + id);
+ EXPECT_EQ(s.str(), *name);
+ }
+}
+
+// This test verifies that the unparse function returns what is expected.
+TEST_F(CfgOptionTest, unparse) {
+ CfgOption cfg;
+
+ // Add some options.
+ OptionPtr opt1(new Option(Option::V6, 100, OptionBuffer(4, 0x12)));
+ cfg.add(opt1, false, "dns");
+ OptionPtr opt2(new Option(Option::V6, 101, OptionBuffer(4, 12)));
+ OptionDescriptor desc2(opt2, false, "12, 12, 12, 12");
+ std::string ctx = "{ \"comment\": \"foo\", \"bar\": 1 }";
+ desc2.setContext(data::Element::fromJSON(ctx));
+ cfg.add(desc2, "dns");
+ OptionPtr opt3(new Option(Option::V6, D6O_STATUS_CODE, OptionBuffer(2, 0)));
+ cfg.add(opt3, false, DHCP6_OPTION_SPACE);
+ OptionPtr opt4(new Option(Option::V6, 100, OptionBuffer(4, 0x21)));
+ cfg.add(opt4, true, "vendor-1234");
+
+ // Unparse
+ std::string expected = "[\n"
+ "{\n"
+ " \"code\": 100,\n"
+ " \"space\": \"dns\",\n"
+ " \"csv-format\": false,\n"
+ " \"data\": \"12121212\",\n"
+ " \"always-send\": false\n"
+ "},{\n"
+ " \"code\": 101,\n"
+ " \"space\": \"dns\",\n"
+ " \"csv-format\": true,\n"
+ " \"data\": \"12, 12, 12, 12\",\n"
+ " \"always-send\": false,\n"
+ " \"user-context\": { \"comment\": \"foo\", \"bar\": 1 }\n"
+ "},{\n"
+ " \"code\": 13,\n"
+ " \"name\": \"status-code\",\n"
+ " \"space\": \"dhcp6\",\n"
+ " \"csv-format\": false,\n"
+ " \"data\": \"0000\",\n"
+ " \"always-send\": false\n"
+ "},{\n"
+ " \"code\": 100,\n"
+ " \"space\": \"vendor-1234\",\n"
+ " \"csv-format\": false,\n"
+ " \"data\": \"21212121\",\n"
+ " \"always-send\": true\n"
+ "}]\n";
+ isc::test::runToElementTest<CfgOption>(expected, cfg);
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/cfg_rsoo_unittest.cc b/src/lib/dhcpsrv/tests/cfg_rsoo_unittest.cc
new file mode 100644
index 0000000..de2d5bb
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_rsoo_unittest.cc
@@ -0,0 +1,100 @@
+// Copyright (C) 2015,2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcp/dhcp6.h>
+#include <dhcpsrv/cfg_rsoo.h>
+#include <testutils/test_to_element.h>
+
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+
+namespace {
+
+// This test verifies that the RSOO configuration holds the default
+// RSOO-enabled options.
+TEST(CfgRSOOTest, defaults) {
+ CfgRSOO rsoo;
+ EXPECT_TRUE(rsoo.enabled(D6O_ERP_LOCAL_DOMAIN_NAME));
+ for (uint16_t code = 0; code < 200; ++code) {
+ if (code != D6O_ERP_LOCAL_DOMAIN_NAME) {
+ EXPECT_FALSE(rsoo.enabled(code))
+ << "expected that the option with code "
+ << code << " is by default RSOO-disabled, but"
+ " it is enabled";
+ }
+ }
+
+ // Now, let's see if we can remove the default options.
+ ASSERT_NO_THROW(rsoo.clear());
+ EXPECT_FALSE(rsoo.enabled(D6O_ERP_LOCAL_DOMAIN_NAME));
+
+ // Make sure it can be added again.
+ ASSERT_NO_THROW(rsoo.enable(D6O_ERP_LOCAL_DOMAIN_NAME));
+ EXPECT_TRUE(rsoo.enabled(D6O_ERP_LOCAL_DOMAIN_NAME));
+}
+
+// This test verifies that it is possible to enable more RSOO options
+// and later remove all of them.
+TEST(CfgRSOOTest, enableAndClear) {
+ CfgRSOO rsoo;
+ EXPECT_TRUE(rsoo.enabled(D6O_ERP_LOCAL_DOMAIN_NAME));
+
+ // Enable option 88.
+ ASSERT_FALSE(rsoo.enabled(88));
+ ASSERT_NO_THROW(rsoo.enable(88));
+ EXPECT_TRUE(rsoo.enabled(88));
+
+ // Enable option 89.
+ ASSERT_FALSE(rsoo.enabled(89));
+ ASSERT_NO_THROW(rsoo.enable(89));
+ EXPECT_TRUE(rsoo.enabled(89));
+
+ // Remove them and make sure they have been removed.
+ ASSERT_NO_THROW(rsoo.clear());
+ for (uint16_t code = 0; code < 200; ++code) {
+ EXPECT_FALSE(rsoo.enabled(code))
+ << "expected that the option with code "
+ << code << " is RSOO-disabled after clearing"
+ " the RSOO configuration, but it is not";
+ }
+}
+
+// This test verifies that the same option may be specified
+// multiple times and that the code doesn't fail.
+TEST(CfgRSOOTest, enableTwice) {
+ CfgRSOO rsoo;
+ // By default there should be the default option enabled.
+ // Let's try to enable it again. It should pass.
+ ASSERT_NO_THROW(rsoo.enable(D6O_ERP_LOCAL_DOMAIN_NAME));
+ EXPECT_TRUE(rsoo.enabled(D6O_ERP_LOCAL_DOMAIN_NAME));
+
+ // Enable option 88.
+ ASSERT_FALSE(rsoo.enabled(88));
+ ASSERT_NO_THROW(rsoo.enable(88));
+ EXPECT_TRUE(rsoo.enabled(88));
+
+ // And enable it again.
+ ASSERT_NO_THROW(rsoo.enabled(88));
+ EXPECT_TRUE(rsoo.enabled(88));
+
+ // Remove all.
+ ASSERT_NO_THROW(rsoo.clear());
+ ASSERT_FALSE(rsoo.enabled(D6O_ERP_LOCAL_DOMAIN_NAME));
+ ASSERT_FALSE(rsoo.enabled(88));
+}
+
+// This test verifies that the unparse function returns what is expected.
+TEST(CfgRSOOTest, unparse) {
+ CfgRSOO rsoo;
+ // option codes are put in strings
+ isc::test::runToElementTest<CfgRSOO>("[ \"65\" ]", rsoo);
+ // isc::test::runToElementTest<CfgRSOO>("[ 65 ]", rsoo);
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/cfg_shared_networks4_unittest.cc b/src/lib/dhcpsrv/tests/cfg_shared_networks4_unittest.cc
new file mode 100644
index 0000000..d81be1e
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_shared_networks4_unittest.cc
@@ -0,0 +1,381 @@
+// Copyright (C) 2017-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <exceptions/exceptions.h>
+#include <dhcp/option_string.h>
+#include <dhcpsrv/cfg_shared_networks.h>
+#include <testutils/test_to_element.h>
+#include <asiolink/io_address.h>
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::util;
+using namespace asiolink;
+
+namespace {
+
+/// @brief Attempts to verify an expected network within a collection
+/// of networks
+///
+/// @param networks set of networks in which to look
+/// @param name name of the expected network
+/// @param exp_valid expected valid lifetime of the network
+/// @param exp_subnets list of subnet IDs the network is expected to own
+void checkMergedNetwork(const CfgSharedNetworks4& networks, const std::string& name,
+ const Triplet<uint32_t>& exp_valid,
+ const std::vector<SubnetID>& exp_subnets) {
+ auto network = networks.getByName(name);
+ ASSERT_TRUE(network) << "expected network: " << name << " not found";
+ ASSERT_EQ(exp_valid, network->getValid()) << " network valid lifetime wrong";
+ const Subnet4SimpleCollection* subnets = network->getAllSubnets();
+ ASSERT_EQ(exp_subnets.size(), subnets->size()) << " wrong number of subnets";
+ for (auto exp_id : exp_subnets) {
+ ASSERT_TRUE(network->getSubnet(exp_id))
+ << " did not find expected subnet: " << exp_id;
+ }
+}
+
+// This test verifies that shared networks can be added to the configruation
+// and retrieved by name.
+TEST(CfgSharedNetworks4Test, getByName) {
+ SharedNetwork4Ptr network1(new SharedNetwork4("frog"));
+ SharedNetwork4Ptr network2(new SharedNetwork4("dog"));
+
+ CfgSharedNetworks4 cfg;
+ ASSERT_NO_THROW(cfg.add(network1));
+ ASSERT_NO_THROW(cfg.add(network2));
+
+ SharedNetwork4Ptr returned_network1 = cfg.getByName("frog");
+ ASSERT_TRUE(returned_network1);
+ SharedNetwork4Ptr returned_network2 = cfg.getByName("dog");
+ ASSERT_TRUE(returned_network2);
+
+ // Check that non-existent name does not return bogus data.
+ EXPECT_FALSE(cfg.getByName("ant"));
+}
+
+// This test verifies that it is possible to delete a network.
+TEST(CfgSharedNetworks4Test, deleteByName) {
+ SharedNetwork4Ptr network1(new SharedNetwork4("frog"));
+ SharedNetwork4Ptr network2(new SharedNetwork4("dog"));
+
+ // Add two networks to the configuration.
+ CfgSharedNetworks4 cfg;
+ ASSERT_NO_THROW(cfg.add(network1));
+ ASSERT_NO_THROW(cfg.add(network2));
+
+ // Try to delete non-existing network. This should throw.
+ ASSERT_THROW(cfg.del("lion"), BadValue);
+
+ // Delete network #1.
+ ASSERT_NO_THROW(cfg.del(network1->getName()));
+ ASSERT_FALSE(cfg.getByName(network1->getName()));
+ ASSERT_TRUE(cfg.getByName(network2->getName()));
+
+ // Delete network #2.
+ ASSERT_NO_THROW(cfg.del(network2->getName()));
+ ASSERT_FALSE(cfg.getByName(network1->getName()));
+ ASSERT_FALSE(cfg.getByName(network2->getName()));
+
+ // Check that attempting to delete the same subnet twice will fail.
+ ASSERT_THROW(cfg.del(network1->getName()), BadValue);
+ ASSERT_THROW(cfg.del(network2->getName()), BadValue);
+}
+
+// Checks that subnets have their shared network pointers updated when
+// the network is deleted. This is used when the shared network is deleted
+// by admin commands.
+TEST(CfgSharedNetworks4Test, deleteNetworkWithSubnets) {
+ CfgSharedNetworks4 cfg;
+ SharedNetwork4Ptr network(new SharedNetwork4("frog"));
+ SubnetID id1(100);
+ SubnetID id2(101);
+ Subnet4Ptr sub1(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3, id1));
+ Subnet4Ptr sub2(new Subnet4(IOAddress("192.0.3.0"), 24, 1, 2, 3, id2));
+ network->add(sub1);
+ network->add(sub2);
+ cfg.add(network);
+
+ // Make sure the subnets are part of the network.
+ SharedNetwork4Ptr test;
+ sub1->getSharedNetwork(test);
+ EXPECT_TRUE(test);
+ EXPECT_EQ(network->toElement()->str(), test->toElement()->str());
+ sub2->getSharedNetwork(test);
+ EXPECT_TRUE(test);
+ EXPECT_EQ(network->toElement()->str(), test->toElement()->str());
+
+ // Now remove the network. Subnets should be disassociated with the network.
+ cfg.del("frog");
+ sub1->getSharedNetwork(test);
+ EXPECT_FALSE(test);
+ sub2->getSharedNetwork(test);
+ EXPECT_FALSE(test);
+}
+
+// This test verifies that it is possible to delete a shared network by
+// its database identifier.
+TEST(CfgSharedNetworks4Test, deleteNetworksById) {
+ // Create three shared networks.
+ CfgSharedNetworks4 cfg;
+ SharedNetwork4Ptr network1(new SharedNetwork4("frog"));
+ SharedNetwork4Ptr network2(new SharedNetwork4("whale"));
+ SharedNetwork4Ptr network3(new SharedNetwork4("fly"));
+
+ // Add one subnet to each shared network.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3, 1));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.3.0"), 24, 1, 2, 3, 2));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.4.0"), 24, 1, 2, 3, 3));
+
+ network1->add(subnet1);
+ network2->add(subnet2);
+ network3->add(subnet3);
+
+ // Set unique identifier for the second shared network.
+ network2->setId(123);
+
+ // Verify that we have two networks with a default identifier and one
+ // with a unique identifier.
+ EXPECT_EQ(0, network1->getId());
+ EXPECT_EQ(123, network2->getId());
+ EXPECT_EQ(0, network3->getId());
+
+ // Add our networks to the configuration.
+ cfg.add(network1);
+ cfg.add(network2);
+ cfg.add(network3);
+
+ // Delete second network by id.
+ uint64_t deleted_num = 0;
+ ASSERT_NO_THROW(deleted_num = cfg.del(network2->getId()));
+ EXPECT_EQ(1, deleted_num);
+
+ // Make sure that the subnet no longer points to the deleted network.
+ SharedNetwork4Ptr returned_network;
+ subnet2->getSharedNetwork(returned_network);
+ EXPECT_FALSE(returned_network);
+ EXPECT_FALSE(cfg.getByName("whale"));
+
+ // Delete the remaining two shared network using id of 0.
+ ASSERT_NO_THROW(deleted_num = cfg.del(network1->getId()));
+ EXPECT_EQ(2, deleted_num);
+
+ // The subnets should no longer point to the deleted networks and
+ // the shared networks should no longer exist in the configuration.
+ subnet1->getSharedNetwork(returned_network);
+ EXPECT_FALSE(returned_network);
+ EXPECT_FALSE(cfg.getByName("frog"));
+
+ subnet3->getSharedNetwork(returned_network);
+ EXPECT_FALSE(returned_network);
+ EXPECT_FALSE(cfg.getByName("fly"));
+
+ EXPECT_EQ(0, cfg.del(network1->getId()));
+}
+
+// This test verifies that shared networks must have unique names.
+TEST(CfgSharedNetworks4Test, duplicateName) {
+ SharedNetwork4Ptr network1(new SharedNetwork4("frog"));
+ SharedNetwork4Ptr network2(new SharedNetwork4("frog"));
+
+ CfgSharedNetworks4 cfg;
+ ASSERT_NO_THROW(cfg.add(network1));
+ ASSERT_THROW(cfg.add(network2), BadValue);
+}
+
+// This test verifies that unparsing shared networks returns valid structure.
+TEST(CfgSharedNetworks4Test, unparse) {
+ SharedNetwork4Ptr network1(new SharedNetwork4("frog"));
+ SharedNetwork4Ptr network2(new SharedNetwork4("dog"));
+ SharedNetwork4Ptr network3(new SharedNetwork4("cat"));
+
+ network1->setIface("eth0");
+ network1->addRelayAddress(IOAddress("198.16.1.1"));
+ network1->addRelayAddress(IOAddress("198.16.1.2"));
+ network1->setCalculateTeeTimes(true);
+ network1->setT1Percent(.35);
+ network1->setT2Percent(.655);
+ network1->setDdnsSendUpdates(true);
+ network1->setDdnsOverrideNoUpdate(true);
+ network1->setDdnsOverrideClientUpdate(true);
+ network1->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_ALWAYS);
+ network1->setDdnsGeneratedPrefix("prefix");
+ network1->setDdnsQualifyingSuffix("example.com.");
+ network1->setHostnameCharSet("[^A-Z]");
+ network1->setHostnameCharReplacement("x");
+ network1->setCacheThreshold(.20);
+
+ network2->setIface("eth1");
+ network2->setT1(Triplet<uint32_t>(100));
+ network2->setT2(Triplet<uint32_t>(200));
+ network2->setValid(Triplet<uint32_t>(200, 300, 400));
+ network2->setDdnsSendUpdates(false);
+ network2->setStoreExtendedInfo(true);
+ network2->setCacheMaxAge(50);
+
+ network3->setIface("eth2");
+ network3->setValid(Triplet<uint32_t>(100));
+
+ CfgSharedNetworks4 cfg;
+ ASSERT_NO_THROW(cfg.add(network1));
+ ASSERT_NO_THROW(cfg.add(network2));
+ ASSERT_NO_THROW(cfg.add(network3));
+
+ std::string expected =
+ "[\n"
+ " {\n"
+ " \"interface\": \"eth2\",\n"
+ " \"name\": \"cat\",\n"
+ " \"option-data\": [ ],\n"
+ " \"relay\": { \"ip-addresses\": [ ] },\n"
+ " \"subnet4\": [ ],\n"
+ " \"valid-lifetime\": 100,\n"
+ " \"min-valid-lifetime\": 100,\n"
+ " \"max-valid-lifetime\": 100\n"
+ " },\n"
+ " {\n"
+ " \"ddns-send-updates\": false,\n"
+ " \"interface\": \"eth1\",\n"
+ " \"name\": \"dog\",\n"
+ " \"rebind-timer\": 200,\n"
+ " \"option-data\": [ ],\n"
+ " \"renew-timer\": 100,\n"
+ " \"relay\": { \"ip-addresses\": [ ] },\n"
+ " \"subnet4\": [ ],\n"
+ " \"valid-lifetime\": 300,\n"
+ " \"min-valid-lifetime\": 200,\n"
+ " \"max-valid-lifetime\": 400,\n"
+ " \"store-extended-info\": true,\n"
+ " \"cache-max-age\": 50\n"
+ " },\n"
+ " {\n"
+ " \"calculate-tee-times\": true,\n"
+ " \"ddns-generated-prefix\": \"prefix\",\n"
+ " \"ddns-override-no-update\": true,\n"
+ " \"ddns-override-client-update\": true,\n"
+ " \"ddns-qualifying-suffix\": \"example.com.\",\n"
+ " \"ddns-replace-client-name\": \"always\",\n"
+ " \"ddns-send-updates\": true,\n"
+ " \"interface\": \"eth0\",\n"
+ " \"name\": \"frog\",\n"
+ " \"option-data\": [ ],\n"
+ " \"relay\": { \"ip-addresses\": [ \"198.16.1.1\", \"198.16.1.2\" ] },\n"
+ " \"subnet4\": [ ],\n"
+ " \"t1-percent\": .35,\n"
+ " \"t2-percent\": .655,\n"
+ " \"hostname-char-replacement\": \"x\",\n"
+ " \"hostname-char-set\": \"[^A-Z]\",\n"
+ " \"cache-threshold\": .20\n"
+ " }\n"
+ "]\n";
+
+ test::runToElementTest<CfgSharedNetworks4>(expected, cfg);
+}
+
+// This test verifies that shared-network configurations are properly merged.
+TEST(CfgSharedNetworks4Test, mergeNetworks) {
+ // Create custom options dictionary for testing merge. We're keeping it
+ // simple because they are more rigorous tests elsewhere.
+ CfgOptionDefPtr cfg_def(new CfgOptionDef());
+ cfg_def->add((OptionDefinitionPtr(new OptionDefinition("one", 1, "isc", "string"))));
+
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.1.0"),
+ 26, 1, 2, 100, SubnetID(1)));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"),
+ 26, 1, 2, 100, SubnetID(2)));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.3.0"),
+ 26, 1, 2, 100, SubnetID(3)));
+ Subnet4Ptr subnet4(new Subnet4(IOAddress("192.0.4.0"),
+ 26, 1, 2, 100, SubnetID(4)));
+
+ // Create network1 and add two subnets to it
+ SharedNetwork4Ptr network1(new SharedNetwork4("network1"));
+ network1->setValid(Triplet<uint32_t>(100));
+ ASSERT_NO_THROW(network1->add(subnet1));
+ ASSERT_NO_THROW(network1->add(subnet2));
+
+ // Create network2 with no subnets.
+ SharedNetwork4Ptr network2(new SharedNetwork4("network2"));
+ network2->setValid(Triplet<uint32_t>(200));
+
+ // Create network3 with one subnet.
+ SharedNetwork4Ptr network3(new SharedNetwork4("network3"));
+ network3->setValid(Triplet<uint32_t>(300));
+ ASSERT_NO_THROW(network3->add(subnet3));
+
+ // Create our "existing" configured networks.
+ // Add all three networks to the existing config.
+ CfgSharedNetworks4 cfg_to;
+ ASSERT_NO_THROW(cfg_to.add(network1));
+ ASSERT_NO_THROW(cfg_to.add(network2));
+ ASSERT_NO_THROW(cfg_to.add(network3));
+
+ // Merge in an "empty" config. Should have the original config, still intact.
+ CfgSharedNetworks4 cfg_from;
+ ASSERT_NO_THROW(cfg_to.merge(cfg_def, cfg_from));
+
+ ASSERT_EQ(3, cfg_to.getAll()->size());
+ ASSERT_NO_FATAL_FAILURE(checkMergedNetwork(cfg_to, "network1", Triplet<uint32_t>(100),
+ std::vector<SubnetID>{SubnetID(1), SubnetID(2)}));
+ ASSERT_NO_FATAL_FAILURE(checkMergedNetwork(cfg_to, "network2", Triplet<uint32_t>(200),
+ std::vector<SubnetID>()));
+
+ ASSERT_NO_FATAL_FAILURE(checkMergedNetwork(cfg_to, "network3", Triplet<uint32_t>(300),
+ std::vector<SubnetID>{SubnetID(3)}));
+
+ // Create network1b, this is an "update" of network1
+ // We'll double the valid time and add subnet4 to it
+ SharedNetwork4Ptr network1b(new SharedNetwork4("network1"));
+ network1b->setValid(Triplet<uint32_t>(200));
+
+ // Now let's a add generic option 1 to network1b.
+ std::string value("Yay!");
+ OptionPtr option(new Option(Option::V4, 1));
+ option->setData(value.begin(), value.end());
+ ASSERT_NO_THROW(network1b->getCfgOption()->add(option, false, "isc"));
+ ASSERT_NO_THROW(network1b->add(subnet4));
+
+ // Network2 we will not touch.
+
+ // Create network3b, this is an "update" of network3.
+ // We'll double it's valid time, but leave off the subnet.
+ SharedNetwork4Ptr network3b(new SharedNetwork4("network3"));
+ network3b->setValid(Triplet<uint32_t>(600));
+
+ // Create our "existing" configured networks.
+ ASSERT_NO_THROW(cfg_from.add(network1b));
+ ASSERT_NO_THROW(cfg_from.add(network3b));
+
+ ASSERT_NO_THROW(cfg_to.merge(cfg_def, cfg_from));
+
+ // Should still have 3 networks.
+
+ // Network1 should have doubled its valid lifetime but still only have
+ // the orignal two subnets. Merge should discard associations on CB
+ // subnets and preserve the associations from existing config.
+ ASSERT_EQ(3, cfg_to.getAll()->size());
+ ASSERT_NO_FATAL_FAILURE(checkMergedNetwork(cfg_to, "network1", Triplet<uint32_t>(200),
+ std::vector<SubnetID>{SubnetID(1), SubnetID(2)}));
+
+ // Make sure we have option 1 and that it has been replaced with a string option.
+ auto network = cfg_to.getByName("network1");
+ auto desc = network->getCfgOption()->get("isc", 1);
+ OptionStringPtr opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("Yay!", opstr->getValue());
+
+ // No changes to network2.
+ ASSERT_NO_FATAL_FAILURE(checkMergedNetwork(cfg_to, "network2", Triplet<uint32_t>(200),
+ std::vector<SubnetID>()));
+
+ // Network1 should have doubled its valid lifetime and still subnet3.
+ ASSERT_NO_FATAL_FAILURE(checkMergedNetwork(cfg_to, "network3", Triplet<uint32_t>(600),
+ std::vector<SubnetID>{SubnetID(3)}));
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/cfg_shared_networks6_unittest.cc b/src/lib/dhcpsrv/tests/cfg_shared_networks6_unittest.cc
new file mode 100644
index 0000000..a9a45d9
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_shared_networks6_unittest.cc
@@ -0,0 +1,391 @@
+// Copyright (C) 2017-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <exceptions/exceptions.h>
+#include <dhcp/option_string.h>
+#include <dhcpsrv/cfg_shared_networks.h>
+#include <asiolink/io_address.h>
+#include <testutils/test_to_element.h>
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::util;
+using namespace asiolink;
+
+namespace {
+
+/// @brief Attempts to verify an expected network within a collection
+/// of networks
+///
+/// @param networks set of networks in which to look
+/// @param name name of the expected network
+/// @param exp_valid expected valid lifetime of the network
+/// @param exp_subnets list of subnet IDs the network is expected to own
+void checkMergedNetwork(const CfgSharedNetworks6& networks, const std::string& name,
+ const Triplet<uint32_t>& exp_valid,
+ const std::vector<SubnetID>& exp_subnets) {
+ auto network = networks.getByName(name);
+ ASSERT_TRUE(network) << "expected network: " << name << " not found";
+ ASSERT_EQ(exp_valid, network->getValid()) << " network valid lifetime wrong";
+ const Subnet6SimpleCollection* subnets = network->getAllSubnets();
+ ASSERT_EQ(exp_subnets.size(), subnets->size()) << " wrong number of subnets";
+ for (auto exp_id : exp_subnets) {
+ ASSERT_TRUE(network->getSubnet(exp_id))
+ << " did not find expected subnet: " << exp_id;
+ }
+}
+
+// This test verifies that shared networks can be added to the configruation
+// and retrieved by name.
+TEST(CfgSharedNetworks6Test, getByName) {
+ SharedNetwork6Ptr network1(new SharedNetwork6("frog"));
+ SharedNetwork6Ptr network2(new SharedNetwork6("dog"));
+
+ CfgSharedNetworks6 cfg;
+ ASSERT_NO_THROW(cfg.add(network1));
+ ASSERT_NO_THROW(cfg.add(network2));
+
+ SharedNetwork6Ptr returned_network1 = cfg.getByName("frog");
+ ASSERT_TRUE(returned_network1);
+ SharedNetwork6Ptr returned_network2 = cfg.getByName("dog");
+ ASSERT_TRUE(returned_network2);
+
+ // Check that non-existent name does not return bogus data.
+ EXPECT_FALSE(cfg.getByName("ant"));
+}
+
+// This test verifies that it is possible to delete a network.
+TEST(CfgSharedNetworks6Test, deleteByName) {
+ SharedNetwork6Ptr network1(new SharedNetwork6("frog"));
+ SharedNetwork6Ptr network2(new SharedNetwork6("dog"));
+
+ // Add two networks to the configuration.
+ CfgSharedNetworks6 cfg;
+ ASSERT_NO_THROW(cfg.add(network1));
+ ASSERT_NO_THROW(cfg.add(network2));
+
+ // Try to delete non-existing network. This should throw.
+ ASSERT_THROW(cfg.del("lion"), BadValue);
+
+ // Delete network #1.
+ ASSERT_NO_THROW(cfg.del(network1->getName()));
+ ASSERT_FALSE(cfg.getByName(network1->getName()));
+ ASSERT_TRUE(cfg.getByName(network2->getName()));
+
+ // Delete network #2.
+ ASSERT_NO_THROW(cfg.del(network2->getName()));
+ ASSERT_FALSE(cfg.getByName(network1->getName()));
+ ASSERT_FALSE(cfg.getByName(network2->getName()));
+
+ // Check that attempting to delete the same subnet twice will fail.
+ ASSERT_THROW(cfg.del(network1->getName()), BadValue);
+ ASSERT_THROW(cfg.del(network2->getName()), BadValue);
+}
+
+// Checks that subnets have their shared network pointers updated when
+// the network is deleted. This is used when the shared network is deleted
+// by admin commands.
+TEST(CfgSharedNetworks6Test, deleteNetworkWithSubnets) {
+ CfgSharedNetworks6 cfg;
+ SharedNetwork6Ptr network(new SharedNetwork6("frog"));
+ SubnetID id1(100);
+ SubnetID id2(101);
+ Subnet6Ptr sub1(new Subnet6(IOAddress("2001:db8::"), 48, 1, 2, 3, 4, id1));
+ Subnet6Ptr sub2(new Subnet6(IOAddress("fec0::"), 12, 1, 2, 3, 4, id2));
+ network->add(sub1);
+ network->add(sub2);
+ cfg.add(network);
+
+ // Make sure the subnets are part of the network.
+ SharedNetwork6Ptr test;
+ sub1->getSharedNetwork(test);
+ EXPECT_TRUE(test);
+ EXPECT_EQ(network->toElement()->str(), test->toElement()->str());
+ sub2->getSharedNetwork(test);
+ EXPECT_TRUE(test);
+ EXPECT_EQ(network->toElement()->str(), test->toElement()->str());
+
+ // Now remove the network. Subnets should be disassociated with the network.
+ cfg.del("frog");
+ sub1->getSharedNetwork(test);
+ EXPECT_FALSE(test);
+ sub2->getSharedNetwork(test);
+ EXPECT_FALSE(test);
+}
+
+
+// This test verifies that it is possible to delete a shared network by
+// its database identifier.
+TEST(CfgSharedNetworks6Test, deleteNetworksById) {
+ // Create three shared networks.
+ CfgSharedNetworks6 cfg;
+ SharedNetwork6Ptr network1(new SharedNetwork6("frog"));
+ SharedNetwork6Ptr network2(new SharedNetwork6("whale"));
+ SharedNetwork6Ptr network3(new SharedNetwork6("fly"));
+
+ // Add one subnet to each shared network.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 64, 1, 2, 3, 4, 1));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 64, 1, 2, 3, 4, 2));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 64, 1, 2, 3, 4, 3));
+
+ network1->add(subnet1);
+ network2->add(subnet2);
+ network3->add(subnet3);
+
+ // Set unique identifier for the second shared network.
+ network2->setId(123);
+
+ // Verify that we have two networks with a default identifier and one
+ // with a unique identifier.
+ EXPECT_EQ(0, network1->getId());
+ EXPECT_EQ(123, network2->getId());
+ EXPECT_EQ(0, network3->getId());
+
+ // Add our networks to the configuration.
+ cfg.add(network1);
+ cfg.add(network2);
+ cfg.add(network3);
+
+ // Delete second network by id.
+ uint64_t deleted_num = 0;
+ ASSERT_NO_THROW(deleted_num = cfg.del(network2->getId()));
+ EXPECT_EQ(1, deleted_num);
+
+ // Make sure that the subnet no longer points to the deleted network.
+ SharedNetwork6Ptr returned_network;
+ subnet2->getSharedNetwork(returned_network);
+ EXPECT_FALSE(returned_network);
+ EXPECT_FALSE(cfg.getByName("whale"));
+
+ // Delete the remaining two shared network using id of 0.
+ ASSERT_NO_THROW(deleted_num = cfg.del(network1->getId()));
+ EXPECT_EQ(2, deleted_num);
+
+ // The subnets should no longer point to the deleted networks and
+ // the shared networks should no longer exist in the configuration.
+ subnet1->getSharedNetwork(returned_network);
+ EXPECT_FALSE(returned_network);
+ EXPECT_FALSE(cfg.getByName("frog"));
+
+ subnet3->getSharedNetwork(returned_network);
+ EXPECT_FALSE(returned_network);
+ EXPECT_FALSE(cfg.getByName("fly"));
+
+ EXPECT_EQ(0, cfg.del(network1->getId()));
+}
+
+// This test verifies that shared networks must have unique names.
+TEST(CfgSharedNetworks6Test, duplicateName) {
+ SharedNetwork6Ptr network1(new SharedNetwork6("frog"));
+ SharedNetwork6Ptr network2(new SharedNetwork6("frog"));
+
+ CfgSharedNetworks6 cfg;
+ ASSERT_NO_THROW(cfg.add(network1));
+ ASSERT_THROW(cfg.add(network2), BadValue);
+}
+
+// This test verifies that unparsing shared networks returns valid structure.
+TEST(CfgSharedNetworks6Test, unparse) {
+ SharedNetwork6Ptr network1(new SharedNetwork6("frog"));
+ SharedNetwork6Ptr network2(new SharedNetwork6("dog"));
+ SharedNetwork6Ptr network3(new SharedNetwork6("cat"));
+
+ network1->setIface("eth0");
+ network1->addRelayAddress(IOAddress("2001:db8:1::1"));
+ network1->addRelayAddress(IOAddress("2001:db8:1::2"));
+ network1->setCalculateTeeTimes(true);
+ network1->setT1Percent(.35);
+ network1->setT2Percent(.655);
+ network1->setDdnsSendUpdates(true);
+ network1->setDdnsOverrideNoUpdate(true);
+ network1->setDdnsOverrideClientUpdate(true);
+ network1->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_ALWAYS);
+ network1->setDdnsGeneratedPrefix("prefix");
+ network1->setDdnsQualifyingSuffix("example.com.");
+ network1->setHostnameCharSet("[^A-Z]");
+ network1->setHostnameCharReplacement("x");
+ network1->setCacheThreshold(.20);
+
+ network2->setIface("eth1");
+ network2->setT1(Triplet<uint32_t>(100));
+ network2->setT2(Triplet<uint32_t>(200));
+ network2->setPreferred(Triplet<uint32_t>(200));
+ network2->setValid(Triplet<uint32_t>(300));
+ network2->setDdnsSendUpdates(false);
+ network2->setStoreExtendedInfo(true);
+ network2->setCacheMaxAge(80);
+
+ network3->setIface("eth2");
+ network3->setPreferred(Triplet<uint32_t>(100,200,300));
+ network3->setValid(Triplet<uint32_t>(200,300,400));
+
+ CfgSharedNetworks6 cfg;
+ ASSERT_NO_THROW(cfg.add(network1));
+ ASSERT_NO_THROW(cfg.add(network2));
+ ASSERT_NO_THROW(cfg.add(network3));
+
+ std::string expected =
+ "[\n"
+ " {\n"
+ " \"interface\": \"eth2\",\n"
+ " \"name\": \"cat\",\n"
+ " \"option-data\": [ ],\n"
+ " \"relay\": { \"ip-addresses\": [ ] },\n"
+ " \"subnet6\": [ ],\n"
+ " \"preferred-lifetime\": 200,\n"
+ " \"min-preferred-lifetime\": 100,\n"
+ " \"max-preferred-lifetime\": 300,\n"
+ " \"valid-lifetime\": 300,\n"
+ " \"min-valid-lifetime\": 200,\n"
+ " \"max-valid-lifetime\": 400\n"
+ " },\n"
+ " {\n"
+ " \"ddns-send-updates\": false,\n"
+ " \"interface\": \"eth1\",\n"
+ " \"name\": \"dog\",\n"
+ " \"option-data\": [ ],\n"
+ " \"rebind-timer\": 200,\n"
+ " \"relay\": { \"ip-addresses\": [ ] },\n"
+ " \"renew-timer\": 100,\n"
+ " \"subnet6\": [ ],\n"
+ " \"preferred-lifetime\": 200,\n"
+ " \"min-preferred-lifetime\": 200,\n"
+ " \"max-preferred-lifetime\": 200,\n"
+ " \"valid-lifetime\": 300,\n"
+ " \"min-valid-lifetime\": 300,\n"
+ " \"max-valid-lifetime\": 300,\n"
+ " \"store-extended-info\": true,\n"
+ " \"cache-max-age\": 80\n"
+ " },\n"
+ " {\n"
+ " \"calculate-tee-times\": true,\n"
+ " \"ddns-generated-prefix\": \"prefix\",\n"
+ " \"ddns-override-no-update\": true,\n"
+ " \"ddns-override-client-update\": true,\n"
+ " \"ddns-qualifying-suffix\": \"example.com.\",\n"
+ " \"ddns-replace-client-name\": \"always\",\n"
+ " \"ddns-send-updates\": true,\n"
+ " \"interface\": \"eth0\",\n"
+ " \"name\": \"frog\",\n"
+ " \"option-data\": [ ],\n"
+ " \"relay\": { \"ip-addresses\": [ \"2001:db8:1::1\", \"2001:db8:1::2\" ] },\n"
+ " \"subnet6\": [ ],\n"
+ " \"t1-percent\": .35,\n"
+ " \"t2-percent\": .655,\n"
+ " \"hostname-char-replacement\": \"x\",\n"
+ " \"hostname-char-set\": \"[^A-Z]\",\n"
+ " \"cache-threshold\": .20\n"
+ " }\n"
+ "]\n";
+
+ test::runToElementTest<CfgSharedNetworks6>(expected, cfg);
+}
+
+// This test verifies that shared-network configurations are properly merged.
+TEST(CfgSharedNetworks6Test, mergeNetworks) {
+ // Create custom options dictionary for testing merge. We're keeping it
+ // simple because they are more rigorous tests elsewhere.
+ CfgOptionDefPtr cfg_def(new CfgOptionDef());
+ cfg_def->add((OptionDefinitionPtr(new OptionDefinition("one", 1, "isc", "string"))));
+
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:1::"),
+ 64, 60, 80, 100, 200, SubnetID(1)));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:2::"),
+ 64, 60, 80, 100, 200, SubnetID(2)));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:3::"),
+ 64, 60, 80, 100, 200, SubnetID(3)));
+ Subnet6Ptr subnet4(new Subnet6(IOAddress("2001:4::"),
+ 64, 60, 80, 100, 200, SubnetID(4)));
+
+ // Create network1 and add two subnets to it
+ SharedNetwork6Ptr network1(new SharedNetwork6("network1"));
+ network1->setValid(Triplet<uint32_t>(100));
+ ASSERT_NO_THROW(network1->add(subnet1));
+ ASSERT_NO_THROW(network1->add(subnet2));
+
+ // Create network2 with no subnets.
+ SharedNetwork6Ptr network2(new SharedNetwork6("network2"));
+ network2->setValid(Triplet<uint32_t>(200));
+
+ // Create network3 with one subnet.
+ SharedNetwork6Ptr network3(new SharedNetwork6("network3"));
+ network3->setValid(Triplet<uint32_t>(300));
+ ASSERT_NO_THROW(network3->add(subnet3));
+
+ // Create our "existing" configured networks.
+ // Add all three networks to the existing config.
+ CfgSharedNetworks6 cfg_to;
+ ASSERT_NO_THROW(cfg_to.add(network1));
+ ASSERT_NO_THROW(cfg_to.add(network2));
+ ASSERT_NO_THROW(cfg_to.add(network3));
+
+ // Merge in an "empty" config. Should have the original config, still intact.
+ CfgSharedNetworks6 cfg_from;
+ ASSERT_NO_THROW(cfg_to.merge(cfg_def, cfg_from));
+
+ ASSERT_EQ(3, cfg_to.getAll()->size());
+ ASSERT_NO_FATAL_FAILURE(checkMergedNetwork(cfg_to, "network1", Triplet<uint32_t>(100),
+ std::vector<SubnetID>{SubnetID(1), SubnetID(2)}));
+ ASSERT_NO_FATAL_FAILURE(checkMergedNetwork(cfg_to, "network2", Triplet<uint32_t>(200),
+ std::vector<SubnetID>()));
+
+ ASSERT_NO_FATAL_FAILURE(checkMergedNetwork(cfg_to, "network3", Triplet<uint32_t>(300),
+ std::vector<SubnetID>{SubnetID(3)}));
+
+ // Create network1b, this is an "update" of network1
+ // We'll double the valid time and add subnet4 to it
+ SharedNetwork6Ptr network1b(new SharedNetwork6("network1"));
+ network1b->setValid(Triplet<uint32_t>(200));
+
+ // Now let's a add generic option 1 to network1b.
+ std::string value("Yay!");
+ OptionPtr option(new Option(Option::V6, 1));
+ option->setData(value.begin(), value.end());
+ ASSERT_NO_THROW(network1b->getCfgOption()->add(option, false, "isc"));
+ ASSERT_NO_THROW(network1b->add(subnet4));
+
+ // Network2 we will not touch.
+
+ // Create network3b, this is an "update" of network3.
+ // We'll double it's valid time, but leave off the subnet.
+ SharedNetwork6Ptr network3b(new SharedNetwork6("network3"));
+ network3b->setValid(Triplet<uint32_t>(600));
+
+ // Create our "existing" configured networks.
+ ASSERT_NO_THROW(cfg_from.add(network1b));
+ ASSERT_NO_THROW(cfg_from.add(network3b));
+
+ ASSERT_NO_THROW(cfg_to.merge(cfg_def, cfg_from));
+
+ // Should still have 3 networks.
+
+ // Network1 should have doubled its valid lifetime but still only have
+ // the orignal two subnets. Merge should discard associations on CB
+ // subnets and preserve the associations from existing config.
+ ASSERT_EQ(3, cfg_to.getAll()->size());
+ ASSERT_NO_FATAL_FAILURE(checkMergedNetwork(cfg_to, "network1", Triplet<uint32_t>(200),
+ std::vector<SubnetID>{SubnetID(1), SubnetID(2)}));
+
+ // Make sure we have option 1 and that it has been replaced with a string option.
+ auto network = cfg_to.getByName("network1");
+ auto desc = network->getCfgOption()->get("isc", 1);
+ ASSERT_TRUE(desc.option_);
+ OptionStringPtr opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("Yay!", opstr->getValue());
+
+ // No changes to network2.
+ ASSERT_NO_FATAL_FAILURE(checkMergedNetwork(cfg_to, "network2", Triplet<uint32_t>(200),
+ std::vector<SubnetID>()));
+
+ // Network1 should have doubled its valid lifetime and still subnet3.
+ ASSERT_NO_FATAL_FAILURE(checkMergedNetwork(cfg_to, "network3", Triplet<uint32_t>(600),
+ std::vector<SubnetID>{SubnetID(3)}));
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/cfg_subnets4_unittest.cc b/src/lib/dhcpsrv/tests/cfg_subnets4_unittest.cc
new file mode 100644
index 0000000..1fbd5bf
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_subnets4_unittest.cc
@@ -0,0 +1,2087 @@
+// Copyright (C) 2014-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcp/classify.h>
+#include <dhcp/libdhcp++.h>
+#include <dhcp/option_custom.h>
+#include <dhcp/option_definition.h>
+#include <dhcp/option_space.h>
+#include <dhcp/option_string.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/parsers/dhcp_parsers.h>
+#include <dhcpsrv/cfg_shared_networks.h>
+#include <dhcpsrv/cfg_subnets4.h>
+#include <dhcpsrv/shared_network.h>
+#include <dhcpsrv/subnet.h>
+#include <dhcpsrv/subnet_id.h>
+#include <dhcpsrv/subnet_selector.h>
+#include <dhcpsrv/cfg_hosts.h>
+#include <stats/stats_mgr.h>
+#include <testutils/gtest_utils.h>
+#include <testutils/test_to_element.h>
+#include <util/doubles.h>
+
+#include <gtest/gtest.h>
+#include <vector>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::stats;
+using namespace isc::test;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Verifies that a set of subnets contains a given a subnet
+///
+/// @param cfg_subnets set of subnets in which to look
+/// @param prefix prefix of the target subnet
+/// @param exp_subnet_id expected id of the target subnet
+/// @param exp_valid expected valid lifetime of the subnet
+/// @param exp_network pointer to the subnet's shared-network (if one)
+void checkMergedSubnet(CfgSubnets4& cfg_subnets,
+ const std::string& prefix,
+ const SubnetID exp_subnet_id,
+ int exp_valid,
+ SharedNetwork4Ptr exp_network) {
+ // Look for the network by prefix.
+ auto subnet = cfg_subnets.getByPrefix(prefix);
+ ASSERT_TRUE(subnet) << "subnet: " << prefix << " not found";
+
+ // Make sure we have the one we expect.
+ ASSERT_EQ(exp_subnet_id, subnet->getID()) << "subnet ID is wrong";
+ ASSERT_EQ(exp_valid, subnet->getValid()) << "subnet valid time is wrong";
+
+ SharedNetwork4Ptr shared_network;
+ subnet->getSharedNetwork(shared_network);
+ if (exp_network) {
+ ASSERT_TRUE(shared_network)
+ << " expected network: " << exp_network->getName() << " not found";
+ ASSERT_TRUE(shared_network == exp_network) << " networks do no match";
+ } else {
+ ASSERT_FALSE(shared_network) << " unexpected network assignment: "
+ << shared_network->getName();
+ }
+}
+
+// This test verifies that specific subnet can be retrieved by specifying
+// subnet identifier or subnet prefix.
+TEST(CfgSubnets4Test, getSpecificSubnet) {
+ CfgSubnets4 cfg;
+
+ // Create 3 subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"),
+ 26, 1, 2, 3, SubnetID(5)));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"),
+ 26, 1, 2, 3, SubnetID(8)));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"),
+ 26, 1, 2, 3, SubnetID(10)));
+
+ // Store the subnets in a vector to make it possible to loop over
+ // all configured subnets.
+ std::vector<Subnet4Ptr> subnets;
+ subnets.push_back(subnet1);
+ subnets.push_back(subnet2);
+ subnets.push_back(subnet3);
+
+ // Add all subnets to the configuration.
+ for (auto subnet = subnets.cbegin(); subnet != subnets.cend(); ++subnet) {
+ ASSERT_NO_THROW(cfg.add(*subnet)) << "failed to add subnet with id: "
+ << (*subnet)->getID();
+ }
+
+ // Iterate over all subnets and make sure they can be retrieved by
+ // subnet identifier.
+ for (auto subnet = subnets.rbegin(); subnet != subnets.rend(); ++subnet) {
+ ConstSubnet4Ptr subnet_returned = cfg.getBySubnetId((*subnet)->getID());
+ ASSERT_TRUE(subnet_returned) << "failed to return subnet with id: "
+ << (*subnet)->getID();
+ EXPECT_EQ((*subnet)->getID(), subnet_returned->getID());
+ EXPECT_EQ((*subnet)->toText(), subnet_returned->toText());
+ }
+
+ // Repeat the previous test, but this time retrieve subnets by their
+ // prefixes.
+ for (auto subnet = subnets.rbegin(); subnet != subnets.rend(); ++subnet) {
+ ConstSubnet4Ptr subnet_returned = cfg.getByPrefix((*subnet)->toText());
+ ASSERT_TRUE(subnet_returned) << "failed to return subnet with id: "
+ << (*subnet)->getID();
+ EXPECT_EQ((*subnet)->getID(), subnet_returned->getID());
+ EXPECT_EQ((*subnet)->toText(), subnet_returned->toText());
+ }
+
+ // Make sure that null pointers are returned for non-existing subnets.
+ EXPECT_FALSE(cfg.getBySubnetId(SubnetID(123)));
+ EXPECT_FALSE(cfg.getByPrefix("10.20.30.0/29"));
+}
+
+// This test verifies that a single subnet can be removed from the configuration.
+TEST(CfgSubnets4Test, deleteSubnet) {
+ CfgSubnets4 cfg;
+
+ // Create 3 subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"),
+ 26, 1, 2, 3, SubnetID(5)));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.3.0"),
+ 26, 1, 2, 3, SubnetID(8)));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.4.0"),
+ 26, 1, 2, 3, SubnetID(10)));
+
+ ASSERT_NO_THROW(cfg.add(subnet1));
+ ASSERT_NO_THROW(cfg.add(subnet2));
+ ASSERT_NO_THROW(cfg.add(subnet3));
+
+ // There should be three subnets.
+ ASSERT_EQ(3, cfg.getAll()->size());
+ // We're going to remove the subnet #2. Let's make sure it exists before
+ // we remove it.
+ ASSERT_TRUE(cfg.getByPrefix("192.0.3.0/26"));
+
+ // Remove the subnet and make sure it is gone.
+ ASSERT_NO_THROW(cfg.del(subnet2));
+ ASSERT_EQ(2, cfg.getAll()->size());
+ EXPECT_FALSE(cfg.getByPrefix("192.0.3.0/26"));
+
+ // Remove another subnet by ID.
+ ASSERT_NO_THROW(cfg.del(subnet1->getID()));
+ ASSERT_EQ(1, cfg.getAll()->size());
+ EXPECT_FALSE(cfg.getByPrefix("192.0.2.0/26"));
+}
+
+// This test verifies that replace a subnet works as expected.
+TEST(CfgSubnets4Test, replaceSubnet) {
+ CfgSubnets4 cfg;
+
+ // Create 3 subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.1.0"),
+ 26, 1, 2, 100, SubnetID(10)));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"),
+ 26, 1, 2, 100, SubnetID(2)));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.3.0"),
+ 26, 1, 2, 100, SubnetID(13)));
+
+ ASSERT_NO_THROW(cfg.add(subnet1));
+ ASSERT_NO_THROW(cfg.add(subnet2));
+ ASSERT_NO_THROW(cfg.add(subnet3));
+
+ // There should be three subnets.
+ ASSERT_EQ(3, cfg.getAll()->size());
+ // We're going to replace the subnet #2. Let's make sure it exists before
+ // we replace it.
+ ASSERT_TRUE(cfg.getByPrefix("192.0.3.0/26"));
+
+ // Replace the subnet and make sure it was updated.
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"),
+ 26, 10, 20, 1000, SubnetID(2)));
+ Subnet4Ptr replaced = cfg.replace(subnet);
+ ASSERT_TRUE(replaced);
+ EXPECT_TRUE(replaced == subnet2);
+ ASSERT_EQ(3, cfg.getAll()->size());
+ Subnet4Ptr returned = cfg.getSubnet(SubnetID(2));
+ ASSERT_TRUE(returned);
+ EXPECT_TRUE(returned == subnet);
+
+ // Restore.
+ replaced = cfg.replace(replaced);
+ ASSERT_TRUE(replaced);
+ EXPECT_TRUE(replaced == subnet);
+ ASSERT_EQ(3, cfg.getAll()->size());
+ returned = cfg.getSubnet(SubnetID(2));
+ ASSERT_TRUE(returned);
+ EXPECT_TRUE(returned == subnet2);
+
+ // Prefix conflict returns null.
+ subnet.reset(new Subnet4(IOAddress("192.0.3.0"),
+ 26, 10, 20, 1000, SubnetID(2)));
+ replaced = cfg.replace(subnet);
+ EXPECT_FALSE(replaced);
+ returned = cfg.getSubnet(SubnetID(2));
+ ASSERT_TRUE(returned);
+ EXPECT_TRUE(returned == subnet2);
+
+ // Changing prefix works even it is highly not recommended.
+ subnet.reset(new Subnet4(IOAddress("192.0.10.0"),
+ 26, 10, 20, 1000, SubnetID(2)));
+ replaced = cfg.replace(subnet);
+ ASSERT_TRUE(replaced);
+ EXPECT_TRUE(replaced == subnet2);
+ returned = cfg.getSubnet(SubnetID(2));
+ ASSERT_TRUE(returned);
+ EXPECT_TRUE(returned == subnet);
+}
+
+// This test verifies that subnets configuration is properly merged.
+TEST(CfgSubnets4Test, mergeSubnets) {
+ // Create custom options dictionary for testing merge. We're keeping it
+ // simple because they are more rigorous tests elsewhere.
+ CfgOptionDefPtr cfg_def(new CfgOptionDef());
+ cfg_def->add((OptionDefinitionPtr(new OptionDefinition("one", 1, "isc", "string"))));
+
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.1.0"),
+ 26, 1, 2, 100, SubnetID(1)));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"),
+ 26, 1, 2, 100, SubnetID(2)));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.3.0"),
+ 26, 1, 2, 100, SubnetID(3)));
+ Subnet4Ptr subnet4(new Subnet4(IOAddress("192.0.4.0"),
+ 26, 1, 2, 100, SubnetID(4)));
+
+ // Create the "existing" list of shared networks
+ CfgSharedNetworks4Ptr networks(new CfgSharedNetworks4());
+ SharedNetwork4Ptr shared_network1(new SharedNetwork4("shared-network1"));
+ networks->add(shared_network1);
+ SharedNetwork4Ptr shared_network2(new SharedNetwork4("shared-network2"));
+ networks->add(shared_network2);
+
+ // Empty network pointer.
+ SharedNetwork4Ptr no_network;
+
+ // Add Subnets 1, 2 and 4 to shared networks.
+ ASSERT_NO_THROW(shared_network1->add(subnet1));
+ ASSERT_NO_THROW(shared_network2->add(subnet2));
+ ASSERT_NO_THROW(shared_network2->add(subnet4));
+
+ // Create our "existing" configured subnets.
+ CfgSubnets4 cfg_to;
+ ASSERT_NO_THROW(cfg_to.add(subnet1));
+ ASSERT_NO_THROW(cfg_to.add(subnet2));
+ ASSERT_NO_THROW(cfg_to.add(subnet3));
+ ASSERT_NO_THROW(cfg_to.add(subnet4));
+
+ // Merge in an "empty" config. Should have the original config,
+ // still intact.
+ CfgSubnets4 cfg_from;
+ ASSERT_NO_THROW(cfg_to.merge(cfg_def, networks, cfg_from));
+
+ // We should have all four subnets, with no changes.
+ ASSERT_EQ(4, cfg_to.getAll()->size());
+
+ // Should be no changes to the configuration.
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.1.0/26",
+ SubnetID(1), 100, shared_network1));
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.2.0/26",
+ SubnetID(2), 100, shared_network2));
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.3.0/26",
+ SubnetID(3), 100, no_network));
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.4.0/26",
+ SubnetID(4), 100, shared_network2));
+
+ // Fill cfg_from configuration with subnets.
+ // subnet 1b updates subnet 1 but leaves it in network 1 with the same ID.
+ Subnet4Ptr subnet1b(new Subnet4(IOAddress("192.0.10.0"),
+ 26, 2, 3, 400, SubnetID(1)));
+ subnet1b->setSharedNetworkName("shared-network1");
+
+ // Add generic option 1 to subnet 1b.
+ std::string value("Yay!");
+ OptionPtr option(new Option(Option::V4, 1));
+ option->setData(value.begin(), value.end());
+ ASSERT_NO_THROW(subnet1b->getCfgOption()->add(option, false, "isc"));
+
+ // subnet 3b updates subnet 3 with different ID and removes it
+ // from network 2
+ Subnet4Ptr subnet3b(new Subnet4(IOAddress("192.0.3.0"),
+ 26, 3, 4, 500, SubnetID(30)));
+
+ // Now Add generic option 1 to subnet 3b.
+ value = "Team!";
+ option.reset(new Option(Option::V4, 1));
+ option->setData(value.begin(), value.end());
+ ASSERT_NO_THROW(subnet3b->getCfgOption()->add(option, false, "isc"));
+
+ // subnet 4b updates subnet 4 and moves it from network2 to network 1
+ Subnet4Ptr subnet4b(new Subnet4(IOAddress("192.0.4.0"),
+ 26, 3, 4, 500, SubnetID(4)));
+ subnet4b->setSharedNetworkName("shared-network1");
+
+ // subnet 5 is new and belongs to network 2
+ // Has two pools both with an option 1
+ Subnet4Ptr subnet5(new Subnet4(IOAddress("192.0.5.0"),
+ 26, 1, 2, 300, SubnetID(5)));
+ subnet5->setSharedNetworkName("shared-network2");
+
+ // Add pool 1
+ Pool4Ptr pool(new Pool4(IOAddress("192.0.5.10"), IOAddress("192.0.5.20")));
+ value = "POOLS";
+ option.reset(new Option(Option::V4, 1));
+ option->setData(value.begin(), value.end());
+ ASSERT_NO_THROW(pool->getCfgOption()->add(option, false, "isc"));
+ subnet5->addPool(pool);
+
+ // Add pool 2
+ pool.reset(new Pool4(IOAddress("192.0.5.30"), IOAddress("192.0.5.40")));
+ value ="RULE!";
+ option.reset(new Option(Option::V4, 1));
+ option->setData(value.begin(), value.end());
+ ASSERT_NO_THROW(pool->getCfgOption()->add(option, false, "isc"));
+ subnet5->addPool(pool);
+
+ // Add subnets to the merge from config.
+ ASSERT_NO_THROW(cfg_from.add(subnet1b));
+ ASSERT_NO_THROW(cfg_from.add(subnet3b));
+ ASSERT_NO_THROW(cfg_from.add(subnet4b));
+ ASSERT_NO_THROW(cfg_from.add(subnet5));
+
+ // Merge again.
+ ASSERT_NO_THROW(cfg_to.merge(cfg_def, networks, cfg_from));
+ ASSERT_EQ(5, cfg_to.getAll()->size());
+
+ // The subnet1 should be replaced by subnet1b.
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.10.0/26",
+ SubnetID(1), 400, shared_network1));
+
+ // Let's verify that our option is there and populated correctly.
+ auto subnet = cfg_to.getByPrefix("192.0.10.0/26");
+ auto desc = subnet->getCfgOption()->get("isc", 1);
+ ASSERT_TRUE(desc.option_);
+ OptionStringPtr opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("Yay!", opstr->getValue());
+
+ // The subnet2 should not be affected because it was not present.
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.2.0/26",
+ SubnetID(2), 100, shared_network2));
+
+ // subnet3 should be replaced by subnet3b and no longer assigned to a network.
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.3.0/26",
+ SubnetID(30), 500, no_network));
+ // Let's verify that our option is there and populated correctly.
+ subnet = cfg_to.getByPrefix("192.0.3.0/26");
+ desc = subnet->getCfgOption()->get("isc", 1);
+ ASSERT_TRUE(desc.option_);
+ opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("Team!", opstr->getValue());
+
+ // subnet4 should be replaced by subnet4b and moved to network1.
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.4.0/26",
+ SubnetID(4), 500, shared_network1));
+
+ // subnet5 should have been added to configuration.
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "192.0.5.0/26",
+ SubnetID(5), 300, shared_network2));
+
+ // Let's verify that both pools have the proper options.
+ subnet = cfg_to.getByPrefix("192.0.5.0/26");
+ const PoolPtr merged_pool = subnet->getPool(Lease::TYPE_V4, IOAddress("192.0.5.10"));
+ ASSERT_TRUE(merged_pool);
+ desc = merged_pool->getCfgOption()->get("isc", 1);
+ opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("POOLS", opstr->getValue());
+
+ const PoolPtr merged_pool2 = subnet->getPool(Lease::TYPE_V4, IOAddress("192.0.5.30"));
+ ASSERT_TRUE(merged_pool2);
+ desc = merged_pool2->getCfgOption()->get("isc", 1);
+ opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("RULE!", opstr->getValue());
+}
+
+// This test verifies that it is possible to retrieve a subnet using an
+// IP address.
+TEST(CfgSubnets4Test, selectSubnetByCiaddr) {
+ CfgSubnets4 cfg;
+
+ // Create 3 subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3));
+
+ // Make sure that initially the subnets don't exist.
+ SubnetSelector selector;
+ selector.ciaddr_ = IOAddress("192.0.2.0");
+ // Set some unicast local address to simulate a Renew.
+ selector.local_address_ = IOAddress("10.0.0.100");
+ ASSERT_FALSE(cfg.selectSubnet(selector));
+
+ // Add one subnet and make sure it is returned.
+ cfg.add(subnet1);
+ selector.ciaddr_ = IOAddress("192.0.2.63");
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+
+ // Add all other subnets.
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ // Make sure they are returned for the appropriate addresses.
+ selector.ciaddr_ = IOAddress("192.0.2.15");
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+ selector.ciaddr_ = IOAddress("192.0.2.85");
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.ciaddr_ = IOAddress("192.0.2.191");
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+
+ // Also, make sure that the NULL pointer is returned if the subnet
+ // cannot be found.
+ selector.ciaddr_ = IOAddress("192.0.2.192");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+}
+
+// This test verifies that it is possible to select a subnet by
+// matching an interface name.
+TEST(CfgSubnets4Test, selectSubnetByIface) {
+ // The IfaceMgrTestConfig object initializes fake interfaces:
+ // eth0, eth1 and lo on the configuration manager. The CfgSubnets4
+ // object uses interface names to select the appropriate subnet.
+ IfaceMgrTestConfig config(true);
+
+ CfgSubnets4 cfg;
+
+ // Create 3 subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3));
+ // No interface defined for subnet1
+ subnet2->setIface("lo");
+ subnet3->setIface("eth1");
+
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ // Make sure that initially the subnets don't exist.
+ SubnetSelector selector;
+ // Set an interface to a name that is not defined in the config.
+ // Subnet selection should fail.
+ selector.iface_name_ = "eth0";
+ ASSERT_FALSE(cfg.selectSubnet(selector));
+
+ // Now select an interface name that matches. Selection should succeed
+ // and return subnet3.
+ selector.iface_name_ = "eth1";
+ Subnet4Ptr selected = cfg.selectSubnet(selector);
+ ASSERT_TRUE(selected);
+ EXPECT_EQ(subnet3, selected);
+}
+
+// This test verifies that it is possible to select subnet by interface
+// name specified on the shared network level.
+TEST(CfgSubnets4Test, selectSharedNetworkByIface) {
+ // The IfaceMgrTestConfig object initializes fake interfaces:
+ // eth0, eth1 and lo on the configuration manager. The CfgSubnets4
+ // object uses interface names to select the appropriate subnet.
+ IfaceMgrTestConfig config(true);
+
+ CfgSubnets4 cfg;
+
+ // Create 3 subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("172.16.2.0"), 24, 1, 2, 3,
+ SubnetID(1)));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("10.1.2.0"), 24, 1, 2, 3,
+ SubnetID(2)));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.3.4.0"), 24, 1, 2, 3,
+ SubnetID(3)));
+ subnet2->setIface("lo");
+
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ SharedNetwork4Ptr network(new SharedNetwork4("network_eth1"));
+ network->setIface("eth1");
+ ASSERT_NO_THROW(network->add(subnet1));
+ ASSERT_NO_THROW(network->add(subnet2));
+
+ // Make sure that initially the subnets don't exist.
+ SubnetSelector selector;
+ // Set an interface to a name that is not defined in the config.
+ // Subnet selection should fail.
+ selector.iface_name_ = "eth0";
+ ASSERT_FALSE(cfg.selectSubnet(selector));
+
+ // Now select an interface name that matches. Selection should succeed
+ // and return subnet3.
+ selector.iface_name_ = "eth1";
+ Subnet4Ptr selected = cfg.selectSubnet(selector);
+ ASSERT_TRUE(selected);
+ SharedNetwork4Ptr network_returned;
+ selected->getSharedNetwork(network_returned);
+ ASSERT_TRUE(network_returned);
+ EXPECT_EQ(network, network_returned);
+
+ const Subnet4SimpleCollection* subnets_eth1 =
+ network_returned->getAllSubnets();
+ EXPECT_EQ(2, subnets_eth1->size());
+ ASSERT_TRUE(network_returned->getSubnet(SubnetID(1)));
+ ASSERT_TRUE(network_returned->getSubnet(SubnetID(2)));
+
+ // Make sure that it is still possible to select subnet2 which is
+ // outside of a shared network.
+ selector.iface_name_ = "lo";
+ selected = cfg.selectSubnet(selector);
+ ASSERT_TRUE(selected);
+ EXPECT_EQ(2, selected->getID());
+
+ // Try selecting by eth1 again, but this time set subnet specific
+ // interface name to eth0. Subnet selection should fail.
+ selector.iface_name_ = "eth1";
+ subnet1->setIface("eth0");
+ subnet3->setIface("eth0");
+ selected = cfg.selectSubnet(selector);
+ ASSERT_FALSE(selected);
+
+ // It should be possible to select by eth0, though.
+ selector.iface_name_ = "eth0";
+ selected = cfg.selectSubnet(selector);
+ ASSERT_TRUE(selected);
+ EXPECT_EQ(subnet1, selected);
+}
+
+// This test verifies that when the classification information is specified for
+// subnets, the proper subnets are returned by the subnet configuration.
+TEST(CfgSubnets4Test, selectSubnetByClasses) {
+ CfgSubnets4 cfg;
+
+ // Create 3 subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3));
+
+ // Add them to the configuration.
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ SubnetSelector selector;
+
+ selector.local_address_ = IOAddress("10.0.0.10");
+
+ selector.ciaddr_ = IOAddress("192.0.2.5");
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+ selector.ciaddr_ = IOAddress("192.0.2.70");
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.ciaddr_ = IOAddress("192.0.2.130");
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+
+ ClientClasses client_classes;
+ client_classes.insert("bar");
+ selector.client_classes_ = client_classes;
+
+ // There are no class restrictions defined, so everything should work
+ // as before
+ selector.ciaddr_ = IOAddress("192.0.2.5");
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+ selector.ciaddr_ = IOAddress("192.0.2.70");
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.ciaddr_ = IOAddress("192.0.2.130");
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+
+ // Now let's add client class restrictions.
+ subnet1->allowClientClass("foo"); // Serve here only clients from foo class
+ subnet2->allowClientClass("bar"); // Serve here only clients from bar class
+ subnet3->allowClientClass("baz"); // Serve here only clients from baz class
+
+ // The same check as above should result in client being served only in
+ // bar class, i.e. subnet2.
+ selector.ciaddr_ = IOAddress("192.0.2.5");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.ciaddr_ = IOAddress("192.0.2.70");
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.ciaddr_ = IOAddress("192.0.2.130");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+
+ // Now let's check that client with wrong class is not supported.
+ client_classes.clear();
+ client_classes.insert("some_other_class");
+ selector.client_classes_ = client_classes;
+ selector.ciaddr_ = IOAddress("192.0.2.5");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.ciaddr_ = IOAddress("192.0.2.70");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.ciaddr_ = IOAddress("192.0.2.130");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+
+ // Finally, let's check that client without any classes is not supported.
+ client_classes.clear();
+ selector.ciaddr_ = IOAddress("192.0.2.5");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.ciaddr_ = IOAddress("192.0.2.70");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.ciaddr_ = IOAddress("192.0.2.130");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+}
+
+// This test verifies that shared network can be selected based on client
+// classification.
+TEST(CfgSubnets4Test, selectSharedNetworkByClasses) {
+ IfaceMgrTestConfig config(true);
+
+ CfgSubnets4 cfg;
+
+ // Create 3 subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3));
+
+ // Add them to the configuration.
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ // Create first network and add first two subnets to it.
+ SharedNetwork4Ptr network1(new SharedNetwork4("network1"));
+ network1->setIface("eth1");
+ network1->allowClientClass("device-type1");
+ ASSERT_NO_THROW(network1->add(subnet1));
+ ASSERT_NO_THROW(network1->add(subnet2));
+
+ // Create second network and add last subnet there.
+ SharedNetwork4Ptr network2(new SharedNetwork4("network2"));
+ network2->setIface("eth1");
+ network2->allowClientClass("device-type2");
+ ASSERT_NO_THROW(network2->add(subnet3));
+
+ // Use interface name as a selector. This guarantees that subnet
+ // selection will be made based on the classification.
+ SubnetSelector selector;
+ selector.iface_name_ = "eth1";
+
+ // If the client has "device-type2" class, it is expected that the
+ // second network will be used. This network has only one subnet
+ // in it, i.e. subnet3.
+ ClientClasses client_classes;
+ client_classes.insert("device-type2");
+ selector.client_classes_ = client_classes;
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+
+ // Switch to device-type1 and expect that we assigned a subnet from
+ // another shared network.
+ client_classes.clear();
+ client_classes.insert("device-type1");
+ selector.client_classes_ = client_classes;
+
+ Subnet4Ptr subnet = cfg.selectSubnet(selector);
+ ASSERT_TRUE(subnet);
+ SharedNetwork4Ptr network;
+ subnet->getSharedNetwork(network);
+ ASSERT_TRUE(network);
+ EXPECT_EQ("network1", network->getName());
+}
+
+// This test verifies the option selection can be used and is only
+// used when present.
+TEST(CfgSubnets4Test, selectSubnetByOptionSelect) {
+ CfgSubnets4 cfg;
+
+ // Create 3 subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3));
+
+ // Add them to the configuration.
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ SubnetSelector selector;
+
+ // Check that without option selection something else is used
+ selector.ciaddr_ = IOAddress("192.0.2.5");
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+
+ // The option selection has precedence
+ selector.option_select_ = IOAddress("192.0.2.130");
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+
+ // Over relay-info too
+ selector.giaddr_ = IOAddress("10.0.0.1");
+ subnet2->addRelayAddress(IOAddress("10.0.0.1"));
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+ selector.option_select_ = IOAddress("0.0.0.0");
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+
+ // Check that a not matching option selection it shall fail
+ selector.option_select_ = IOAddress("10.0.0.1");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+}
+
+// This test verifies that the relay information can be used to retrieve the
+// subnet.
+TEST(CfgSubnets4Test, selectSubnetByRelayAddress) {
+ CfgSubnets4 cfg;
+
+ // Create 3 subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3));
+
+ // Add them to the configuration.
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ SubnetSelector selector;
+
+ // Check that without relay-info specified, subnets are not selected
+ selector.giaddr_ = IOAddress("10.0.0.1");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.giaddr_ = IOAddress("10.0.0.2");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.giaddr_ = IOAddress("10.0.0.3");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+
+ // Now specify relay info
+ subnet1->addRelayAddress(IOAddress("10.0.0.1"));
+ subnet2->addRelayAddress(IOAddress("10.0.0.2"));
+ subnet3->addRelayAddress(IOAddress("10.0.0.3"));
+
+ // And try again. This time relay-info is there and should match.
+ selector.giaddr_ = IOAddress("10.0.0.1");
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+ selector.giaddr_ = IOAddress("10.0.0.2");
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.giaddr_ = IOAddress("10.0.0.3");
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+}
+
+// This test verifies that the relay information specified on the shared
+// network level can be used to select a subnet.
+TEST(CfgSubnets4Test, selectSharedNetworkByRelayAddressNetworkLevel) {
+ CfgSubnets4 cfg;
+
+ // Create 3 subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3));
+
+ // Add them to the configuration.
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ SharedNetwork4Ptr network(new SharedNetwork4("network"));
+ network->add(subnet2);
+
+ SubnetSelector selector;
+
+ // Now specify relay info. Note that for the second subnet we specify
+ // relay info on the network level.
+ subnet1->addRelayAddress(IOAddress("10.0.0.1"));
+ network->addRelayAddress(IOAddress("10.0.0.2"));
+ subnet3->addRelayAddress(IOAddress("10.0.0.3"));
+
+ // And try again. This time relay-info is there and should match.
+ selector.giaddr_ = IOAddress("10.0.0.1");
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+ selector.giaddr_ = IOAddress("10.0.0.2");
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.giaddr_ = IOAddress("10.0.0.3");
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+}
+
+// This test verifies that the relay information specified on the subnet
+// level can be used to select a subnet and the fact that a subnet belongs
+// to a shared network doesn't affect the process.
+TEST(CfgSubnets4Test, selectSharedNetworkByRelayAddressSubnetLevel) {
+ CfgSubnets4 cfg;
+
+ // Create 3 subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3));
+
+ // Add them to the configuration.
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ SharedNetwork4Ptr network1(new SharedNetwork4("network1"));
+ network1->add(subnet1);
+
+ SharedNetwork4Ptr network2(new SharedNetwork4("network2"));
+ network2->add(subnet2);
+
+ SubnetSelector selector;
+
+ // Now specify relay info. Note that for the second subnet we specify
+ // relay info on the network level.
+ subnet1->addRelayAddress(IOAddress("10.0.0.1"));
+ subnet2->addRelayAddress(IOAddress("10.0.0.2"));
+ subnet3->addRelayAddress(IOAddress("10.0.0.3"));
+
+ // And try again. This time relay-info is there and should match.
+ selector.giaddr_ = IOAddress("10.0.0.1");
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+ selector.giaddr_ = IOAddress("10.0.0.2");
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.giaddr_ = IOAddress("10.0.0.3");
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+}
+
+// This test verifies that the subnet can be selected for the client
+// using a source address if the client hasn't set the ciaddr.
+TEST(CfgSubnets4Test, selectSubnetNoCiaddr) {
+ CfgSubnets4 cfg;
+
+ // Create 3 subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3));
+
+ // Make sure that initially the subnets don't exist.
+ SubnetSelector selector;
+ selector.remote_address_ = IOAddress("192.0.2.0");
+ // Set some unicast local address to simulate a Renew.
+ selector.local_address_ = IOAddress("10.0.0.100");
+ ASSERT_FALSE(cfg.selectSubnet(selector));
+
+ // Add one subnet and make sure it is returned.
+ cfg.add(subnet1);
+ selector.remote_address_ = IOAddress("192.0.2.63");
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+
+ // Add all other subnets.
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ // Make sure they are returned for the appropriate addresses.
+ selector.remote_address_ = IOAddress("192.0.2.15");
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+ selector.remote_address_ = IOAddress("192.0.2.85");
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.remote_address_ = IOAddress("192.0.2.191");
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+
+ // Also, make sure that the NULL pointer is returned if the subnet
+ // cannot be found.
+ selector.remote_address_ = IOAddress("192.0.2.192");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+}
+
+// This test verifies that the subnet can be selected using an address
+// set on the local interface.
+TEST(CfgSubnets4Test, selectSubnetInterface) {
+ // The IfaceMgrTestConfig object initializes fake interfaces:
+ // eth0, eth1 and lo on the configuration manager. The CfgSubnets4
+ // object uses addresses assigned to these fake interfaces to
+ // select the appropriate subnet.
+ IfaceMgrTestConfig config(true);
+
+ CfgSubnets4 cfg;
+ SubnetSelector selector;
+
+ // Initially, there are no subnets configured, so none of the IPv4
+ // addresses assigned to eth0 and eth1 can match with any subnet.
+ selector.iface_name_ = "eth0";
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.iface_name_ = "eth1";
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+
+ // Configure first subnet which address on eth0 corresponds to.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("10.0.0.1"), 24, 1, 2, 3));
+ cfg.add(subnet1);
+
+ // The address on eth0 should match the existing subnet.
+ selector.iface_name_ = "eth0";
+ Subnet4Ptr subnet1_ret = cfg.selectSubnet(selector);
+ ASSERT_TRUE(subnet1_ret);
+ EXPECT_EQ(subnet1->get().first, subnet1_ret->get().first);
+ // There should still be no match for eth1.
+ selector.iface_name_ = "eth1";
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+
+ // Configure a second subnet.
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.1"), 24, 1, 2, 3));
+ cfg.add(subnet2);
+
+ // There should be match between eth0 and subnet1 and between eth1 and
+ // subnet 2.
+ selector.iface_name_ = "eth0";
+ subnet1_ret = cfg.selectSubnet(selector);
+ ASSERT_TRUE(subnet1_ret);
+ EXPECT_EQ(subnet1->get().first, subnet1_ret->get().first);
+ selector.iface_name_ = "eth1";
+ Subnet4Ptr subnet2_ret = cfg.selectSubnet(selector);
+ ASSERT_TRUE(subnet2_ret);
+ EXPECT_EQ(subnet2->get().first, subnet2_ret->get().first);
+
+ // This function throws an exception if the name of the interface is wrong.
+ selector.iface_name_ = "bogus-interface";
+ EXPECT_THROW(cfg.selectSubnet(selector), isc::BadValue);
+}
+
+// Checks that detection of duplicated subnet IDs works as expected. It should
+// not be possible to add two IPv4 subnets holding the same ID.
+TEST(CfgSubnets4Test, duplication) {
+ CfgSubnets4 cfg;
+
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 123));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, 124));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, 123));
+ Subnet4Ptr subnet4(new Subnet4(IOAddress("192.0.2.1"), 26, 1, 2, 3, 125));
+
+ ASSERT_NO_THROW(cfg.add(subnet1));
+ EXPECT_NO_THROW(cfg.add(subnet2));
+ // Subnet 3 has the same ID as subnet 1. It shouldn't be able to add it.
+ EXPECT_THROW(cfg.add(subnet3), isc::dhcp::DuplicateSubnetID);
+ // Subnet 4 has a similar but different subnet as subnet 1.
+ EXPECT_NO_THROW(cfg.add(subnet4));
+}
+
+// This test checks if the IPv4 subnet can be selected based on the IPv6 address.
+TEST(CfgSubnets4Test, 4o6subnetMatchByAddress) {
+ CfgSubnets4 cfg;
+
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 123));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, 124));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, 125));
+
+ subnet2->get4o6().setSubnet4o6(IOAddress("2001:db8:1::"), 48);
+ subnet3->get4o6().setSubnet4o6(IOAddress("2001:db8:2::"), 48);
+
+
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ SubnetSelector selector;
+ selector.dhcp4o6_ = true;
+ selector.remote_address_ = IOAddress("2001:db8:1::dead:beef");
+
+ EXPECT_EQ(subnet2, cfg.selectSubnet4o6(selector));
+}
+
+// This test checks if the IPv4 subnet can be selected based on the value of
+// interface-id option.
+TEST(CfgSubnets4Test, 4o6subnetMatchByInterfaceId) {
+ CfgSubnets4 cfg;
+
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 123));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, 124));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, 125));
+
+ const uint8_t dummyPayload1[] = { 1, 2, 3, 4};
+ const uint8_t dummyPayload2[] = { 1, 2, 3, 5};
+ std::vector<uint8_t> data1(dummyPayload1, dummyPayload1 + sizeof(dummyPayload1));
+ std::vector<uint8_t> data2(dummyPayload2, dummyPayload2 + sizeof(dummyPayload2));
+
+ OptionPtr interfaceId1(new Option(Option::V6, D6O_INTERFACE_ID, data1));
+ OptionPtr interfaceId2(new Option(Option::V6, D6O_INTERFACE_ID, data2));
+
+ subnet2->get4o6().setInterfaceId(interfaceId1);
+
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ SubnetSelector selector;
+ selector.dhcp4o6_ = true;
+ selector.interface_id_ = interfaceId2;
+ // We have mismatched interface-id options (data1 vs data2). Should not match.
+ EXPECT_FALSE(cfg.selectSubnet4o6(selector));
+
+ // This time we have correct interface-id. Should match.
+ selector.interface_id_ = interfaceId1;
+ EXPECT_EQ(subnet2, cfg.selectSubnet4o6(selector));
+}
+
+// This test checks if the IPv4 subnet can be selected based on the value of
+// interface name option.
+TEST(CfgSubnets4Test, 4o6subnetMatchByInterfaceName) {
+ CfgSubnets4 cfg;
+
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 123));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, 124));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, 125));
+
+ subnet2->get4o6().setIface4o6("eth7");
+
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ SubnetSelector selector;
+ selector.dhcp4o6_ = true;
+ selector.iface_name_ = "eth5";
+ // We have mismatched interface names. Should not match.
+ EXPECT_FALSE(cfg.selectSubnet4o6(selector));
+
+ // This time we have correct names. Should match.
+ selector.iface_name_ = "eth7";
+ EXPECT_EQ(subnet2, cfg.selectSubnet4o6(selector));
+}
+
+// This test check if IPv4 subnets can be unparsed in a predictable way,
+TEST(CfgSubnets4Test, unparseSubnet) {
+ CfgSubnets4 cfg;
+
+ // Add some subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 123));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, 124));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, 125));
+
+ subnet1->allowClientClass("foo");
+
+ subnet1->setT1Percent(0.45);
+ subnet1->setT2Percent(0.70);
+ subnet1->setCacheThreshold(0.20);
+
+ subnet2->setIface("lo");
+ subnet2->addRelayAddress(IOAddress("10.0.0.1"));
+ subnet2->setValid(Triplet<uint32_t>(100));
+ subnet2->setStoreExtendedInfo(true);
+ subnet2->setCacheMaxAge(80);
+
+ subnet3->setIface("eth1");
+ subnet3->requireClientClass("foo");
+ subnet3->requireClientClass("bar");
+ subnet3->setCalculateTeeTimes(true);
+ subnet3->setT1Percent(0.50);
+ subnet3->setT2Percent(0.65);
+ subnet3->setReservationsGlobal(false);
+ subnet3->setReservationsInSubnet(true);
+ subnet3->setReservationsOutOfPool(false);
+ subnet3->setAuthoritative(false);
+ subnet3->setMatchClientId(true);
+ subnet3->setSiaddr(IOAddress("192.0.2.2"));
+ subnet3->setSname("frog");
+ subnet3->setFilename("/dev/null");
+ subnet3->setValid(Triplet<uint32_t>(100, 200, 300));
+ subnet3->setDdnsSendUpdates(true);
+ subnet3->setDdnsOverrideNoUpdate(true);
+ subnet3->setDdnsOverrideClientUpdate(true);
+ subnet3->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_ALWAYS);
+ subnet3->setDdnsGeneratedPrefix("prefix");
+ subnet3->setDdnsQualifyingSuffix("example.com.");
+ subnet3->setHostnameCharSet("[^A-Z]");
+ subnet3->setHostnameCharReplacement("x");
+
+ 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);
+
+ // Unparse
+ std::string expected = "[\n"
+ "{\n"
+ " \"id\": 123,\n"
+ " \"subnet\": \"192.0.2.0/26\",\n"
+ " \"t1-percent\": 0.45,"
+ " \"t2-percent\": 0.7,"
+ " \"cache-threshold\": .20,\n"
+ " \"renew-timer\": 1,\n"
+ " \"rebind-timer\": 2,\n"
+ " \"relay\": { \"ip-addresses\": [ ] },\n"
+ " \"valid-lifetime\": 3,\n"
+ " \"min-valid-lifetime\": 3,\n"
+ " \"max-valid-lifetime\": 3,\n"
+ " \"client-class\": \"foo\",\n"
+ " \"4o6-interface\": \"\",\n"
+ " \"4o6-interface-id\": \"\",\n"
+ " \"4o6-subnet\": \"\",\n"
+ " \"option-data\": [ ],\n"
+ " \"pools\": [ ],\n"
+ " \"user-context\": { \"comment\": \"foo\" }\n"
+ "},{\n"
+ " \"id\": 124,\n"
+ " \"subnet\": \"192.0.2.64/26\",\n"
+ " \"interface\": \"lo\",\n"
+ " \"renew-timer\": 1,\n"
+ " \"rebind-timer\": 2,\n"
+ " \"relay\": { \"ip-addresses\": [ \"10.0.0.1\" ] },\n"
+ " \"valid-lifetime\": 100,\n"
+ " \"min-valid-lifetime\": 100,\n"
+ " \"max-valid-lifetime\": 100,\n"
+ " \"4o6-interface\": \"\",\n"
+ " \"4o6-interface-id\": \"\",\n"
+ " \"4o6-subnet\": \"\",\n"
+ " \"user-context\": {},\n"
+ " \"option-data\": [ ],\n"
+ " \"pools\": [ ],\n"
+ " \"store-extended-info\": true,\n"
+ " \"cache-max-age\": 80\n"
+ "},{\n"
+ " \"id\": 125,\n"
+ " \"subnet\": \"192.0.2.128/26\",\n"
+ " \"interface\": \"eth1\",\n"
+ " \"match-client-id\": true,\n"
+ " \"next-server\": \"192.0.2.2\",\n"
+ " \"server-hostname\": \"frog\",\n"
+ " \"boot-file-name\": \"/dev/null\",\n"
+ " \"renew-timer\": 1,\n"
+ " \"rebind-timer\": 2,\n"
+ " \"relay\": { \"ip-addresses\": [ ] },\n"
+ " \"valid-lifetime\": 200,\n"
+ " \"min-valid-lifetime\": 100,\n"
+ " \"max-valid-lifetime\": 300,\n"
+ " \"4o6-interface\": \"\",\n"
+ " \"4o6-interface-id\": \"\",\n"
+ " \"4o6-subnet\": \"\",\n"
+ " \"authoritative\": false,\n"
+ " \"reservations-global\": false,\n"
+ " \"reservations-in-subnet\": true,\n"
+ " \"reservations-out-of-pool\": false,\n"
+ " \"option-data\": [ ],\n"
+ " \"pools\": [ ]\n,"
+ " \"require-client-classes\": [ \"foo\", \"bar\" ],\n"
+ " \"calculate-tee-times\": true,\n"
+ " \"t1-percent\": 0.50,\n"
+ " \"t2-percent\": 0.65,\n"
+ " \"ddns-generated-prefix\": \"prefix\",\n"
+ " \"ddns-override-client-update\": true,\n"
+ " \"ddns-override-no-update\": true,\n"
+ " \"ddns-qualifying-suffix\": \"example.com.\",\n"
+ " \"ddns-replace-client-name\": \"always\",\n"
+ " \"ddns-send-updates\": true,\n"
+ " \"hostname-char-replacement\": \"x\",\n"
+ " \"hostname-char-set\": \"[^A-Z]\"\n"
+ "} ]\n";
+
+ runToElementTest<CfgSubnets4>(expected, cfg);
+}
+
+// This test check if IPv4 pools can be unparsed in a predictable way,
+TEST(CfgSubnets4Test, unparsePool) {
+ CfgSubnets4 cfg;
+
+ // Add a subnet with pools
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3, 123));
+ 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);
+ cfg.add(subnet);
+
+ // Unparse
+ std::string expected = "[\n"
+ "{\n"
+ " \"id\": 123,\n"
+ " \"subnet\": \"192.0.2.0/24\",\n"
+ " \"renew-timer\": 1,\n"
+ " \"rebind-timer\": 2,\n"
+ " \"relay\": { \"ip-addresses\": [ ] },\n"
+ " \"valid-lifetime\": 3,\n"
+ " \"min-valid-lifetime\": 3,\n"
+ " \"max-valid-lifetime\": 3,\n"
+ " \"4o6-interface\": \"\",\n"
+ " \"4o6-interface-id\": \"\",\n"
+ " \"4o6-subnet\": \"\",\n"
+ " \"option-data\": [],\n"
+ " \"pools\": [\n"
+ " {\n"
+ " \"option-data\": [ ],\n"
+ " \"pool\": \"192.0.2.1-192.0.2.10\",\n"
+ " \"user-context\": { \"comment\": \"foo\",\n"
+ " \"version\": 1 }\n"
+ " },{\n"
+ " \"option-data\": [ ],\n"
+ " \"pool\": \"192.0.2.64/26\",\n"
+ " \"user-context\": { \"foo\": \"bar\" },\n"
+ " \"client-class\": \"bar\",\n"
+ " \"require-client-classes\": [ \"foo\" ]\n"
+ " }\n"
+ " ]\n"
+ "} ]\n";
+ runToElementTest<CfgSubnets4>(expected, cfg);
+}
+
+// This test verifies that it is possible to retrieve a subnet using subnet-id.
+TEST(CfgSubnets4Test, getSubnet) {
+ CfgSubnets4 cfg;
+
+ // Create 3 subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 100));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.64"), 26, 1, 2, 3, 200));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.2.128"), 26, 1, 2, 3, 300));
+
+ // Add one subnet and make sure it is returned.
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ EXPECT_EQ(subnet1, cfg.getSubnet(100));
+ EXPECT_EQ(subnet2, cfg.getSubnet(200));
+ EXPECT_EQ(subnet3, cfg.getSubnet(300));
+ EXPECT_EQ(Subnet4Ptr(), cfg.getSubnet(400)); // no such subnet
+}
+
+// This test verifies that hasSubnetWithServerId returns correct value.
+TEST(CfgSubnets4Test, hasSubnetWithServerId) {
+ CfgSubnets4 cfg;
+
+ // Initially, there is no server identifier option present.
+ EXPECT_FALSE(cfg.hasSubnetWithServerId(IOAddress("1.2.3.4")));
+
+ OptionDefinitionPtr def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_SERVER_IDENTIFIER);
+ OptionCustomPtr opt_server_id(new OptionCustom(*def, Option::V4));
+ opt_server_id->writeAddress(IOAddress("1.2.3.4"));
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 100));
+ subnet->getCfgOption()->add(opt_server_id, false, DHCP4_OPTION_SPACE);
+ cfg.add(subnet);
+
+ EXPECT_TRUE(cfg.hasSubnetWithServerId(IOAddress("1.2.3.4")));
+ EXPECT_FALSE(cfg.hasSubnetWithServerId(IOAddress("2.3.4.5")));
+}
+
+// This test verifies the Subnet4 parser's validation logic for
+// t1-percent and t2-percent parameters.
+TEST(CfgSubnets4Test, teeTimePercentValidation) {
+
+ // Describes a single test scenario.
+ struct Scenario {
+ std::string label; // label used for logging test failures
+ bool calculate_tee_times; // value of calculate-tee-times parameter
+ double t1_percent; // value of t1-percent parameter
+ double t2_percent; // value of t2-percent parameter
+ std::string error_message; // expected error message is parsing should fail
+ };
+
+ // Test Scenarios.
+ std::vector<Scenario> tests = {
+ {"off and valid", false, .5, .95, ""},
+ {"on and valid", true, .5, .95, ""},
+ {"t2_negative", true, .5, -.95,
+ "subnet configuration failed: t2-percent:"
+ " -0.95 is invalid, it must be greater than 0.0 and less than 1.0"
+ },
+ {"t2_too_big", true, .5, 1.95,
+ "subnet configuration failed: t2-percent:"
+ " 1.95 is invalid, it must be greater than 0.0 and less than 1.0"
+ },
+ {"t1_negative", true, -.5, .95,
+ "subnet configuration failed: t1-percent:"
+ " -0.5 is invalid it must be greater than 0.0 and less than 1.0"
+ },
+ {"t1_too_big", true, 1.5, .95,
+ "subnet configuration failed: t1-percent:"
+ " 1.5 is invalid it must be greater than 0.0 and less than 1.0"
+ },
+ {"t1_bigger_than_t2", true, .85, .45,
+ "subnet configuration failed: t1-percent:"
+ " 0.85 is invalid, it must be less than t2-percent: 0.45"
+ }
+ };
+
+ // First we create a set of elements that provides all
+ // required for a Subnet4.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"10.1.2.0/24\", \n"
+ " \"interface\": \"\", \n"
+ " \"renew-timer\": 100, \n"
+ " \"rebind-timer\": 200, \n"
+ " \"valid-lifetime\": 300, \n"
+ " \"match-client-id\": false, \n"
+ " \"authoritative\": false, \n"
+ " \"next-server\": \"\", \n"
+ " \"server-hostname\": \"\", \n"
+ " \"boot-file-name\": \"\", \n"
+ " \"client-class\": \"\", \n"
+ " \"require-client-classes\": [] \n,"
+ " \"reservations-global\": false, \n"
+ " \"reservations-in-subnet\": true, \n"
+ " \"reservations-out-of-pool\": false, \n"
+ " \"4o6-interface\": \"\", \n"
+ " \"4o6-interface-id\": \"\", \n"
+ " \"4o6-subnet\": \"\" \n"
+ " }";
+
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ // Iterate over the test scenarios, verifying each prescribed
+ // outcome.
+ for (auto test = tests.begin(); test != tests.end(); ++test) {
+ {
+ SCOPED_TRACE("test: " + (*test).label);
+
+ // Set this scenario's configuration parameters
+ elems->set("calculate-tee-times", data::Element::create((*test).calculate_tee_times));
+ elems->set("t1-percent", data::Element::create((*test).t1_percent));
+ elems->set("t2-percent", data::Element::create((*test).t2_percent));
+
+ Subnet4Ptr subnet;
+ try {
+ // Attempt to parse the configuration.
+ Subnet4ConfigParser parser;
+ subnet = parser.parse(elems);
+ } catch (const std::exception& ex) {
+ if (!(*test).error_message.empty()) {
+ // We expected a failure, did we fail the correct way?
+ EXPECT_EQ((*test).error_message, ex.what());
+ } else {
+ // Should not have failed.
+ ADD_FAILURE() << "Scenario should not have failed: " << ex.what();
+ }
+
+ // Either way we're done with this scenario.
+ continue;
+ }
+
+ // We parsed correctly, make sure the values are right.
+ EXPECT_EQ((*test).calculate_tee_times, subnet->getCalculateTeeTimes());
+ EXPECT_TRUE(util::areDoublesEquivalent((*test).t1_percent, subnet->getT1Percent()));
+ EXPECT_TRUE(util::areDoublesEquivalent((*test).t2_percent, subnet->getT2Percent()));
+ }
+ }
+}
+
+// This test verifies the Subnet4 parser's validation logic for
+// valid-lifetime and indirectly shared lifetime parsing.
+TEST(CfgSubnets4Test, validLifetimeValidation) {
+ // First we create a set of elements that provides all
+ // required for a Subnet4.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"10.1.2.0/24\", \n"
+ " \"interface\": \"\", \n"
+ " \"renew-timer\": 100, \n"
+ " \"rebind-timer\": 200, \n"
+ " \"match-client-id\": false, \n"
+ " \"authoritative\": false, \n"
+ " \"next-server\": \"\", \n"
+ " \"server-hostname\": \"\", \n"
+ " \"boot-file-name\": \"\", \n"
+ " \"client-class\": \"\", \n"
+ " \"require-client-classes\": [] \n,"
+ " \"reservations-global\": false, \n"
+ " \"reservations-in-subnet\": true, \n"
+ " \"reservations-out-of-pool\": false, \n"
+ " \"4o6-interface\": \"\", \n"
+ " \"4o6-interface-id\": \"\", \n"
+ " \"4o6-subnet\": \"\" \n"
+ " }";
+
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ {
+ SCOPED_TRACE("no valid-lifetime");
+
+ data::ElementPtr copied = data::copy(elems);
+ Subnet4Ptr subnet;
+ Subnet4ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_TRUE(subnet->getValid().unspecified());
+ data::ConstElementPtr repr = subnet->toElement();
+ EXPECT_FALSE(repr->get("valid-lifetime"));
+ EXPECT_FALSE(repr->get("min-valid-lifetime"));
+ EXPECT_FALSE(repr->get("max-valid-lifetime"));
+ }
+
+ {
+ SCOPED_TRACE("valid-lifetime only");
+
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("valid-lifetime", data::Element::create(100));
+ Subnet4Ptr subnet;
+ Subnet4ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getValid().unspecified());
+ EXPECT_EQ(100, subnet->getValid());
+ EXPECT_EQ(100, subnet->getValid().getMin());
+ EXPECT_EQ(100, subnet->getValid().getMax());
+ data::ConstElementPtr repr = subnet->toElement();
+ data::ConstElementPtr value = repr->get("valid-lifetime");
+ ASSERT_TRUE(value);
+ EXPECT_EQ("100", value->str());
+ data::ConstElementPtr min_value = repr->get("min-valid-lifetime");
+ ASSERT_TRUE(min_value);
+ EXPECT_EQ("100", min_value->str());
+ data::ConstElementPtr max_value = repr->get("max-valid-lifetime");
+ ASSERT_TRUE(max_value);
+ EXPECT_EQ("100", max_value->str());
+ }
+
+ {
+ SCOPED_TRACE("min-valid-lifetime only");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("min-valid-lifetime", data::Element::create(100));
+ Subnet4Ptr subnet;
+ Subnet4ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getValid().unspecified());
+ EXPECT_EQ(100, subnet->getValid());
+ EXPECT_EQ(100, subnet->getValid().getMin());
+ EXPECT_EQ(100, subnet->getValid().getMax());
+ data::ConstElementPtr repr = subnet->toElement();
+ data::ConstElementPtr value = repr->get("valid-lifetime");
+ ASSERT_TRUE(value);
+ EXPECT_EQ("100", value->str());
+ data::ConstElementPtr min_value = repr->get("min-valid-lifetime");
+ ASSERT_TRUE(min_value);
+ EXPECT_EQ("100", min_value->str());
+ data::ConstElementPtr max_value = repr->get("max-valid-lifetime");
+ ASSERT_TRUE(max_value);
+ EXPECT_EQ("100", max_value->str());
+ }
+
+ {
+ SCOPED_TRACE("max-valid-lifetime only");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("max-valid-lifetime", data::Element::create(100));
+ Subnet4Ptr subnet;
+ Subnet4ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getValid().unspecified());
+ EXPECT_EQ(100, subnet->getValid());
+ EXPECT_EQ(100, subnet->getValid().getMin());
+ EXPECT_EQ(100, subnet->getValid().getMax());
+ data::ConstElementPtr repr = subnet->toElement();
+ data::ConstElementPtr value = repr->get("valid-lifetime");
+ ASSERT_TRUE(value);
+ EXPECT_EQ("100", value->str());
+ data::ConstElementPtr min_value = repr->get("min-valid-lifetime");
+ ASSERT_TRUE(min_value);
+ EXPECT_EQ("100", min_value->str());
+ data::ConstElementPtr max_value = repr->get("max-valid-lifetime");
+ ASSERT_TRUE(max_value);
+ EXPECT_EQ("100", max_value->str());
+ }
+
+ {
+ SCOPED_TRACE("min-valid-lifetime and valid-lifetime");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("min-valid-lifetime", data::Element::create(100));
+ // Use a different (and greater) value for the default.
+ copied->set("valid-lifetime", data::Element::create(200));
+ Subnet4Ptr subnet;
+ Subnet4ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getValid().unspecified());
+ EXPECT_EQ(200, subnet->getValid());
+ EXPECT_EQ(100, subnet->getValid().getMin());
+ EXPECT_EQ(200, subnet->getValid().getMax());
+ data::ConstElementPtr repr = subnet->toElement();
+ data::ConstElementPtr value = repr->get("valid-lifetime");
+ ASSERT_TRUE(value);
+ EXPECT_EQ("200", value->str());
+ data::ConstElementPtr min_value = repr->get("min-valid-lifetime");
+ ASSERT_TRUE(min_value);
+ EXPECT_EQ("100", min_value->str());
+ data::ConstElementPtr max_value = repr->get("max-valid-lifetime");
+ ASSERT_TRUE(max_value);
+ EXPECT_EQ("200", max_value->str());
+ }
+
+ {
+ SCOPED_TRACE("max-valid-lifetime and valid-lifetime");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("max-valid-lifetime", data::Element::create(200));
+ // Use a different (and smaller) value for the default.
+ copied->set("valid-lifetime", data::Element::create(100));
+ Subnet4Ptr subnet;
+ Subnet4ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getValid().unspecified());
+ EXPECT_EQ(100, subnet->getValid());
+ EXPECT_EQ(100, subnet->getValid().getMin());
+ EXPECT_EQ(200, subnet->getValid().getMax());
+ data::ConstElementPtr repr = subnet->toElement();
+ data::ConstElementPtr value = repr->get("valid-lifetime");
+ ASSERT_TRUE(value);
+ EXPECT_EQ("100", value->str());
+ data::ConstElementPtr min_value = repr->get("min-valid-lifetime");
+ ASSERT_TRUE(min_value);
+ EXPECT_EQ("100", min_value->str());
+ data::ConstElementPtr max_value = repr->get("max-valid-lifetime");
+ ASSERT_TRUE(max_value);
+ EXPECT_EQ("200", max_value->str());
+ }
+
+ {
+ SCOPED_TRACE("min-valid-lifetime and max-valid-lifetime");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("min-valid-lifetime", data::Element::create(100));
+ copied->set("max-valid-lifetime", data::Element::create(200));
+ Subnet4ConfigParser parser;
+ // No idea about the value to use for the default so failing.
+ ASSERT_THROW(parser.parse(copied), DhcpConfigError);
+ }
+
+ {
+ SCOPED_TRACE("all 3 (min, max and default) valid-lifetime");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("min-valid-lifetime", data::Element::create(100));
+ // Use a different (and greater) value for the default.
+ copied->set("valid-lifetime", data::Element::create(200));
+ // Use a different (and greater than both) value for max.
+ copied->set("max-valid-lifetime", data::Element::create(300));
+ Subnet4Ptr subnet;
+ Subnet4ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getValid().unspecified());
+ EXPECT_EQ(200, subnet->getValid());
+ EXPECT_EQ(100, subnet->getValid().getMin());
+ EXPECT_EQ(300, subnet->getValid().getMax());
+ data::ConstElementPtr repr = subnet->toElement();
+ data::ConstElementPtr value = repr->get("valid-lifetime");
+ ASSERT_TRUE(value);
+ EXPECT_EQ("200", value->str());
+ data::ConstElementPtr min_value = repr->get("min-valid-lifetime");
+ ASSERT_TRUE(min_value);
+ EXPECT_EQ("100", min_value->str());
+ data::ConstElementPtr max_value = repr->get("max-valid-lifetime");
+ ASSERT_TRUE(max_value);
+ EXPECT_EQ("300", max_value->str());
+ }
+
+ {
+ SCOPED_TRACE("default value too small");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("min-valid-lifetime", data::Element::create(200));
+ // 100 < 200 so it will fail.
+ copied->set("valid-lifetime", data::Element::create(100));
+ // Use a different (and greater than both) value for max.
+ copied->set("max-valid-lifetime", data::Element::create(300));
+ Subnet4ConfigParser parser;
+ ASSERT_THROW(parser.parse(copied), DhcpConfigError);
+ }
+
+ {
+ SCOPED_TRACE("default value too large");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("min-valid-lifetime", data::Element::create(100));
+ // Use a different (and greater) value for the default.
+ copied->set("valid-lifetime", data::Element::create(300));
+ // 300 > 200 so it will fail.
+ copied->set("max-valid-lifetime", data::Element::create(200));
+ Subnet4ConfigParser parser;
+ ASSERT_THROW(parser.parse(copied), DhcpConfigError);
+ }
+
+ {
+ SCOPED_TRACE("equal bounds are no longer ignored");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("min-valid-lifetime", data::Element::create(100));
+ copied->set("valid-lifetime", data::Element::create(100));
+ copied->set("max-valid-lifetime", data::Element::create(100));
+ Subnet4Ptr subnet;
+ Subnet4ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getValid().unspecified());
+ EXPECT_EQ(100, subnet->getValid());
+ EXPECT_EQ(100, subnet->getValid().getMin());
+ EXPECT_EQ(100, subnet->getValid().getMax());
+ data::ConstElementPtr repr = subnet->toElement();
+ data::ConstElementPtr value = repr->get("valid-lifetime");
+ ASSERT_TRUE(value);
+ EXPECT_EQ("100", value->str());
+ data::ConstElementPtr min_value = repr->get("min-valid-lifetime");
+ ASSERT_TRUE(min_value);
+ EXPECT_EQ("100", min_value->str());
+ data::ConstElementPtr max_value = repr->get("max-valid-lifetime");
+ ASSERT_TRUE(max_value);
+ EXPECT_EQ("100", max_value->str());
+ }
+}
+
+// This test verifies the Subnet4 parser's validation logic for
+// hostname sanitizer values.
+TEST(CfgSubnets4Test, hostnameSanitizierValidation) {
+
+ // First we create a set of elements that provides all
+ // required for a Subnet4.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"10.1.2.0/24\", \n"
+ " \"interface\": \"\", \n"
+ " \"renew-timer\": 100, \n"
+ " \"rebind-timer\": 200, \n"
+ " \"match-client-id\": false, \n"
+ " \"authoritative\": false, \n"
+ " \"next-server\": \"\", \n"
+ " \"server-hostname\": \"\", \n"
+ " \"boot-file-name\": \"\", \n"
+ " \"client-class\": \"\", \n"
+ " \"require-client-classes\": [] \n,"
+ " \"reservations-global\": false, \n"
+ " \"reservations-in-subnet\": true, \n"
+ " \"reservations-out-of-pool\": false, \n"
+ " \"4o6-interface\": \"\", \n"
+ " \"4o6-interface-id\": \"\", \n"
+ " \"4o6-subnet\": \"\" \n"
+ " }";
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ {
+ SCOPED_TRACE("invalid regular expression");
+
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("hostname-char-set", data::Element::create("^[A-"));
+ copied->set("hostname-char-replacement", data::Element::create("x"));
+ Subnet4Ptr subnet;
+ Subnet4ConfigParser parser;
+ EXPECT_THROW_MSG(subnet = parser.parse(copied), DhcpConfigError,
+ "subnet configuration failed: hostname-char-set "
+ "'^[A-' is not a valid regular expression");
+
+ }
+ {
+ SCOPED_TRACE("valid regular expression");
+
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("hostname-char-set", data::Element::create("^[A-Z]"));
+ copied->set("hostname-char-replacement", data::Element::create("x"));
+ Subnet4Ptr subnet;
+ Subnet4ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ EXPECT_EQ("^[A-Z]", subnet->getHostnameCharSet().get());
+ EXPECT_EQ("x", subnet->getHostnameCharReplacement().get());
+ }
+}
+
+// This test verifies the Subnet4 parser's validation logic for
+// lease cache parameters.
+TEST(CfgSubnets4Test, cacheParamValidation) {
+
+ // Describes a single test scenario.
+ struct Scenario {
+ std::string label; // label used for logging test failures
+ double threshold; // value of cache-threshold
+ std::string error_message; // expected error message is parsing should fail
+ };
+
+ // Test Scenarios.
+ std::vector<Scenario> tests = {
+ {"valid", .25, ""},
+ {"negative", -.25,
+ "subnet configuration failed: cache-threshold:"
+ " -0.25 is invalid, it must be greater than 0.0 and less than 1.0"
+ },
+ {"too big", 1.05,
+ "subnet configuration failed: cache-threshold:"
+ " 1.05 is invalid, it must be greater than 0.0 and less than 1.0"
+ }
+ };
+
+ // First we create a set of elements that provides all
+ // required for a Subnet4.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"10.1.2.0/24\", \n"
+ " \"interface\": \"\", \n"
+ " \"renew-timer\": 100, \n"
+ " \"rebind-timer\": 200, \n"
+ " \"valid-lifetime\": 300, \n"
+ " \"match-client-id\": false, \n"
+ " \"authoritative\": false, \n"
+ " \"next-server\": \"\", \n"
+ " \"server-hostname\": \"\", \n"
+ " \"boot-file-name\": \"\", \n"
+ " \"client-class\": \"\", \n"
+ " \"require-client-classes\": [] \n,"
+ " \"reservations-global\": false, \n"
+ " \"reservations-in-subnet\": true, \n"
+ " \"reservations-out-of-pool\": false, \n"
+ " \"4o6-interface\": \"\", \n"
+ " \"4o6-interface-id\": \"\", \n"
+ " \"4o6-subnet\": \"\" \n"
+ " }";
+
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ // Iterate over the test scenarios, verifying each prescribed
+ // outcome.
+ for (auto test = tests.begin(); test != tests.end(); ++test) {
+ {
+ SCOPED_TRACE("test: " + (*test).label);
+
+ // Set this scenario's configuration parameters
+ elems->set("cache-threshold", data::Element::create((*test).threshold));
+
+ Subnet4Ptr subnet;
+ try {
+ // Attempt to parse the configuration.
+ Subnet4ConfigParser parser;
+ subnet = parser.parse(elems);
+ } catch (const std::exception& ex) {
+ if (!(*test).error_message.empty()) {
+ // We expected a failure, did we fail the correct way?
+ EXPECT_EQ((*test).error_message, ex.what());
+ } else {
+ // Should not have failed.
+ ADD_FAILURE() << "Scenario should not have failed: " << ex.what();
+ }
+
+ // Either way we're done with this scenario.
+ continue;
+ }
+
+ // We parsed correctly, make sure the values are right.
+ EXPECT_TRUE(util::areDoublesEquivalent((*test).threshold, subnet->getCacheThreshold()));
+ }
+ }
+}
+
+// This test verifies that the optional interface check works as expected.
+TEST(CfgSubnets4Test, iface) {
+ // Create a configuration.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"10.1.2.0/24\", \n"
+ " \"interface\": \"eth1961\"\n"
+ " }";
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ // The interface check can be disabled.
+ Subnet4ConfigParser parser_no_check(false);
+ Subnet4Ptr subnet;
+ EXPECT_NO_THROW(subnet = parser_no_check.parse(elems));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getIface().unspecified());
+ EXPECT_EQ("eth1961", subnet->getIface().get());
+
+ // Retry with the interface check enabled.
+ Subnet4ConfigParser parser;
+ EXPECT_THROW(parser.parse(elems), DhcpConfigError);
+
+ // Configure default test interfaces.
+ IfaceMgrTestConfig config(true);
+
+ EXPECT_NO_THROW(subnet = parser_no_check.parse(elems));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getIface().unspecified());
+ EXPECT_EQ("eth1961", subnet->getIface().get());
+
+ EXPECT_NO_THROW(subnet = parser.parse(elems));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getIface().unspecified());
+ EXPECT_EQ("eth1961", subnet->getIface().get());
+}
+
+// This test verifies that update statistics works as expected.
+TEST(CfgSubnets4Test, updateStatistics) {
+ CfgMgr::instance().clear();
+
+ CfgSubnets4Ptr cfg = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4();
+ ObservationPtr observation;
+ SubnetID subnet_id = 100;
+
+ LeaseMgrFactory::create("type=memfile universe=4 persist=false");
+
+ // remove all statistics
+ StatsMgr::instance().removeAll();
+
+ // Create subnet.
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 100));
+
+ // Add subnet.
+ cfg->add(subnet);
+
+ observation = StatsMgr::instance().getObservation(
+ "cumulative-assigned-addresses");
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ "declined-addresses");
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ "reclaimed-declined-addresses");
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ "reclaimed-leases");
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-addresses"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-addresses"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-addresses"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "declined-addresses"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-declined-addresses"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-leases"));
+ ASSERT_FALSE(observation);
+
+ cfg->updateStatistics();
+
+ observation = StatsMgr::instance().getObservation(
+ "cumulative-assigned-addresses");
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ "declined-addresses");
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ "reclaimed-declined-addresses");
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ "reclaimed-leases");
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-addresses"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-addresses"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-addresses"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "declined-addresses"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-declined-addresses"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-leases"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+}
+
+// This test verifies that remove statistics works as expected.
+TEST(CfgSubnets4Test, removeStatistics) {
+ CfgSubnets4 cfg;
+ ObservationPtr observation;
+ SubnetID subnet_id = 100;
+
+ // remove all statistics and then set them all to 0
+ StatsMgr::instance().removeAll();
+
+ // Create subnet.
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, 100));
+
+ // Add subnet.
+ cfg.add(subnet);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-addresses"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-addresses"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-addresses"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-addresses"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-addresses"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-addresses"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "declined-addresses"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "declined-addresses"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-declined-addresses"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-declined-addresses"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-leases"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-leases"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ // remove all statistics
+ cfg.removeStatistics();
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-addresses"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-addresses"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-addresses"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "declined-addresses"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-declined-addresses"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-leases"));
+ ASSERT_FALSE(observation);
+}
+
+// This test verifies that in range host reservation works as expected.
+TEST(CfgSubnets4Test, host) {
+ // Create a configuration.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"10.1.2.0/24\", \n"
+ " \"reservations\": [ {\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\", \n"
+ " \"ip-address\": \"10.1.2.1\" } ]\n"
+ " }";
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ Subnet4ConfigParser parser;
+ Subnet4Ptr subnet;
+ EXPECT_NO_THROW(subnet = parser.parse(elems));
+ ASSERT_TRUE(subnet);
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_TRUE(cfg_hosts);
+ HostCollection hosts = cfg_hosts->getAll4(SubnetID(1));
+ ASSERT_EQ(1, hosts.size());
+ ConstHostPtr host = hosts[0];
+ ASSERT_TRUE(host);
+ EXPECT_EQ(1, host->getIPv4SubnetID());
+ EXPECT_EQ("hwaddr=AABBCCDDEEFF", host->getIdentifierAsText());
+ EXPECT_EQ("10.1.2.1", host->getIPv4Reservation().toText());
+
+ CfgMgr::instance().clear();
+}
+
+// This test verifies that an out of range host reservation is rejected.
+TEST(CfgSubnets4Test, outOfRangeHost) {
+ // Create a configuration.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"10.1.2.0/24\", \n"
+ " \"reservations\": [ {\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\", \n"
+ " \"ip-address\": \"10.2.2.1\" } ]\n"
+ " }";
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ Subnet4ConfigParser parser;
+ std::string msg = "specified reservation '10.2.2.1' is not within ";
+ msg += "the IPv4 subnet '10.1.2.0/24'";
+ EXPECT_THROW_MSG(parser.parse(elems), DhcpConfigError, msg);
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/cfg_subnets6_unittest.cc b/src/lib/dhcpsrv/tests/cfg_subnets6_unittest.cc
new file mode 100644
index 0000000..8d18aab
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfg_subnets6_unittest.cc
@@ -0,0 +1,2008 @@
+// Copyright (C) 2014-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcp/classify.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/option.h>
+#include <dhcp/option_string.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/cfg_shared_networks.h>
+#include <dhcpsrv/cfg_subnets6.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/parsers/dhcp_parsers.h>
+#include <dhcpsrv/subnet.h>
+#include <dhcpsrv/subnet_id.h>
+#include <dhcpsrv/subnet_selector.h>
+#include <dhcpsrv/cfg_hosts.h>
+#include <stats/stats_mgr.h>
+#include <testutils/gtest_utils.h>
+#include <testutils/test_to_element.h>
+#include <util/doubles.h>
+
+#include <gtest/gtest.h>
+#include <string>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::stats;
+using namespace isc::test;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Generates interface id option.
+///
+/// @param text Interface id in a textual format.
+OptionPtr
+generateInterfaceId(const std::string& text) {
+ OptionBuffer buffer(text.begin(), text.end());
+ return OptionPtr(new Option(Option::V6, D6O_INTERFACE_ID, buffer));
+}
+
+/// @brief Verifies that a set of subnets contains a given a subnet
+///
+/// @param cfg_subnets set of subnets in which to look
+/// @param exp_subnet_id expected id of the target subnet
+/// @param prefix prefix of the target subnet
+/// @param exp_valid expected valid lifetime of the subnet
+/// @param exp_network pointer to the subnet's shared-network (if one)
+void checkMergedSubnet(CfgSubnets6& cfg_subnets,
+ const std::string& prefix,
+ const SubnetID exp_subnet_id,
+ int exp_valid,
+ SharedNetwork6Ptr exp_network) {
+ // Look for the network by prefix.
+ auto subnet = cfg_subnets.getByPrefix(prefix);
+ ASSERT_TRUE(subnet) << "subnet: " << prefix << " not found";
+
+ // Make sure we have the one we expect.
+ ASSERT_EQ(exp_subnet_id, subnet->getID()) << "subnet ID is wrong";
+ ASSERT_EQ(exp_valid, subnet->getValid()) << "subnetID:"
+ << subnet->getID() << ", subnet valid time is wrong";
+
+ SharedNetwork6Ptr shared_network;
+ subnet->getSharedNetwork(shared_network);
+ if (exp_network) {
+ ASSERT_TRUE(shared_network)
+ << " expected network: " << exp_network->getName() << " not found";
+ ASSERT_TRUE(shared_network == exp_network) << " networks do no match";
+ } else {
+ ASSERT_FALSE(shared_network) << " unexpected network assignment: "
+ << shared_network->getName();
+ }
+}
+
+// This test verifies that specific subnet can be retrieved by specifying
+// subnet identifier or subnet prefix.
+TEST(CfgSubnets6Test, getSpecificSubnet) {
+ CfgSubnets6 cfg;
+
+ // Create 3 subnets.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4,
+ SubnetID(5)));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4,
+ SubnetID(8)));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4,
+ SubnetID(10)));
+
+ // Store the subnets in a vector to make it possible to loop over
+ // all configured subnets.
+ std::vector<Subnet6Ptr> subnets;
+ subnets.push_back(subnet1);
+ subnets.push_back(subnet2);
+ subnets.push_back(subnet3);
+
+ // Add all subnets to the configuration.
+ for (auto subnet = subnets.cbegin(); subnet != subnets.cend(); ++subnet) {
+ ASSERT_NO_THROW(cfg.add(*subnet)) << "failed to add subnet with id: "
+ << (*subnet)->getID();
+ }
+
+ // Iterate over all subnets and make sure they can be retrieved by
+ // subnet identifier.
+ for (auto subnet = subnets.rbegin(); subnet != subnets.rend(); ++subnet) {
+ ConstSubnet6Ptr subnet_returned = cfg.getBySubnetId((*subnet)->getID());
+ ASSERT_TRUE(subnet_returned) << "failed to return subnet with id: "
+ << (*subnet)->getID();
+ EXPECT_EQ((*subnet)->getID(), subnet_returned->getID());
+ EXPECT_EQ((*subnet)->toText(), subnet_returned->toText());
+ }
+
+ // Repeat the previous test, but this time retrieve subnets by their
+ // prefixes.
+ for (auto subnet = subnets.rbegin(); subnet != subnets.rend(); ++subnet) {
+ ConstSubnet6Ptr subnet_returned = cfg.getByPrefix((*subnet)->toText());
+ ASSERT_TRUE(subnet_returned) << "failed to return subnet with id: "
+ << (*subnet)->getID();
+ EXPECT_EQ((*subnet)->getID(), subnet_returned->getID());
+ EXPECT_EQ((*subnet)->toText(), subnet_returned->toText());
+ }
+
+ // Make sure that null pointers are returned for non-existing subnets.
+ EXPECT_FALSE(cfg.getBySubnetId(SubnetID(123)));
+ EXPECT_FALSE(cfg.getByPrefix("3000::/16"));
+}
+
+// This test verifies that a single subnet can be removed from the configuration.
+TEST(CfgSubnets6Test, deleteSubnet) {
+ CfgSubnets6 cfg;
+
+ // Create 3 subnets.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4));
+
+ ASSERT_NO_THROW(cfg.add(subnet1));
+ ASSERT_NO_THROW(cfg.add(subnet2));
+ ASSERT_NO_THROW(cfg.add(subnet3));
+
+ // There should be three subnets.
+ ASSERT_EQ(3, cfg.getAll()->size());
+ // We're going to remove the subnet #2. Let's make sure it exists before
+ // we remove it.
+ ASSERT_TRUE(cfg.getByPrefix("2001:db8:2::/48"));
+
+ // Remove the subnet and make sure it is gone.
+ ASSERT_NO_THROW(cfg.del(subnet2));
+ ASSERT_EQ(2, cfg.getAll()->size());
+ EXPECT_FALSE(cfg.getByPrefix("2001:db8:2::/48"));
+
+ // Remove another subnet by ID.
+ ASSERT_NO_THROW(cfg.del(subnet1->getID()));
+ ASSERT_EQ(1, cfg.getAll()->size());
+ EXPECT_FALSE(cfg.getByPrefix("2001:db8:1::/48"));
+}
+
+// This test verifies that replace a subnet works as expected.
+TEST(CfgSubnets6Test, replaceSubnet) {
+ CfgSubnets6 cfg;
+
+ // Create 3 subnets.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"),
+ 48, 1, 2, 3, 100, SubnetID(10)));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"),
+ 48, 1, 2, 3, 100, SubnetID(2)));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"),
+ 48, 1, 2, 3, 100, SubnetID(13)));
+
+ ASSERT_NO_THROW(cfg.add(subnet1));
+ ASSERT_NO_THROW(cfg.add(subnet2));
+ ASSERT_NO_THROW(cfg.add(subnet3));
+
+ // There should be three subnets.
+ ASSERT_EQ(3, cfg.getAll()->size());
+ // We're going to replace the subnet #2. Let's make sure it exists before
+ // we replace it.
+ ASSERT_TRUE(cfg.getByPrefix("2001:db8:2::/48"));
+
+ // Replace the subnet and make sure it was updated.
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:2::"),
+ 48, 10, 20, 30, 1000, SubnetID(2)));
+ Subnet6Ptr replaced = cfg.replace(subnet);
+ ASSERT_TRUE(replaced);
+ EXPECT_TRUE(replaced == subnet2);
+ ASSERT_EQ(3, cfg.getAll()->size());
+ Subnet6Ptr returned = cfg.getSubnet(SubnetID(2));
+ ASSERT_TRUE(returned);
+ EXPECT_TRUE(returned == subnet);
+
+ // Restore.
+ replaced = cfg.replace(replaced);
+ ASSERT_TRUE(replaced);
+ EXPECT_TRUE(replaced == subnet);
+ ASSERT_EQ(3, cfg.getAll()->size());
+ returned = cfg.getSubnet(SubnetID(2));
+ ASSERT_TRUE(returned);
+ EXPECT_TRUE(returned == subnet2);
+
+ // Prefix conflict returns null.
+ subnet.reset(new Subnet6(IOAddress("2001:db8:3::"),
+ 48, 10, 20, 30, 1000, SubnetID(2)));
+ replaced = cfg.replace(subnet);
+ EXPECT_FALSE(replaced);
+ returned = cfg.getSubnet(SubnetID(2));
+ ASSERT_TRUE(returned);
+ EXPECT_TRUE(returned == subnet2);
+
+ // Changing prefix works even it is highly not recommended.
+ subnet.reset(new Subnet6(IOAddress("2001:db8:10::"),
+ 48, 10, 20, 30, 1000, SubnetID(2)));
+ replaced = cfg.replace(subnet);
+ ASSERT_TRUE(replaced);
+ EXPECT_TRUE(replaced == subnet2);
+ returned = cfg.getSubnet(SubnetID(2));
+ ASSERT_TRUE(returned);
+ EXPECT_TRUE(returned == subnet);
+}
+
+// This test checks that the subnet can be selected using a relay agent's
+// link address.
+TEST(CfgSubnets6Test, selectSubnetByRelayAddress) {
+ CfgSubnets6 cfg;
+
+ // Let's configure 3 subnets
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4));
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ // Make sure that none of the subnets is selected when there is no relay
+ // information configured for them.
+ SubnetSelector selector;
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::1");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::2");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::3");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+
+ // Now specify relay information.
+ subnet1->addRelayAddress(IOAddress("2001:db8:ff::1"));
+ subnet2->addRelayAddress(IOAddress("2001:db8:ff::2"));
+ subnet3->addRelayAddress(IOAddress("2001:db8:ff::3"));
+
+ // And try again. This time relay-info is there and should match.
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::1");
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::2");
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::3");
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+}
+
+// This test checks that subnet can be selected using a relay agent's
+// link address specified on the shared network level.
+TEST(CfgSubnets6Test, selectSubnetByNetworkRelayAddress) {
+ // Create 2 shared networks.
+ SharedNetwork6Ptr network1(new SharedNetwork6("net1"));
+ SharedNetwork6Ptr network2(new SharedNetwork6("net2"));
+ SharedNetwork6Ptr network3(new SharedNetwork6("net3"));
+
+ // Let's configure 3 subnets
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4));
+
+ // Allow subnet class of clients to use the subnets.
+ subnet1->allowClientClass("subnet");
+ subnet2->allowClientClass("subnet");
+ subnet3->allowClientClass("subnet");
+
+ // Associate subnets with shared networks.
+ network1->add(subnet1);
+ network2->add(subnet2);
+ network3->add(subnet3);
+
+ // Add subnets to the configuration.
+ CfgSubnets6 cfg;
+
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ // Make sure that none of the subnets is selected when there is no relay
+ // information configured for them.
+ SubnetSelector selector;
+ selector.client_classes_.insert("subnet");
+
+ // The relays are not set for networks, so none of the subnets should
+ // be selected.
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::1");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::2");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::3");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+
+ // Now specify relay information.
+ network1->addRelayAddress(IOAddress("2001:db8:ff::1"));
+ network2->addRelayAddress(IOAddress("2001:db8:ff::2"));
+ network3->addRelayAddress(IOAddress("2001:db8:ff::3"));
+
+ // And try again. This time relay-info is there and should match.
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::1");
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::2");
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::3");
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+
+ // Modify the client classes associated with the first two subnets.
+ subnet1->allowClientClass("subnet1");
+ subnet2->allowClientClass("subnet2");
+
+ // This time the non-matching classes should prevent selection.
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::1");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::2");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::3");
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+}
+
+// This test checks that the subnet can be selected using an interface
+// name associated with a asubnet.
+TEST(CfgSubnets6Test, selectSubnetByInterfaceName) {
+ CfgSubnets6 cfg;
+
+ // Let's create 3 subnets.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"), 48, 1, 2, 3, 4));
+ subnet1->setIface("foo");
+ subnet2->setIface("bar");
+ subnet3->setIface("foobar");
+
+ // Until subnets are added to the configuration, there should be nothing
+ // returned.
+ SubnetSelector selector;
+ selector.iface_name_ = "foo";
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+
+ // Add one of the subnets.
+ cfg.add(subnet1);
+
+ // The subnet should be now selected for the interface name "foo".
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+
+ // Check that the interface name is checked even when there is
+ // only one subnet defined: there should be nothing returned when
+ // other interface name is specified.
+ selector.iface_name_ = "bar";
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+
+ // Add other subnets.
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ // When we specify correct interface names, the subnets should be returned.
+ selector.iface_name_ = "foobar";
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+ selector.iface_name_ = "bar";
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+
+ // When specifying a non-existing interface the subnet should not be
+ // returned.
+ selector.iface_name_ = "xyzzy";
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+}
+
+// This test checks that the subnet can be selected using an Interface ID
+// option inserted by a relay.
+TEST(CfgSubnets6Test, selectSubnetByInterfaceId) {
+ CfgSubnets6 cfg;
+
+ // Create 3 subnets.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"), 48, 1, 2, 3, 4));
+
+ // Create Interface-id options used in subnets 1,2, and 3
+ OptionPtr ifaceid1 = generateInterfaceId("relay1.eth0");
+ OptionPtr ifaceid2 = generateInterfaceId("VL32");
+ // That's a strange interface-id, but this is a real life example
+ OptionPtr ifaceid3 = generateInterfaceId("ISAM144|299|ipv6|nt:vp:1:110");
+
+ // Bogus interface-id.
+ OptionPtr ifaceid_bogus = generateInterfaceId("non-existent");
+
+ // Assign interface ids to the respective subnets.
+ subnet1->setInterfaceId(ifaceid1);
+ subnet2->setInterfaceId(ifaceid2);
+ subnet3->setInterfaceId(ifaceid3);
+
+ // There shouldn't be any subnet configured at this stage.
+ SubnetSelector selector;
+ selector.interface_id_ = ifaceid1;
+ // Note that some link address must be specified to indicate that it is
+ // a relayed message!
+ selector.first_relay_linkaddr_ = IOAddress("5000::1");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+
+ // Add one of the subnets.
+ cfg.add(subnet1);
+
+ // If only one subnet has been specified, it should be returned when the
+ // interface id matches. But, for a different interface id there should be
+ // no match.
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+ selector.interface_id_ = ifaceid2;
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+
+ // Add other subnets.
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ // Now that we have all subnets added. we should be able to retrieve them
+ // using appropriate interface ids.
+ selector.interface_id_ = ifaceid3;
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+ selector.interface_id_ = ifaceid2;
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+
+ // For invalid interface id, there should be nothing returned.
+ selector.interface_id_ = ifaceid_bogus;
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+}
+
+// Test that the client classes are considered when the subnet is selected by
+// the relay link address.
+TEST(CfgSubnets6Test, selectSubnetByRelayAddressAndClassify) {
+ CfgSubnets6 cfg;
+
+ // Let's configure 3 subnets
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"), 48, 1, 2, 3, 4));
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ // Let's sanity check that we can use that configuration.
+ SubnetSelector selector;
+ selector.first_relay_linkaddr_ = IOAddress("2000::123");
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("3000::345");
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("4000::567");
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+
+ // Client now belongs to bar class.
+ selector.client_classes_.insert("bar");
+
+ // There are no class restrictions defined, so everything should work
+ // as before.
+ selector.first_relay_linkaddr_ = IOAddress("2000::123");
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("3000::345");
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("4000::567");
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+
+ // Now let's add client class restrictions.
+ subnet1->allowClientClass("foo"); // Serve here only clients from foo class
+ subnet2->allowClientClass("bar"); // Serve here only clients from bar class
+ subnet3->allowClientClass("baz"); // Serve here only clients from baz class
+
+ // The same check as above should result in client being served only in
+ // bar class, i.e. subnet2
+ selector.first_relay_linkaddr_ = IOAddress("2000::123");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("3000::345");
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("4000::567");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+
+ // Now let's check that client with wrong class is not supported
+ selector.client_classes_.clear();
+ selector.client_classes_.insert("some_other_class");
+ selector.first_relay_linkaddr_ = IOAddress("2000::123");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("3000::345");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("4000::567");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+
+ // Finally, let's check that client without any classes is not supported
+ selector.client_classes_.clear();
+ selector.first_relay_linkaddr_ = IOAddress("2000::123");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("3000::345");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.first_relay_linkaddr_ = IOAddress("4000::567");
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+}
+
+// Test that client classes are considered when the subnet is selected by the
+// interface name.
+TEST(CfgSubnets6Test, selectSubnetByInterfaceNameAndClassify) {
+ CfgSubnets6 cfg;
+
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"), 48, 1, 2, 3, 4));
+ subnet1->setIface("foo");
+ subnet2->setIface("bar");
+ subnet3->setIface("foobar");
+
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ // Now we have only one subnet, any request will be served from it
+ SubnetSelector selector;
+ selector.client_classes_.insert("bar");
+ selector.iface_name_ = "foo";
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+ selector.iface_name_ = "bar";
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.iface_name_ = "foobar";
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+
+ subnet1->allowClientClass("foo"); // Serve here only clients from foo class
+ subnet2->allowClientClass("bar"); // Serve here only clients from bar class
+ subnet3->allowClientClass("baz"); // Serve here only clients from baz class
+
+ selector.iface_name_ = "foo";
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.iface_name_ = "bar";
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.iface_name_ = "foobar";
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+}
+
+// Test that client classes are considered when the interface is selected by
+// the interface id.
+TEST(CfgSubnets6Test, selectSubnetByInterfaceIdAndClassify) {
+ CfgSubnets6 cfg;
+
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"), 48, 1, 2, 3, 4));
+
+ // interface-id options used in subnets 1,2, and 3
+ OptionPtr ifaceid1 = generateInterfaceId("relay1.eth0");
+ OptionPtr ifaceid2 = generateInterfaceId("VL32");
+ // That's a strange interface-id, but this is a real life example
+ OptionPtr ifaceid3 = generateInterfaceId("ISAM144|299|ipv6|nt:vp:1:110");
+
+ // bogus interface-id
+ OptionPtr ifaceid_bogus = generateInterfaceId("non-existent");
+
+ subnet1->setInterfaceId(ifaceid1);
+ subnet2->setInterfaceId(ifaceid2);
+ subnet3->setInterfaceId(ifaceid3);
+
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ // If we have only a single subnet and the request came from a local
+ // address, let's use that subnet
+ SubnetSelector selector;
+ selector.first_relay_linkaddr_ = IOAddress("5000::1");
+ selector.client_classes_.insert("bar");
+ selector.interface_id_ = ifaceid1;
+ EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
+ selector.interface_id_ = ifaceid2;
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.interface_id_ = ifaceid3;
+ EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
+
+ subnet1->allowClientClass("foo"); // Serve here only clients from foo class
+ subnet2->allowClientClass("bar"); // Serve here only clients from bar class
+ subnet3->allowClientClass("baz"); // Serve here only clients from baz class
+
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+ selector.interface_id_ = ifaceid2;
+ EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
+ selector.interface_id_ = ifaceid3;
+ EXPECT_FALSE(cfg.selectSubnet(selector));
+}
+
+// Checks that detection of duplicated subnet IDs works as expected. It should
+// not be possible to add two IPv6 subnets holding the same ID.
+TEST(CfgSubnets6Test, duplication) {
+ CfgSubnets6 cfg;
+
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"), 48, 1, 2, 3, 4, 123));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 48, 1, 2, 3, 4, 124));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"), 48, 1, 2, 3, 4, 123));
+ Subnet6Ptr subnet4(new Subnet6(IOAddress("2000::1"), 48, 1, 2, 3, 4, 125));
+
+ ASSERT_NO_THROW(cfg.add(subnet1));
+ EXPECT_NO_THROW(cfg.add(subnet2));
+ // Subnet 3 has the same ID as subnet 1. It shouldn't be able to add it.
+ EXPECT_THROW(cfg.add(subnet3), isc::dhcp::DuplicateSubnetID);
+ // Subnet 4 has a similar but different subnet as subnet 1.
+ EXPECT_NO_THROW(cfg.add(subnet4));
+}
+
+// This test check if IPv6 subnets can be unparsed in a predictable way,
+TEST(CfgSubnets6Test, unparseSubnet) {
+ CfgSubnets6 cfg;
+
+ // Add some subnets.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"),
+ 48, 1, 2, 3, 4, 123));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"),
+ 48, 1, 2, 3, 4, 124));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"),
+ 48, 1, 2, 3, 4, 125));
+
+ OptionPtr ifaceid = generateInterfaceId("relay.eth0");
+ subnet1->setInterfaceId(ifaceid);
+ subnet1->allowClientClass("foo");
+
+ subnet1->setT1Percent(0.45);
+ subnet1->setT2Percent(0.70);
+ subnet1->setCacheThreshold(0.20);
+
+ subnet2->setIface("lo");
+ subnet2->addRelayAddress(IOAddress("2001:db8:ff::2"));
+ subnet2->setValid(Triplet<uint32_t>(200));
+ subnet2->setPreferred(Triplet<uint32_t>(100));
+ subnet2->setStoreExtendedInfo(true);
+ subnet2->setCacheMaxAge(80);
+
+ subnet3->setIface("eth1");
+ subnet3->requireClientClass("foo");
+ subnet3->requireClientClass("bar");
+ subnet3->setReservationsGlobal(false);
+ subnet3->setReservationsInSubnet(true);
+ subnet3->setReservationsOutOfPool(false);
+ subnet3->setRapidCommit(false);
+ subnet3->setCalculateTeeTimes(true);
+ subnet3->setT1Percent(0.50);
+ subnet3->setT2Percent(0.65);
+ subnet3->setValid(Triplet<uint32_t>(100, 200, 300));
+ subnet3->setPreferred(Triplet<uint32_t>(50, 100, 150));
+ subnet3->setDdnsSendUpdates(true);
+ subnet3->setDdnsOverrideNoUpdate(true);
+ subnet3->setDdnsOverrideClientUpdate(true);
+ subnet3->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_ALWAYS);
+ subnet3->setDdnsGeneratedPrefix("prefix");
+ subnet3->setDdnsQualifyingSuffix("example.com.");
+ subnet3->setHostnameCharSet("[^A-Z]");
+ subnet3->setHostnameCharReplacement("x");
+
+ 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);
+
+ // Unparse
+ std::string expected = "[\n"
+ "{\n"
+ " \"id\": 123,\n"
+ " \"subnet\": \"2001:db8:1::/48\",\n"
+ " \"t1-percent\": 0.45,"
+ " \"t2-percent\": 0.7,"
+ " \"cache-threshold\": .20,\n"
+ " \"interface-id\": \"relay.eth0\",\n"
+ " \"renew-timer\": 1,\n"
+ " \"rebind-timer\": 2,\n"
+ " \"relay\": { \"ip-addresses\": [ ] },\n"
+ " \"preferred-lifetime\": 3,\n"
+ " \"min-preferred-lifetime\": 3,\n"
+ " \"max-preferred-lifetime\": 3,\n"
+ " \"valid-lifetime\": 4,\n"
+ " \"min-valid-lifetime\": 4,\n"
+ " \"max-valid-lifetime\": 4,\n"
+ " \"client-class\": \"foo\",\n"
+ " \"pools\": [ ],\n"
+ " \"pd-pools\": [ ],\n"
+ " \"option-data\": [ ],\n"
+ " \"user-context\": { \"comment\": \"foo\" }\n"
+ "},{\n"
+ " \"id\": 124,\n"
+ " \"subnet\": \"2001:db8:2::/48\",\n"
+ " \"interface\": \"lo\",\n"
+ " \"renew-timer\": 1,\n"
+ " \"rebind-timer\": 2,\n"
+ " \"relay\": { \"ip-addresses\": [ \"2001:db8:ff::2\" ] },\n"
+ " \"preferred-lifetime\": 100,\n"
+ " \"min-preferred-lifetime\": 100,\n"
+ " \"max-preferred-lifetime\": 100,\n"
+ " \"valid-lifetime\": 200,\n"
+ " \"min-valid-lifetime\": 200,\n"
+ " \"max-valid-lifetime\": 200,\n"
+ " \"user-context\": { },\n"
+ " \"pools\": [ ],\n"
+ " \"pd-pools\": [ ],\n"
+ " \"option-data\": [ ],\n"
+ " \"store-extended-info\": true,\n"
+ " \"cache-max-age\": 80\n"
+ "},{\n"
+ " \"id\": 125,\n"
+ " \"subnet\": \"2001:db8:3::/48\",\n"
+ " \"interface\": \"eth1\",\n"
+ " \"renew-timer\": 1,\n"
+ " \"rebind-timer\": 2,\n"
+ " \"relay\": { \"ip-addresses\": [ ] },\n"
+ " \"preferred-lifetime\": 100,\n"
+ " \"min-preferred-lifetime\": 50,\n"
+ " \"max-preferred-lifetime\": 150,\n"
+ " \"valid-lifetime\": 200,\n"
+ " \"min-valid-lifetime\": 100,\n"
+ " \"max-valid-lifetime\": 300,\n"
+ " \"rapid-commit\": false,\n"
+ " \"reservations-global\": false,\n"
+ " \"reservations-in-subnet\": true,\n"
+ " \"reservations-out-of-pool\": false,\n"
+ " \"pools\": [ ],\n"
+ " \"pd-pools\": [ ],\n"
+ " \"option-data\": [ ],\n"
+ " \"require-client-classes\": [ \"foo\", \"bar\" ],\n"
+ " \"calculate-tee-times\": true,\n"
+ " \"t1-percent\": 0.50,\n"
+ " \"t2-percent\": 0.65,\n"
+ " \"ddns-generated-prefix\": \"prefix\",\n"
+ " \"ddns-override-client-update\": true,\n"
+ " \"ddns-override-no-update\": true,\n"
+ " \"ddns-qualifying-suffix\": \"example.com.\",\n"
+ " \"ddns-replace-client-name\": \"always\",\n"
+ " \"ddns-send-updates\": true,\n"
+ " \"hostname-char-replacement\": \"x\",\n"
+ " \"hostname-char-set\": \"[^A-Z]\"\n"
+ "} ]\n";
+
+ runToElementTest<CfgSubnets6>(expected, cfg);
+}
+
+// This test check if IPv6 pools can be unparsed in a predictable way,
+TEST(CfgSubnets6Test, unparsePool) {
+ CfgSubnets6 cfg;
+
+ // Add a subnet with pools
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"),
+ 48, 1, 2, 3, 4, 123));
+ Pool6Ptr pool1(new Pool6(Lease::TYPE_NA,
+ IOAddress("2001:db8:1::100"),
+ 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);
+ cfg.add(subnet);
+
+ // Unparse
+ std::string expected = "[\n"
+ "{\n"
+ " \"id\": 123,\n"
+ " \"subnet\": \"2001:db8:1::/48\",\n"
+ " \"renew-timer\": 1,\n"
+ " \"rebind-timer\": 2,\n"
+ " \"relay\": { \"ip-addresses\": [ ] },\n"
+ " \"preferred-lifetime\": 3,\n"
+ " \"min-preferred-lifetime\": 3,\n"
+ " \"max-preferred-lifetime\": 3,\n"
+ " \"valid-lifetime\": 4,\n"
+ " \"min-valid-lifetime\": 4,\n"
+ " \"max-valid-lifetime\": 4,\n"
+ " \"pools\": [\n"
+ " {\n"
+ " \"pool\": \"2001:db8:1::100-2001:db8:1::199\",\n"
+ " \"user-context\": { \"version\": 1,\n"
+ " \"comment\": \"foo\" },\n"
+ " \"option-data\": [ ]\n"
+ " },{\n"
+ " \"pool\": \"2001:db8:1:1::/64\",\n"
+ " \"user-context\": { \"foo\": \"bar\" },\n"
+ " \"option-data\": [ ],\n"
+ " \"client-class\": \"bar\",\n"
+ " \"require-client-classes\": [ \"foo\" ]\n"
+ " }\n"
+ " ],\n"
+ " \"pd-pools\": [ ],\n"
+ " \"option-data\": [ ]\n"
+ "} ]\n";
+ runToElementTest<CfgSubnets6>(expected, cfg);
+}
+
+// This test check if IPv6 prefix delegation pools can be unparsed
+// in a predictable way,
+TEST(CfgSubnets6Test, unparsePdPool) {
+ CfgSubnets6 cfg;
+
+ // Add a subnet with pd-pools
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"),
+ 48, 1, 2, 3, 4, 123));
+ Pool6Ptr pdpool1(new Pool6(Lease::TYPE_PD,
+ 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);
+ cfg.add(subnet);
+
+ // Unparse
+ std::string expected = "[\n"
+ "{\n"
+ " \"id\": 123,\n"
+ " \"subnet\": \"2001:db8:1::/48\",\n"
+ " \"renew-timer\": 1,\n"
+ " \"rebind-timer\": 2,\n"
+ " \"relay\": { \"ip-addresses\": [ ] },\n"
+ " \"preferred-lifetime\": 3,\n"
+ " \"min-preferred-lifetime\": 3,\n"
+ " \"max-preferred-lifetime\": 3,\n"
+ " \"valid-lifetime\": 4,\n"
+ " \"min-valid-lifetime\": 4,\n"
+ " \"max-valid-lifetime\": 4,\n"
+ " \"pools\": [ ],\n"
+ " \"pd-pools\": [\n"
+ " {\n"
+ " \"prefix\": \"2001:db8:2::\",\n"
+ " \"prefix-len\": 48,\n"
+ " \"delegated-len\": 64,\n"
+ " \"user-context\": { \"foo\": [ \"bar\" ] },\n"
+ " \"option-data\": [ ],\n"
+ " \"require-client-classes\": [ \"bar\" ]\n"
+ " },{\n"
+ " \"prefix\": \"2001:db8:3::\",\n"
+ " \"prefix-len\": 48,\n"
+ " \"delegated-len\": 56,\n"
+ " \"excluded-prefix\": \"2001:db8:3::\",\n"
+ " \"excluded-prefix-len\": 64,\n"
+ " \"option-data\": [ ],\n"
+ " \"client-class\": \"bar\"\n"
+ " }\n"
+ " ],\n"
+ " \"option-data\": [ ]\n"
+ "} ]\n";
+ runToElementTest<CfgSubnets6>(expected, cfg);
+}
+
+// This test verifies that it is possible to retrieve a subnet using subnet-id.
+TEST(CfgSubnets6Test, getSubnet) {
+ CfgSubnets6 cfg;
+
+ // Let's configure 3 subnets
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4, 100));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4, 200));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4, 300));
+ cfg.add(subnet1);
+ cfg.add(subnet2);
+ cfg.add(subnet3);
+
+ EXPECT_EQ(subnet1, cfg.getSubnet(100));
+ EXPECT_EQ(subnet2, cfg.getSubnet(200));
+ EXPECT_EQ(subnet3, cfg.getSubnet(300));
+ EXPECT_EQ(Subnet6Ptr(), cfg.getSubnet(400)); // no such subnet
+}
+
+// This test verifies that subnets configuration is properly merged.
+TEST(CfgSubnets6Test, mergeSubnets) {
+ // Create custom options dictionary for testing merge. We're keeping it
+ // simple because they are more rigorous tests elsewhere.
+ CfgOptionDefPtr cfg_def(new CfgOptionDef());
+ cfg_def->add((OptionDefinitionPtr(new OptionDefinition("one", 1, "isc", "string"))));
+
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:1::"),
+ 64, 1, 2, 100, 100, SubnetID(1)));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:2::"),
+ 64, 1, 2, 100, 100, SubnetID(2)));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:3::"),
+ 64, 1, 2, 100, 100, SubnetID(3)));
+ Subnet6Ptr subnet4(new Subnet6(IOAddress("2001:4::"),
+ 64, 1, 2, 100, 100, SubnetID(4)));
+
+ // Create the "existing" list of shared networks
+ CfgSharedNetworks6Ptr networks(new CfgSharedNetworks6());
+ SharedNetwork6Ptr shared_network1(new SharedNetwork6("shared-network1"));
+ networks->add(shared_network1);
+ SharedNetwork6Ptr shared_network2(new SharedNetwork6("shared-network2"));
+ networks->add(shared_network2);
+
+ // Empty network pointer.
+ SharedNetwork6Ptr no_network;
+
+ // Add Subnets 1, 2 and 4 to shared networks.
+ ASSERT_NO_THROW(shared_network1->add(subnet1));
+ ASSERT_NO_THROW(shared_network2->add(subnet2));
+ ASSERT_NO_THROW(shared_network2->add(subnet4));
+
+ // Create our "existing" configured subnets.
+ CfgSubnets6 cfg_to;
+ ASSERT_NO_THROW(cfg_to.add(subnet1));
+ ASSERT_NO_THROW(cfg_to.add(subnet2));
+ ASSERT_NO_THROW(cfg_to.add(subnet3));
+ ASSERT_NO_THROW(cfg_to.add(subnet4));
+
+ // Merge in an "empty" config. Should have the original config,
+ // still intact.
+ CfgSubnets6 cfg_from;
+ ASSERT_NO_THROW(cfg_to.merge(cfg_def, networks, cfg_from));
+
+ // We should have all four subnets, with no changes.
+ ASSERT_EQ(4, cfg_to.getAll()->size());
+
+ // Should be no changes to the configuration.
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:1::/64",
+ SubnetID(1), 100, shared_network1));
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:2::/64",
+ SubnetID(2), 100, shared_network2));
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:3::/64",
+ SubnetID(3), 100, no_network));
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:4::/64",
+ SubnetID(4), 100, shared_network2));
+
+ // Fill cfg_from configuration with subnets.
+ // subnet 1b updates subnet 1 but leaves it in network 1 with the same ID.
+ Subnet6Ptr subnet1b(new Subnet6(IOAddress("2001:10::"),
+ 64, 2, 3, 100, 400, SubnetID(1)));
+ subnet1b->setSharedNetworkName("shared-network1");
+
+ // Add generic option 1 to subnet 1b.
+ std::string value("Yay!");
+ OptionPtr option(new Option(Option::V6, 1));
+ option->setData(value.begin(), value.end());
+ ASSERT_NO_THROW(subnet1b->getCfgOption()->add(option, false, "isc"));
+
+ // subnet 3b updates subnet 3 with different UD and removes it
+ // from network 2
+ Subnet6Ptr subnet3b(new Subnet6(IOAddress("2001:3::"),
+ 64, 3, 4, 100, 500, SubnetID(30)));
+
+ // Now Add generic option 1 to subnet 3b.
+ value = "Team!";
+ option.reset(new Option(Option::V6, 1));
+ option->setData(value.begin(), value.end());
+ ASSERT_NO_THROW(subnet3b->getCfgOption()->add(option, false, "isc"));
+
+ // subnet 4b updates subnet 4 and moves it from network2 to network 1
+ Subnet6Ptr subnet4b(new Subnet6(IOAddress("2001:4::"),
+ 64, 3, 4, 100, 500, SubnetID(4)));
+ subnet4b->setSharedNetworkName("shared-network1");
+
+ // subnet 5 is new and belongs to network 2
+ // Has two pools both with an option 1
+ Subnet6Ptr subnet5(new Subnet6(IOAddress("2001:5::"),
+ 64, 1, 2, 100, 300, SubnetID(5)));
+ subnet5->setSharedNetworkName("shared-network2");
+
+ // Add pool 1
+ Pool6Ptr pool(new Pool6(Lease::TYPE_NA, IOAddress("2001:5::10"), IOAddress("2001:5::20")));
+ value = "POOLS";
+ option.reset(new Option(Option::V6, 1));
+ option->setData(value.begin(), value.end());
+ ASSERT_NO_THROW(pool->getCfgOption()->add(option, false, "isc"));
+ subnet5->addPool(pool);
+
+ // Add pool 2
+ pool.reset(new Pool6(Lease::TYPE_PD, IOAddress("2001:6::"), 64));
+ value ="RULE!";
+ option.reset(new Option(Option::V6, 1));
+ option->setData(value.begin(), value.end());
+ ASSERT_NO_THROW(pool->getCfgOption()->add(option, false, "isc"));
+ subnet5->addPool(pool);
+
+ // Add subnets to the merge from config.
+ ASSERT_NO_THROW(cfg_from.add(subnet1b));
+ ASSERT_NO_THROW(cfg_from.add(subnet3b));
+ ASSERT_NO_THROW(cfg_from.add(subnet4b));
+ ASSERT_NO_THROW(cfg_from.add(subnet5));
+
+ // Merge again.
+ ASSERT_NO_THROW(cfg_to.merge(cfg_def, networks, cfg_from));
+ ASSERT_EQ(5, cfg_to.getAll()->size());
+
+ // The subnet1 should be replaced by subnet1b.
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:10::/64",
+ SubnetID(1), 400, shared_network1));
+
+ // Let's verify that our option is there and populated correctly.
+ auto subnet = cfg_to.getByPrefix("2001:10::/64");
+ auto desc = subnet->getCfgOption()->get("isc", 1);
+ ASSERT_TRUE(desc.option_);
+ OptionStringPtr opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("Yay!", opstr->getValue());
+
+ // The subnet2 should not be affected because it was not present.
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:2::/64",
+ SubnetID(2), 100, shared_network2));
+
+ // subnet3 should be replaced by subnet3b and no longer assigned to a network.
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:3::/64",
+ SubnetID(30), 500, no_network));
+ // Let's verify that our option is there and populated correctly.
+ subnet = cfg_to.getByPrefix("2001:3::/64");
+ desc = subnet->getCfgOption()->get("isc", 1);
+ ASSERT_TRUE(desc.option_);
+ opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("Team!", opstr->getValue());
+
+ // subnet4 should be replaced by subnet4b and moved to network1.
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:4::/64",
+ SubnetID(4), 500, shared_network1));
+
+ // subnet5 should have been added to configuration.
+ ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:5::/64",
+ SubnetID(5), 300, shared_network2));
+
+ // Let's verify that both pools have the proper options.
+ subnet = cfg_to.getByPrefix("2001:5::/64");
+ const PoolPtr merged_pool = subnet->getPool(Lease::TYPE_NA, IOAddress("2001:5::10"));
+ ASSERT_TRUE(merged_pool);
+ desc = merged_pool->getCfgOption()->get("isc", 1);
+ opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("POOLS", opstr->getValue());
+
+ const PoolPtr merged_pool2 = subnet->getPool(Lease::TYPE_PD, IOAddress("2001:1::"));
+ ASSERT_TRUE(merged_pool2);
+ desc = merged_pool2->getCfgOption()->get("isc", 1);
+ opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
+ ASSERT_TRUE(opstr);
+ EXPECT_EQ("RULE!", opstr->getValue());
+}
+
+// This test verifies the Subnet6 parser's validation logic for
+// t1-percent and t2-percent parameters.
+TEST(CfgSubnets6Test, teeTimePercentValidation) {
+
+ // Describes a single test scenario.
+ struct Scenario {
+ std::string label; // label used for logging test failures
+ bool calculate_tee_times; // value of calculate-tee-times parameter
+ double t1_percent; // value of t1-percent parameter
+ double t2_percent; // value of t2-percent parameter
+ std::string error_message; // expected error message is parsing should fail
+ };
+
+ // Test Scenarios.
+ std::vector<Scenario> tests = {
+ {"off and valid", false, .5, .95, ""},
+ {"on and valid", true, .5, .95, ""},
+ {"t2_negative", true, .5, -.95,
+ "subnet configuration failed: t2-percent:"
+ " -0.95 is invalid, it must be greater than 0.0 and less than 1.0"
+ },
+ {"t2_too_big", true, .5, 1.95,
+ "subnet configuration failed: t2-percent:"
+ " 1.95 is invalid, it must be greater than 0.0 and less than 1.0"
+ },
+ {"t1_negative", true, -.5, .95,
+ "subnet configuration failed: t1-percent:"
+ " -0.5 is invalid it must be greater than 0.0 and less than 1.0"
+ },
+ {"t1_too_big", true, 1.5, .95,
+ "subnet configuration failed: t1-percent:"
+ " 1.5 is invalid it must be greater than 0.0 and less than 1.0"
+ },
+ {"t1_bigger_than_t2", true, .85, .45,
+ "subnet configuration failed: t1-percent:"
+ " 0.85 is invalid, it must be less than t2-percent: 0.45"
+ }
+ };
+
+ // First we create a set of elements that provides all
+ // required for a Subnet6.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"2001:db8:1::/64\", \n"
+ " \"interface\": \"\", \n"
+ " \"renew-timer\": 100, \n"
+ " \"rebind-timer\": 200, \n"
+ " \"valid-lifetime\": 300, \n"
+ " \"client-class\": \"\", \n"
+ " \"require-client-classes\": [] \n,"
+ " \"reservations-global\": false, \n"
+ " \"reservations-in-subnet\": true, \n"
+ " \"reservations-out-of-pool\": false \n"
+ " }";
+
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ // Iterate over the test scenarios, verifying each prescribed
+ // outcome.
+ for (auto test = tests.begin(); test != tests.end(); ++test) {
+ {
+ SCOPED_TRACE("test: " + (*test).label);
+
+ // Set this scenario's configuration parameters
+ elems->set("calculate-tee-times", data::Element::create((*test).calculate_tee_times));
+ elems->set("t1-percent", data::Element::create((*test).t1_percent));
+ elems->set("t2-percent", data::Element::create((*test).t2_percent));
+
+ Subnet6Ptr subnet;
+ try {
+ // Attempt to parse the configuration.
+ Subnet6ConfigParser parser;
+ subnet = parser.parse(elems);
+ } catch (const std::exception& ex) {
+ if (!(*test).error_message.empty()) {
+ // We expected a failure, did we fail the correct way?
+ EXPECT_EQ((*test).error_message, ex.what());
+ } else {
+ // Should not have failed.
+ ADD_FAILURE() << "Scenario should not have failed: " << ex.what();
+ }
+
+ // Either way we're done with this scenario.
+ continue;
+ }
+
+ // We parsed correctly, make sure the values are right.
+ EXPECT_EQ((*test).calculate_tee_times, subnet->getCalculateTeeTimes());
+ EXPECT_TRUE(util::areDoublesEquivalent((*test).t1_percent, subnet->getT1Percent()))
+ << "expected:" << (*test).t1_percent << " actual: " << subnet->getT1Percent();
+ EXPECT_TRUE(util::areDoublesEquivalent((*test).t2_percent, subnet->getT2Percent()))
+ << "expected:" << (*test).t2_percent << " actual: " << subnet->getT2Percent();
+ }
+ }
+}
+
+// This test verifies the Subnet6 parser's validation logic for
+// preferred-lifetime and indirectly shared lifetime parsing.
+// Note the valid-lifetime stuff is similar and already done for Subnet4.
+TEST(CfgSubnets6Test, preferredLifetimeValidation) {
+ // First we create a set of elements that provides all
+ // required for a Subnet6.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"2001:db8:1::/64\", \n"
+ " \"interface\": \"\", \n"
+ " \"renew-timer\": 100, \n"
+ " \"rebind-timer\": 200, \n"
+ " \"valid-lifetime\": 300, \n"
+ " \"client-class\": \"\", \n"
+ " \"require-client-classes\": [] \n,"
+ " \"reservations-global\": false, \n"
+ " \"reservations-in-subnet\": true, \n"
+ " \"reservations-out-of-pool\": false \n"
+ " }";
+
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ {
+ SCOPED_TRACE("no preferred-lifetime");
+
+ data::ElementPtr copied = data::copy(elems);
+ Subnet6Ptr subnet;
+ Subnet6ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_TRUE(subnet->getPreferred().unspecified());
+ data::ConstElementPtr repr = subnet->toElement();
+ EXPECT_FALSE(repr->get("preferred-lifetime"));
+ EXPECT_FALSE(repr->get("min-preferred-lifetime"));
+ EXPECT_FALSE(repr->get("max-preferred-lifetime"));
+ }
+
+ {
+ SCOPED_TRACE("preferred-lifetime only");
+
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("preferred-lifetime", data::Element::create(100));
+ Subnet6Ptr subnet;
+ Subnet6ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getPreferred().unspecified());
+ EXPECT_EQ(100, subnet->getPreferred());
+ EXPECT_EQ(100, subnet->getPreferred().getMin());
+ EXPECT_EQ(100, subnet->getPreferred().getMax());
+ data::ConstElementPtr repr = subnet->toElement();
+ data::ConstElementPtr value = repr->get("preferred-lifetime");
+ ASSERT_TRUE(value);
+ EXPECT_EQ("100", value->str());
+ data::ConstElementPtr min_value = repr->get("min-preferred-lifetime");
+ ASSERT_TRUE(min_value);
+ EXPECT_EQ("100", min_value->str());
+ data::ConstElementPtr max_value = repr->get("max-preferred-lifetime");
+ ASSERT_TRUE(max_value);
+ EXPECT_EQ("100", max_value->str());
+ }
+
+ {
+ SCOPED_TRACE("min-preferred-lifetime only");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("min-preferred-lifetime", data::Element::create(100));
+ Subnet6Ptr subnet;
+ Subnet6ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getPreferred().unspecified());
+ EXPECT_EQ(100, subnet->getPreferred());
+ EXPECT_EQ(100, subnet->getPreferred().getMin());
+ EXPECT_EQ(100, subnet->getPreferred().getMax());
+ data::ConstElementPtr repr = subnet->toElement();
+ data::ConstElementPtr value = repr->get("preferred-lifetime");
+ ASSERT_TRUE(value);
+ EXPECT_EQ("100", value->str());
+ data::ConstElementPtr min_value = repr->get("min-preferred-lifetime");
+ ASSERT_TRUE(min_value);
+ EXPECT_EQ("100", min_value->str());
+ data::ConstElementPtr max_value = repr->get("max-preferred-lifetime");
+ ASSERT_TRUE(max_value);
+ EXPECT_EQ("100", max_value->str());
+ }
+
+ {
+ SCOPED_TRACE("max-preferred-lifetime only");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("max-preferred-lifetime", data::Element::create(100));
+ Subnet6Ptr subnet;
+ Subnet6ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getPreferred().unspecified());
+ EXPECT_EQ(100, subnet->getPreferred());
+ EXPECT_EQ(100, subnet->getPreferred().getMin());
+ EXPECT_EQ(100, subnet->getPreferred().getMax());
+ data::ConstElementPtr repr = subnet->toElement();
+ data::ConstElementPtr value = repr->get("preferred-lifetime");
+ ASSERT_TRUE(value);
+ EXPECT_EQ("100", value->str());
+ data::ConstElementPtr min_value = repr->get("min-preferred-lifetime");
+ ASSERT_TRUE(min_value);
+ EXPECT_EQ("100", min_value->str());
+ data::ConstElementPtr max_value = repr->get("max-preferred-lifetime");
+ ASSERT_TRUE(max_value);
+ EXPECT_EQ("100", max_value->str());
+ }
+
+ {
+ SCOPED_TRACE("min-preferred-lifetime and preferred-lifetime");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("min-preferred-lifetime", data::Element::create(100));
+ // Use a different (and greater) value for the default.
+ copied->set("preferred-lifetime", data::Element::create(200));
+ Subnet6Ptr subnet;
+ Subnet6ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getPreferred().unspecified());
+ EXPECT_EQ(200, subnet->getPreferred());
+ EXPECT_EQ(100, subnet->getPreferred().getMin());
+ EXPECT_EQ(200, subnet->getPreferred().getMax());
+ data::ConstElementPtr repr = subnet->toElement();
+ data::ConstElementPtr value = repr->get("preferred-lifetime");
+ ASSERT_TRUE(value);
+ EXPECT_EQ("200", value->str());
+ data::ConstElementPtr min_value = repr->get("min-preferred-lifetime");
+ ASSERT_TRUE(min_value);
+ EXPECT_EQ("100", min_value->str());
+ data::ConstElementPtr max_value = repr->get("max-preferred-lifetime");
+ ASSERT_TRUE(max_value);
+ EXPECT_EQ("200", max_value->str());
+ }
+
+ {
+ SCOPED_TRACE("max-preferred-lifetime and preferred-lifetime");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("max-preferred-lifetime", data::Element::create(200));
+ // Use a different (and smaller) value for the default.
+ copied->set("preferred-lifetime", data::Element::create(100));
+ Subnet6Ptr subnet;
+ Subnet6ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getPreferred().unspecified());
+ EXPECT_EQ(100, subnet->getPreferred());
+ EXPECT_EQ(100, subnet->getPreferred().getMin());
+ EXPECT_EQ(200, subnet->getPreferred().getMax());
+ data::ConstElementPtr repr = subnet->toElement();
+ data::ConstElementPtr value = repr->get("preferred-lifetime");
+ ASSERT_TRUE(value);
+ EXPECT_EQ("100", value->str());
+ data::ConstElementPtr min_value = repr->get("min-preferred-lifetime");
+ ASSERT_TRUE(min_value);
+ EXPECT_EQ("100", min_value->str());
+ data::ConstElementPtr max_value = repr->get("max-preferred-lifetime");
+ ASSERT_TRUE(max_value);
+ EXPECT_EQ("200", max_value->str());
+ }
+
+ {
+ SCOPED_TRACE("min-preferred-lifetime and max-preferred-lifetime");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("min-preferred-lifetime", data::Element::create(100));
+ copied->set("max-preferred-lifetime", data::Element::create(200));
+ Subnet6ConfigParser parser;
+ // No idea about the value to use for the default so failing.
+ ASSERT_THROW(parser.parse(copied), DhcpConfigError);
+ }
+
+ {
+ SCOPED_TRACE("all 3 (min, max and default) preferred-lifetime");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("min-preferred-lifetime", data::Element::create(100));
+ // Use a different (and greater) value for the default.
+ copied->set("preferred-lifetime", data::Element::create(200));
+ // Use a different (and greater than both) value for max.
+ copied->set("max-preferred-lifetime", data::Element::create(300));
+ Subnet6Ptr subnet;
+ Subnet6ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getPreferred().unspecified());
+ EXPECT_EQ(200, subnet->getPreferred());
+ EXPECT_EQ(100, subnet->getPreferred().getMin());
+ EXPECT_EQ(300, subnet->getPreferred().getMax());
+ data::ConstElementPtr repr = subnet->toElement();
+ data::ConstElementPtr value = repr->get("preferred-lifetime");
+ ASSERT_TRUE(value);
+ EXPECT_EQ("200", value->str());
+ data::ConstElementPtr min_value = repr->get("min-preferred-lifetime");
+ ASSERT_TRUE(min_value);
+ EXPECT_EQ("100", min_value->str());
+ data::ConstElementPtr max_value = repr->get("max-preferred-lifetime");
+ ASSERT_TRUE(max_value);
+ EXPECT_EQ("300", max_value->str());
+ }
+
+ {
+ SCOPED_TRACE("default value too small");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("min-preferred-lifetime", data::Element::create(200));
+ // 100 < 200 so it will fail.
+ copied->set("preferred-lifetime", data::Element::create(100));
+ // Use a different (and greater than both) value for max.
+ copied->set("max-preferred-lifetime", data::Element::create(300));
+ Subnet6ConfigParser parser;
+ ASSERT_THROW(parser.parse(copied), DhcpConfigError);
+ }
+
+ {
+ SCOPED_TRACE("default value too large");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("min-preferred-lifetime", data::Element::create(100));
+ // Use a different (and greater) value for the default.
+ copied->set("preferred-lifetime", data::Element::create(300));
+ // 300 > 200 so it will fail.
+ copied->set("max-preferred-lifetime", data::Element::create(200));
+ Subnet6ConfigParser parser;
+ ASSERT_THROW(parser.parse(copied), DhcpConfigError);
+ }
+
+ {
+ SCOPED_TRACE("equal bounds are no longer ignored");
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("min-preferred-lifetime", data::Element::create(100));
+ copied->set("preferred-lifetime", data::Element::create(100));
+ copied->set("max-preferred-lifetime", data::Element::create(100));
+ Subnet6Ptr subnet;
+ Subnet6ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getPreferred().unspecified());
+ EXPECT_EQ(100, subnet->getPreferred());
+ EXPECT_EQ(100, subnet->getPreferred().getMin());
+ EXPECT_EQ(100, subnet->getPreferred().getMax());
+ data::ConstElementPtr repr = subnet->toElement();
+ data::ConstElementPtr value = repr->get("preferred-lifetime");
+ ASSERT_TRUE(value);
+ EXPECT_EQ("100", value->str());
+ data::ConstElementPtr min_value = repr->get("min-preferred-lifetime");
+ ASSERT_TRUE(min_value);
+ EXPECT_EQ("100", min_value->str());
+ data::ConstElementPtr max_value = repr->get("max-preferred-lifetime");
+ ASSERT_TRUE(max_value);
+ EXPECT_EQ("100", max_value->str());
+ }
+}
+
+// This test verifies the Subnet6 parser's validation logic for
+// hostname sanitizer values.
+TEST(CfgSubnets6Test, hostnameSanitizierValidation) {
+
+ // First we create a set of elements that provides all
+ // required for a Subnet6.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"2001:db8:1::/64\", \n"
+ " \"interface\": \"\", \n"
+ " \"renew-timer\": 100, \n"
+ " \"rebind-timer\": 200, \n"
+ " \"valid-lifetime\": 300, \n"
+ " \"client-class\": \"\", \n"
+ " \"require-client-classes\": [] \n,"
+ " \"reservations-global\": false, \n"
+ " \"reservations-in-subnet\": true, \n"
+ " \"reservations-out-of-pool\": false \n"
+ " }";
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ {
+ SCOPED_TRACE("invalid regular expression");
+
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("hostname-char-set", data::Element::create("^[A-"));
+ copied->set("hostname-char-replacement", data::Element::create("x"));
+ Subnet6Ptr subnet;
+ Subnet6ConfigParser parser;
+ EXPECT_THROW_MSG(subnet = parser.parse(copied), DhcpConfigError,
+ "subnet configuration failed: hostname-char-set "
+ "'^[A-' is not a valid regular expression");
+
+ }
+ {
+ SCOPED_TRACE("valid regular expression");
+
+ data::ElementPtr copied = data::copy(elems);
+ copied->set("hostname-char-set", data::Element::create("^[A-Z]"));
+ copied->set("hostname-char-replacement", data::Element::create("x"));
+ Subnet6Ptr subnet;
+ Subnet6ConfigParser parser;
+ ASSERT_NO_THROW(subnet = parser.parse(copied));
+ EXPECT_EQ("^[A-Z]", subnet->getHostnameCharSet().get());
+ EXPECT_EQ("x", subnet->getHostnameCharReplacement().get());
+ }
+}
+
+// This test verifies the Subnet6 parser's validation logic for
+// lease cache parameters.
+TEST(CfgSubnets6Test, cacheParamValidation) {
+
+ // Describes a single test scenario.
+ struct Scenario {
+ std::string label; // label used for logging test failures
+ double threshold; // value of cache-threshold
+ std::string error_message; // expected error message is parsing should fail
+ };
+
+ // Test Scenarios.
+ std::vector<Scenario> tests = {
+ {"valid", .25, ""},
+ {"negative", -.25,
+ "subnet configuration failed: cache-threshold:"
+ " -0.25 is invalid, it must be greater than 0.0 and less than 1.0"
+ },
+ {"too big", 1.05,
+ "subnet configuration failed: cache-threshold:"
+ " 1.05 is invalid, it must be greater than 0.0 and less than 1.0"
+ }
+ };
+
+ // First we create a set of elements that provides all
+ // required for a Subnet6.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"2001:db8:1::/64\", \n"
+ " \"interface\": \"\", \n"
+ " \"renew-timer\": 100, \n"
+ " \"rebind-timer\": 200, \n"
+ " \"valid-lifetime\": 300, \n"
+ " \"client-class\": \"\", \n"
+ " \"require-client-classes\": [] \n,"
+ " \"reservations-global\": false, \n"
+ " \"reservations-in-subnet\": true, \n"
+ " \"reservations-out-of-pool\": false \n"
+ " }";
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ // Iterate over the test scenarios, verifying each prescribed
+ // outcome.
+ for (auto test = tests.begin(); test != tests.end(); ++test) {
+ {
+ SCOPED_TRACE("test: " + (*test).label);
+
+ // Set this scenario's configuration parameters
+ elems->set("cache-threshold", data::Element::create((*test).threshold));
+
+ Subnet6Ptr subnet;
+ try {
+ // Attempt to parse the configuration.
+ Subnet6ConfigParser parser;
+ subnet = parser.parse(elems);
+ } catch (const std::exception& ex) {
+ if (!(*test).error_message.empty()) {
+ // We expected a failure, did we fail the correct way?
+ EXPECT_EQ((*test).error_message, ex.what());
+ } else {
+ // Should not have failed.
+ ADD_FAILURE() << "Scenario should not have failed: " << ex.what();
+ }
+
+ // Either way we're done with this scenario.
+ continue;
+ }
+
+ // We parsed correctly, make sure the values are right.
+ EXPECT_TRUE(util::areDoublesEquivalent((*test).threshold, subnet->getCacheThreshold()));
+ }
+ }
+}
+
+// This test verifies that the optional interface check works as expected.
+TEST(CfgSubnets6Test, iface) {
+ // Create a configuration.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"2001:db8:1::/64\", \n"
+ " \"interface\": \"eth1961\"\n"
+ " }";
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ // The interface check can be disabled.
+ Subnet6ConfigParser parser_no_check(false);
+ Subnet6Ptr subnet;
+ EXPECT_NO_THROW(subnet = parser_no_check.parse(elems));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getIface().unspecified());
+ EXPECT_EQ("eth1961", subnet->getIface().get());
+
+ // Retry with the interface check enabled.
+ Subnet6ConfigParser parser;
+ EXPECT_THROW(parser.parse(elems), DhcpConfigError);
+
+ // Configure default test interfaces.
+ IfaceMgrTestConfig config(true);
+
+ EXPECT_NO_THROW(subnet = parser_no_check.parse(elems));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getIface().unspecified());
+ EXPECT_EQ("eth1961", subnet->getIface().get());
+
+ EXPECT_NO_THROW(subnet = parser.parse(elems));
+ ASSERT_TRUE(subnet);
+ EXPECT_FALSE(subnet->getIface().unspecified());
+ EXPECT_EQ("eth1961", subnet->getIface().get());
+}
+
+// This test verifies that update statistics works as expected.
+TEST(CfgSubnets6Test, updateStatistics) {
+ CfgMgr::instance().clear();
+
+ CfgSubnets6Ptr cfg = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6();
+ ObservationPtr observation;
+ SubnetID subnet_id = 100;
+
+ LeaseMgrFactory::create("type=memfile universe=6 persist=false");
+
+ // remove all statistics
+ StatsMgr::instance().removeAll();
+
+ // Create subnet.
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4, 100));
+
+ // Add subnet.
+ cfg->add(subnet);
+
+ observation = StatsMgr::instance().getObservation(
+ "cumulative-assigned-nas");
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ "cumulative-assigned-pds");
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ "declined-addresses");
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ "reclaimed-declined-addresses");
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ "reclaimed-leases");
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-nas"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-pds"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-nas"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-pds"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-nas"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-pds"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "declined-addresses"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-declined-addresses"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-leases"));
+ ASSERT_FALSE(observation);
+
+ cfg->updateStatistics();
+
+ observation = StatsMgr::instance().getObservation(
+ "cumulative-assigned-nas");
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ "cumulative-assigned-pds");
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ "declined-addresses");
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ "reclaimed-declined-addresses");
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ "reclaimed-leases");
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-nas"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-pds"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-nas"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-pds"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-nas"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-pds"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "declined-addresses"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-declined-addresses"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-leases"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+}
+
+// This test verifies that remove statistics works as expected.
+TEST(CfgSubnets6Test, removeStatistics) {
+ CfgSubnets6 cfg;
+ ObservationPtr observation;
+ SubnetID subnet_id = 100;
+
+ // remove all statistics and then set them all to 0
+ StatsMgr::instance().removeAll();
+
+ // Create subnet.
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4, 100));
+
+ // Add subnet.
+ cfg.add(subnet);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-nas"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-nas"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-pds"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-pds"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-nas"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-nas"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-pds"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-pds"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-nas"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-nas"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-pds"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-pds"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "declined-addresses"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "declined-addresses"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-declined-addresses"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-declined-addresses"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ StatsMgr::instance().setValue(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-leases"),
+ int64_t(0));
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-leases"));
+ ASSERT_TRUE(observation);
+ ASSERT_EQ(0, observation->getInteger().first);
+
+ // remove all statistics
+ cfg.removeStatistics();
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-nas"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "total-pds"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-nas"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "assigned-pds"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-nas"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "cumulative-assigned-pds"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "declined-addresses"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-declined-addresses"));
+ ASSERT_FALSE(observation);
+
+ observation = StatsMgr::instance().getObservation(
+ StatsMgr::generateName("subnet", subnet_id,
+ "reclaimed-leases"));
+ ASSERT_FALSE(observation);
+}
+
+// This test verifies that in range host reservation works as expected.
+TEST(CfgSubnets6Test, hostNA) {
+ // Create a configuration.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"2001:db8:1::/48\", \n"
+ " \"reservations\": [ {\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\", \n"
+ " \"ip-addresses\": [\"2001:db8:1::1\"] } ]\n"
+ " }";
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ Subnet6ConfigParser parser;
+ Subnet6Ptr subnet;
+ EXPECT_NO_THROW(subnet = parser.parse(elems));
+ ASSERT_TRUE(subnet);
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_TRUE(cfg_hosts);
+ HostCollection hosts = cfg_hosts->getAll6(SubnetID(1));
+ ASSERT_EQ(1, hosts.size());
+ ConstHostPtr host = hosts[0];
+ ASSERT_TRUE(host);
+ EXPECT_EQ(1, host->getIPv6SubnetID());
+ EXPECT_EQ("hwaddr=AABBCCDDEEFF", host->getIdentifierAsText());
+ IPv6ResrvRange addresses = host->getIPv6Reservations(IPv6Resrv::TYPE_NA);
+ ASSERT_EQ(1, std::distance(addresses.first, addresses.second));
+ EXPECT_EQ("2001:db8:1::1", addresses.first->second.getPrefix().toText());
+
+ CfgMgr::instance().clear();
+}
+
+// This test verifies that an out of range host reservation is rejected.
+TEST(CfgSubnets6Test, outOfRangeHost) {
+ // Create a configuration.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"2001:db8:1::/48\", \n"
+ " \"reservations\": [ {\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\", \n"
+ " \"ip-addresses\": [\"2001:db8:1::1\", \n"
+ " \"2001:db8:2::1\"] } ]\n"
+ " }";
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ Subnet6ConfigParser parser;
+ std::string msg = "specified reservation '2001:db8:2::1' is ";
+ msg += "not within the IPv6 subnet '2001:db8:1::/48'";
+ EXPECT_THROW_MSG(parser.parse(elems), DhcpConfigError, msg);
+}
+
+// This test verifies that in range validation does not applies to prefixes.
+TEST(CfgSubnets6Test, hostPD) {
+ // Create a configuration.
+ std::string json =
+ " {"
+ " \"id\": 1,\n"
+ " \"subnet\": \"2001:db8:1::/48\", \n"
+ " \"reservations\": [ {\n"
+ " \"hw-address\": \"aa:bb:cc:dd:ee:ff\", \n"
+ " \"prefixes\": [\"2001:db8:2::/64\"] } ]\n"
+ " }";
+
+ data::ElementPtr elems;
+ ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
+ << "invalid JSON:" << json << "\n test is broken";
+
+ Subnet6ConfigParser parser;
+ Subnet6Ptr subnet;
+ try {
+ subnet = parser.parse(elems);
+ } catch (const std::exception& ex) {
+ std::cerr << "parse fail with " << ex.what() << std::endl;
+ }
+ //EXPECT_NO_THROW(subnet = parser.parse(elems));
+ ASSERT_TRUE(subnet);
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_TRUE(cfg_hosts);
+ HostCollection hosts = cfg_hosts->getAll6(SubnetID(1));
+ ASSERT_EQ(1, hosts.size());
+ ConstHostPtr host = hosts[0];
+ ASSERT_TRUE(host);
+ EXPECT_EQ(1, host->getIPv6SubnetID());
+ EXPECT_EQ("hwaddr=AABBCCDDEEFF", host->getIdentifierAsText());
+ IPv6ResrvRange prefixes = host->getIPv6Reservations(IPv6Resrv::TYPE_PD);
+ ASSERT_EQ(1, std::distance(prefixes.first, prefixes.second));
+ EXPECT_EQ("2001:db8:2::", prefixes.first->second.getPrefix().toText());
+ EXPECT_EQ(64, prefixes.first->second.getPrefixLen());
+
+ // Verify the prefix is not in the subnet range.
+ EXPECT_FALSE(subnet->inRange(prefixes.first->second.getPrefix()));
+
+ CfgMgr::instance().clear();
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/cfgmgr_unittest.cc b/src/lib/dhcpsrv/tests/cfgmgr_unittest.cc
new file mode 100644
index 0000000..d1b5413
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/cfgmgr_unittest.cc
@@ -0,0 +1,1066 @@
+// Copyright (C) 2012-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <exceptions/exceptions.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/subnet_id.h>
+#include <dhcpsrv/parsers/dhcp_parsers.h>
+#include <process/logging_info.h>
+#include <stats/stats_mgr.h>
+#include <util/chrono_time_utils.h>
+
+#include <boost/scoped_ptr.hpp>
+
+#include <gtest/gtest.h>
+
+#include <iostream>
+#include <sstream>
+
+#include <arpa/inet.h>
+
+using namespace std;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::util;
+using namespace isc::stats;
+using namespace isc::process;
+using namespace isc;
+
+// don't import the entire boost namespace. It will unexpectedly hide uint8_t
+// for some systems.
+using boost::scoped_ptr;
+
+namespace {
+
+template <typename Storage>
+bool isZeroPosition(const Storage& storage, const std::string& param_name) {
+ Element::Position position = storage.getPosition(param_name);
+ return ((position.line_ == 0) && (position.pos_ == 0) &&
+ (position.file_.empty()));
+}
+
+// This test verifies that BooleanStorage functions properly.
+TEST(ValueStorageTest, BooleanTesting) {
+ BooleanStorage testStore;
+
+ // Verify that we can add and retrieve parameters.
+ testStore.setParam("firstBool", false, Element::Position("kea.conf", 123, 234));
+ testStore.setParam("secondBool", true, Element::Position("keax.conf", 10, 20));
+
+ EXPECT_FALSE(testStore.getParam("firstBool"));
+ EXPECT_TRUE(testStore.getParam("secondBool"));
+
+ EXPECT_EQ(123, testStore.getPosition("firstBool").line_);
+ EXPECT_EQ(234, testStore.getPosition("firstBool").pos_);
+ EXPECT_EQ("kea.conf", testStore.getPosition("firstBool").file_);
+
+ EXPECT_EQ(10, testStore.getPosition("secondBool").line_);
+ EXPECT_EQ(20, testStore.getPosition("secondBool").pos_);
+ EXPECT_EQ("keax.conf", testStore.getPosition("secondBool").file_);
+
+ // Verify that we can update parameters.
+ testStore.setParam("firstBool", true, Element::Position("keax.conf", 555, 111));
+ testStore.setParam("secondBool", false, Element::Position("kea.conf", 1, 3));
+
+ EXPECT_TRUE(testStore.getParam("firstBool"));
+ EXPECT_FALSE(testStore.getParam("secondBool"));
+
+ EXPECT_EQ(555, testStore.getPosition("firstBool").line_);
+ EXPECT_EQ(111, testStore.getPosition("firstBool").pos_);
+ EXPECT_EQ("keax.conf", testStore.getPosition("firstBool").file_);
+
+ EXPECT_EQ(1, testStore.getPosition("secondBool").line_);
+ EXPECT_EQ(3, testStore.getPosition("secondBool").pos_);
+ EXPECT_EQ("kea.conf", testStore.getPosition("secondBool").file_);
+
+ // Verify that we can delete a parameter and it will no longer be found.
+ testStore.delParam("firstBool");
+ EXPECT_THROW(testStore.getParam("firstBool"), isc::dhcp::DhcpConfigError);
+
+ // Verify that the "zero" position is returned when parameter doesn't exist.
+ EXPECT_TRUE(isZeroPosition(testStore, "firstBool"));
+
+ // Verify that the delete was safe and the store still operates.
+ EXPECT_FALSE(testStore.getParam("secondBool"));
+
+ EXPECT_EQ(1, testStore.getPosition("secondBool").line_);
+ EXPECT_EQ(3, testStore.getPosition("secondBool").pos_);
+ EXPECT_EQ("kea.conf", testStore.getPosition("secondBool").file_);
+
+ // Verify that looking for a parameter that never existed throws.
+ ASSERT_THROW(testStore.getParam("bogusBool"), isc::dhcp::DhcpConfigError);
+
+ // Verify that the "zero" position is returned when parameter doesn't exist.
+ EXPECT_TRUE(isZeroPosition(testStore, "bogusBool"));
+
+ // Verify that attempting to delete a parameter that never existed does not throw.
+ EXPECT_NO_THROW(testStore.delParam("bogusBool"));
+
+ // Verify that we can empty the list.
+ testStore.clear();
+ EXPECT_THROW(testStore.getParam("secondBool"), isc::dhcp::DhcpConfigError);
+
+ // Verify that the "zero" position is returned when parameter doesn't exist.
+ EXPECT_TRUE(isZeroPosition(testStore, "secondBool"));
+}
+
+// This test verifies that Uint32Storage functions properly.
+TEST(ValueStorageTest, Uint32Testing) {
+ Uint32Storage testStore;
+
+ uint32_t int_one = 77;
+ uint32_t int_two = 33;
+
+ // Verify that we can add and retrieve parameters.
+ testStore.setParam("firstInt", int_one, Element::Position("kea.conf", 123, 234));
+ testStore.setParam("secondInt", int_two, Element::Position("keax.conf", 10, 20));
+
+ EXPECT_EQ(testStore.getParam("firstInt"), int_one);
+ EXPECT_EQ(testStore.getParam("secondInt"), int_two);
+
+ EXPECT_EQ(123, testStore.getPosition("firstInt").line_);
+ EXPECT_EQ(234, testStore.getPosition("firstInt").pos_);
+ EXPECT_EQ("kea.conf", testStore.getPosition("firstInt").file_);
+
+ EXPECT_EQ(10, testStore.getPosition("secondInt").line_);
+ EXPECT_EQ(20, testStore.getPosition("secondInt").pos_);
+ EXPECT_EQ("keax.conf", testStore.getPosition("secondInt").file_);
+
+ // Verify that we can update parameters.
+ testStore.setParam("firstInt", --int_one, Element::Position("keax.conf", 555, 111));
+ testStore.setParam("secondInt", ++int_two, Element::Position("kea.conf", 1, 3));
+
+ EXPECT_EQ(testStore.getParam("firstInt"), int_one);
+ EXPECT_EQ(testStore.getParam("secondInt"), int_two);
+
+ EXPECT_EQ(555, testStore.getPosition("firstInt").line_);
+ EXPECT_EQ(111, testStore.getPosition("firstInt").pos_);
+ EXPECT_EQ("keax.conf", testStore.getPosition("firstInt").file_);
+
+ EXPECT_EQ(1, testStore.getPosition("secondInt").line_);
+ EXPECT_EQ(3, testStore.getPosition("secondInt").pos_);
+ EXPECT_EQ("kea.conf", testStore.getPosition("secondInt").file_);
+
+ // Verify that we can delete a parameter and it will no longer be found.
+ testStore.delParam("firstInt");
+ EXPECT_THROW(testStore.getParam("firstInt"), isc::dhcp::DhcpConfigError);
+
+ // Verify that the "zero" position is returned when parameter doesn't exist.
+ EXPECT_TRUE(isZeroPosition(testStore, "firstInt"));
+
+ // Verify that the delete was safe and the store still operates.
+ EXPECT_EQ(testStore.getParam("secondInt"), int_two);
+
+ EXPECT_EQ(1, testStore.getPosition("secondInt").line_);
+ EXPECT_EQ(3, testStore.getPosition("secondInt").pos_);
+ EXPECT_EQ("kea.conf", testStore.getPosition("secondInt").file_);
+
+ // Verify that looking for a parameter that never existed throws.
+ ASSERT_THROW(testStore.getParam("bogusInt"), isc::dhcp::DhcpConfigError);
+
+ // Verify that attempting to delete a parameter that never existed does not throw.
+ EXPECT_NO_THROW(testStore.delParam("bogusInt"));
+
+ // Verify that the "zero" position is returned when parameter doesn't exist.
+ EXPECT_TRUE(isZeroPosition(testStore, "bogusInt"));
+
+ // Verify that we can empty the list.
+ testStore.clear();
+ EXPECT_THROW(testStore.getParam("secondInt"), isc::dhcp::DhcpConfigError);
+
+ // Verify that the "zero" position is returned when parameter doesn't exist.
+ EXPECT_TRUE(isZeroPosition(testStore, "secondInt"));
+}
+
+// This test verifies that StringStorage functions properly.
+TEST(ValueStorageTest, StringTesting) {
+ StringStorage testStore;
+
+ std::string string_one = "seventy-seven";
+ std::string string_two = "thirty-three";
+
+ // Verify that we can add and retrieve parameters.
+ testStore.setParam("firstString", string_one,
+ Element::Position("kea.conf", 123, 234));
+ testStore.setParam("secondString", string_two,
+ Element::Position("keax.conf", 10, 20));
+
+ EXPECT_EQ(testStore.getParam("firstString"), string_one);
+ EXPECT_EQ(testStore.getParam("secondString"), string_two);
+
+ EXPECT_EQ(123, testStore.getPosition("firstString").line_);
+ EXPECT_EQ(234, testStore.getPosition("firstString").pos_);
+ EXPECT_EQ("kea.conf", testStore.getPosition("firstString").file_);
+
+ EXPECT_EQ(10, testStore.getPosition("secondString").line_);
+ EXPECT_EQ(20, testStore.getPosition("secondString").pos_);
+ EXPECT_EQ("keax.conf", testStore.getPosition("secondString").file_);
+
+ // Verify that we can update parameters.
+ string_one.append("-boo");
+ string_two.append("-boo");
+
+ testStore.setParam("firstString", string_one,
+ Element::Position("kea.conf", 555, 111));
+ testStore.setParam("secondString", string_two,
+ Element::Position("keax.conf", 1, 3));
+
+ EXPECT_EQ(testStore.getParam("firstString"), string_one);
+ EXPECT_EQ(testStore.getParam("secondString"), string_two);
+
+ EXPECT_EQ(555, testStore.getPosition("firstString").line_);
+ EXPECT_EQ(111, testStore.getPosition("firstString").pos_);
+ EXPECT_EQ("kea.conf", testStore.getPosition("firstString").file_);
+
+ EXPECT_EQ(1, testStore.getPosition("secondString").line_);
+ EXPECT_EQ(3, testStore.getPosition("secondString").pos_);
+ EXPECT_EQ("keax.conf", testStore.getPosition("secondString").file_);
+
+ // Verify that we can delete a parameter and it will no longer be found.
+ testStore.delParam("firstString");
+ EXPECT_THROW(testStore.getParam("firstString"), isc::dhcp::DhcpConfigError);
+
+ // Verify that the "zero" position is returned when parameter doesn't exist.
+ EXPECT_TRUE(isZeroPosition(testStore, "firstString"));
+
+ // Verify that the delete was safe and the store still operates.
+ EXPECT_EQ(testStore.getParam("secondString"), string_two);
+
+ EXPECT_EQ(1, testStore.getPosition("secondString").line_);
+ EXPECT_EQ(3, testStore.getPosition("secondString").pos_);
+ EXPECT_EQ("keax.conf", testStore.getPosition("secondString").file_);
+
+ // Verify that looking for a parameter that never existed throws.
+ ASSERT_THROW(testStore.getParam("bogusString"), isc::dhcp::DhcpConfigError);
+
+ // Verify that attempting to delete a parameter that never existed does not throw.
+ EXPECT_NO_THROW(testStore.delParam("bogusString"));
+
+ // Verify that the "zero" position is returned when parameter doesn't exist.
+ EXPECT_TRUE(isZeroPosition(testStore, "bogusString"));
+
+ // Verify that we can empty the list.
+ testStore.clear();
+ EXPECT_THROW(testStore.getParam("secondString"), isc::dhcp::DhcpConfigError);
+
+ // Verify that the "zero" position is returned when parameter doesn't exist.
+ EXPECT_TRUE(isZeroPosition(testStore, "secondString"));
+}
+
+
+
+class CfgMgrTest : public ::testing::Test {
+public:
+ CfgMgrTest() {
+ // make sure we start with a clean configuration
+ original_datadir_ = CfgMgr::instance().getDataDir();
+ clear();
+ }
+
+ /// @brief generates interface-id option based on provided text
+ ///
+ /// @param text content of the option to be created
+ ///
+ /// @return pointer to the option object created
+ OptionPtr generateInterfaceId(const string& text) {
+ OptionBuffer buffer(text.begin(), text.end());
+ return OptionPtr(new Option(Option::V6, D6O_INTERFACE_ID, buffer));
+ }
+
+ ~CfgMgrTest() {
+ // clean up after the test
+ clear();
+ }
+
+ void clear() {
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgMgr::instance().setDataDir(original_datadir_);
+ CfgMgr::instance().clear();
+ LeaseMgrFactory::destroy();
+ }
+
+ /// @brief Creates instance of the backend.
+ ///
+ /// @param family AF_INET for v4, AF_INET6 for v6
+ void startBackend(int family = AF_INET) {
+ try {
+ std::ostringstream s;
+ s << "type=memfile persist=false " << (family == AF_INET6 ?
+ "universe=6" : "universe=4");
+ LeaseMgrFactory::create(s.str());
+ } catch (const std::exception& ex) {
+ std::cerr << "*** ERROR: unable to create instance of the Memfile"
+ " lease database backend: " << ex.what() << std::endl;
+ throw;
+ }
+ }
+
+ /// used in client classification (or just empty container for other tests)
+ isc::dhcp::ClientClasses classify_;
+
+private:
+ /// to restore it in destructor.
+ string original_datadir_;
+};
+
+// Checks that there is a configuration structure available and that
+// it is empty by default.
+TEST_F(CfgMgrTest, configuration) {
+
+ ConstSrvConfigPtr configuration = CfgMgr::instance().getCurrentCfg();
+ ASSERT_TRUE(configuration);
+ EXPECT_TRUE(configuration->getLoggingInfo().empty());
+
+ configuration = CfgMgr::instance().getStagingCfg();
+ ASSERT_TRUE(configuration);
+ EXPECT_TRUE(configuration->getLoggingInfo().empty());
+}
+
+// This test checks the data directory handling.
+TEST_F(CfgMgrTest, dataDir) {
+ // It is only in DHCPv6 syntax so switch to IPv6.
+ CfgMgr::instance().setFamily(AF_INET6);
+
+ // Default.
+ EXPECT_TRUE(CfgMgr::instance().getDataDir().unspecified());
+ ConstElementPtr json = CfgMgr::instance().getCurrentCfg()->toElement();
+ ASSERT_TRUE(json);
+ ASSERT_EQ(Element::map, json->getType());
+ ConstElementPtr dhcp = json->get("Dhcp6");
+ ASSERT_TRUE(dhcp);
+ ASSERT_EQ(Element::map, dhcp->getType());
+ ConstElementPtr datadir = dhcp->get("data-directory");
+ EXPECT_FALSE(datadir);
+
+ // Set but not specified.
+ CfgMgr::instance().setDataDir("/tmp");
+ EXPECT_TRUE(CfgMgr::instance().getDataDir().unspecified());
+ EXPECT_EQ("/tmp", string(CfgMgr::instance().getDataDir()));
+ json = CfgMgr::instance().getCurrentCfg()->toElement();
+ ASSERT_TRUE(json);
+ ASSERT_EQ(Element::map, json->getType());
+ dhcp = json->get("Dhcp6");
+ ASSERT_TRUE(dhcp);
+ ASSERT_EQ(Element::map, dhcp->getType());
+ datadir = dhcp->get("data-directory");
+ EXPECT_FALSE(datadir);
+
+ // Set and specified.
+ CfgMgr::instance().setDataDir("/tmp", false);
+ EXPECT_FALSE(CfgMgr::instance().getDataDir().unspecified());
+ EXPECT_EQ("/tmp", string(CfgMgr::instance().getDataDir()));
+ json = CfgMgr::instance().getCurrentCfg()->toElement();
+ ASSERT_TRUE(json);
+ ASSERT_EQ(Element::map, json->getType());
+ dhcp = json->get("Dhcp6");
+ ASSERT_TRUE(dhcp);
+ ASSERT_EQ(Element::map, dhcp->getType());
+ datadir = dhcp->get("data-directory");
+ ASSERT_TRUE(datadir);
+ ASSERT_EQ(Element::string, datadir->getType());
+ EXPECT_EQ("/tmp", datadir->stringValue());
+
+ // Still IPv6 only.
+ CfgMgr::instance().setFamily(AF_INET);
+ EXPECT_FALSE(CfgMgr::instance().getDataDir().unspecified());
+ EXPECT_EQ("/tmp", string(CfgMgr::instance().getDataDir()));
+ json = CfgMgr::instance().getCurrentCfg()->toElement();
+ ASSERT_TRUE(json);
+ ASSERT_EQ(Element::map, json->getType());
+ dhcp = json->get("Dhcp4");
+ ASSERT_TRUE(dhcp);
+ ASSERT_EQ(Element::map, dhcp->getType());
+ datadir = dhcp->get("data-directory");
+ EXPECT_FALSE(datadir);
+}
+
+// This test checks the D2ClientMgr wrapper methods.
+TEST_F(CfgMgrTest, d2ClientConfig) {
+ // After CfgMgr construction, D2ClientMgr member should be initialized
+ // with a D2 configuration that is disabled.
+ // Verify we can Fetch the mgr.
+ D2ClientMgr& d2_mgr = CfgMgr::instance().getD2ClientMgr();
+ EXPECT_FALSE(d2_mgr.ddnsEnabled());
+
+ // Make sure the convenience method fetches the config correctly.
+ D2ClientConfigPtr original_config = CfgMgr::instance().getD2ClientConfig();
+ ASSERT_TRUE(original_config);
+ EXPECT_FALSE(original_config->getEnableUpdates());
+
+ // Verify that we cannot set the configuration to an empty pointer.
+ D2ClientConfigPtr new_cfg;
+ ASSERT_THROW(CfgMgr::instance().setD2ClientConfig(new_cfg), D2ClientError);
+
+ // Create a new, enabled configuration.
+ ASSERT_NO_THROW(new_cfg.reset(new D2ClientConfig(true,
+ isc::asiolink::IOAddress("127.0.0.1"), 477,
+ isc::asiolink::IOAddress("127.0.0.1"), 478,
+ 1024,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON)));
+
+ // Verify that we can assign a new, non-empty configuration.
+ ASSERT_NO_THROW(CfgMgr::instance().setD2ClientConfig(new_cfg));
+
+ // Verify that we can fetch the newly assigned configuration.
+ D2ClientConfigPtr updated_config = CfgMgr::instance().getD2ClientConfig();
+ ASSERT_TRUE(updated_config);
+ EXPECT_TRUE(updated_config->getEnableUpdates());
+
+ // Make sure convenience method agrees with updated configuration.
+ EXPECT_TRUE(CfgMgr::instance().ddnsEnabled());
+
+ // Make sure the configuration we fetched is the one we assigned,
+ // and not the original configuration.
+ EXPECT_EQ(*new_cfg, *updated_config);
+ EXPECT_NE(*original_config, *updated_config);
+
+ // Revert to default configuration.
+ ASSERT_NO_THROW(CfgMgr::instance().setD2ClientConfig(original_config));
+}
+
+// This test verifies that the configuration staging, commit and rollback works
+// as expected.
+TEST_F(CfgMgrTest, staging) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ // Initially, the current configuration is a default one. We are going
+ // to get the current configuration a couple of times and make sure
+ // that always the same instance is returned.
+ ConstSrvConfigPtr const_config;
+ for (int i = 0; i < 5; ++i) {
+ const_config = cfg_mgr.getCurrentCfg();
+ ASSERT_TRUE(const_config) << "Returned NULL current configuration"
+ " for iteration " << i;
+ EXPECT_EQ(0, const_config->getSequence())
+ << "Returned invalid sequence number "
+ << const_config->getSequence() << " for iteration " << i;
+ }
+
+ // Try to get the new staging configuration. When getStagingCfg() is called
+ // for the first time the new instance of the staging configuration is
+ // returned. This instance is returned for every call to getStagingCfg()
+ // until commit is called.
+ SrvConfigPtr config;
+ for (int i = 0; i < 5; ++i) {
+ config = cfg_mgr.getStagingCfg();
+ ASSERT_TRUE(config) << "Returned NULL staging configuration for"
+ " iteration " << i;
+ // The sequence id is 1 for staging because it is ahead of current
+ // configuration having sequence number 0.
+ EXPECT_EQ(1, config->getSequence()) << "Returned invalid sequence"
+ " number " << config->getSequence() << " for iteration " << i;
+ }
+
+ // This should change the staging configuration so as it becomes a current
+ // one.
+ auto before = boost::posix_time::second_clock::universal_time();
+ cfg_mgr.commit();
+ auto after = boost::posix_time::second_clock::universal_time();
+ const_config = cfg_mgr.getCurrentCfg();
+ ASSERT_TRUE(const_config);
+ // Sequence id equal to 1 indicates that the current configuration points
+ // to the configuration that used to be a staging configuration previously.
+ EXPECT_EQ(1, const_config->getSequence());
+ // Last commit timestamp should be between before and after.
+ auto reload = const_config->getLastCommitTime();
+ ASSERT_FALSE(reload.is_not_a_date_time());
+ EXPECT_LE(before, reload);
+ EXPECT_GE(after, reload);
+
+ // Create a new staging configuration. It should be assigned a new
+ // sequence id.
+ config = cfg_mgr.getStagingCfg();
+ ASSERT_TRUE(config);
+ EXPECT_EQ(2, config->getSequence());
+
+ // Let's execute commit a couple of times. The first invocation to commit
+ // changes the configuration having sequence 2 to current configuration.
+ // Other commits are no-op.
+ for (int i = 0; i < 5; ++i) {
+ cfg_mgr.commit();
+ }
+
+ // The current configuration now have sequence number 2.
+ const_config = cfg_mgr.getCurrentCfg();
+ ASSERT_TRUE(const_config);
+ EXPECT_EQ(2, const_config->getSequence());
+
+ // Clear configuration along with a history.
+ cfg_mgr.clear();
+
+ // After clearing configuration we should successfully get the
+ // new staging configuration.
+ config = cfg_mgr.getStagingCfg();
+ ASSERT_TRUE(config);
+ EXPECT_EQ(1, config->getSequence());
+
+ // Modify the staging configuration.
+ config->addLoggingInfo(LoggingInfo());
+ ASSERT_TRUE(config);
+ // The modified staging configuration should have one logger configured.
+ ASSERT_EQ(1, config->getLoggingInfo().size());
+
+ // Rollback should remove a staging configuration, including the logger.
+ ASSERT_NO_THROW(cfg_mgr.rollback());
+
+ // Make sure that the logger is not set. This is an indication that the
+ // rollback worked.
+ config = cfg_mgr.getStagingCfg();
+ ASSERT_TRUE(config);
+ EXPECT_EQ(0, config->getLoggingInfo().size());
+}
+
+// This test verifies that it is possible to revert to an old configuration.
+TEST_F(CfgMgrTest, revert) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ // Let's create 5 unique configurations: differing by a debug level in the
+ // range of 10 to 14.
+ for (int i = 0; i < 5; ++i) {
+ SrvConfigPtr config = cfg_mgr.getStagingCfg();
+ LoggingInfo logging_info;
+ logging_info.debuglevel_ = i + 10;
+ config->addLoggingInfo(logging_info);
+ cfg_mgr.commit();
+ }
+
+ // Now we have 6 configurations with:
+ // - debuglevel = 99 (a default one)
+ // - debuglevel = 10
+ // - debuglevel = 11
+ // - debuglevel = 12
+ // - debuglevel = 13
+ // - debuglevel = 14 (current)
+
+ // Hence, the maximum index of the configuration to revert is 5 (which
+ // points to the configuration with debuglevel = 99). For the index greater
+ // than 5 we should get an exception.
+ ASSERT_THROW(cfg_mgr.revert(6), isc::OutOfRange);
+ // Value of 0 also doesn't make sense.
+ ASSERT_THROW(cfg_mgr.revert(0), isc::OutOfRange);
+
+ // We should be able to revert to configuration with debuglevel = 10.
+ ASSERT_NO_THROW(cfg_mgr.revert(4));
+ // And this configuration should be now the current one and the debuglevel
+ // of this configuration is 10.
+ EXPECT_EQ(10, cfg_mgr.getCurrentCfg()->getLoggingInfo()[0].debuglevel_);
+ EXPECT_NE(cfg_mgr.getCurrentCfg()->getSequence(), 1);
+
+ // The new set of configuration is now as follows:
+ // - debuglevel = 99
+ // - debuglevel = 10
+ // - debuglevel = 11
+ // - debuglevel = 12
+ // - debuglevel = 13
+ // - debuglevel = 14
+ // - debuglevel = 10 (current)
+ // So, reverting to configuration having index 3 means that the debug level
+ // of the current configuration will become 12.
+ ASSERT_NO_THROW(cfg_mgr.revert(3));
+ EXPECT_EQ(12, cfg_mgr.getCurrentCfg()->getLoggingInfo()[0].debuglevel_);
+}
+
+// This test verifies that the address family can be set and obtained
+// from the configuration manager.
+TEST_F(CfgMgrTest, family) {
+ ASSERT_EQ(AF_INET, CfgMgr::instance().getFamily());
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ ASSERT_EQ(AF_INET6, CfgMgr::instance().getFamily());
+
+ CfgMgr::instance().setFamily(AF_INET);
+ EXPECT_EQ(AF_INET, CfgMgr::instance().getFamily());
+}
+
+// This test verifies that once the configuration is committed, statistics
+// are updated appropriately.
+TEST_F(CfgMgrTest, commitStats4) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ StatsMgr& stats_mgr = StatsMgr::instance();
+ startBackend(AF_INET);
+
+ // Let's prepare the "old" configuration: a subnet with id 123
+ // and pretend there were addresses assigned, so statistics are non-zero.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.1.2.0"), 24, 1, 2, 3, 123));
+ CfgSubnets4Ptr subnets = cfg_mgr.getStagingCfg()->getCfgSubnets4();
+ subnets->add(subnet1);
+ cfg_mgr.commit();
+ stats_mgr.addValue("subnet[123].total-addresses", static_cast<int64_t>(256));
+ stats_mgr.setValue("subnet[123].assigned-addresses", static_cast<int64_t>(150));
+
+ // Now, let's change the configuration to something new.
+
+ // There's a subnet 192.1.2.0/24 with ID=42
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.1.2.0"), 24, 1, 2, 3, 42));
+
+ // Let's make a pool with 128 addresses available.
+ PoolPtr pool(new Pool4(IOAddress("192.1.2.0"), 25)); // 128 addrs
+ subnet2->addPool(pool);
+
+ subnets = cfg_mgr.getStagingCfg()->getCfgSubnets4();
+ subnets->add(subnet2);
+
+ // Change the stats default limits.
+ cfg_mgr.getStagingCfg()->addConfiguredGlobal("statistic-default-sample-count",
+ Element::create(15));
+ cfg_mgr.getStagingCfg()->addConfiguredGlobal("statistic-default-sample-age",
+ Element::create(2));
+
+ // Let's commit it
+ cfg_mgr.commit();
+
+ EXPECT_EQ(15, stats_mgr.getMaxSampleCountDefault());
+ EXPECT_EQ("00:00:02", durationToText(stats_mgr.getMaxSampleAgeDefault(), 0));
+
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[123].total-addresses"));
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[123].assigned-addresses"));
+
+ ObservationPtr total_addrs;
+ EXPECT_NO_THROW(total_addrs = stats_mgr.getObservation("subnet[42].total-addresses"));
+ ASSERT_TRUE(total_addrs);
+ EXPECT_EQ(128, total_addrs->getInteger().first);
+ EXPECT_TRUE(total_addrs->getMaxSampleCount().first);
+ EXPECT_EQ(15, total_addrs->getMaxSampleCount().second);
+ EXPECT_FALSE(total_addrs->getMaxSampleAge().first);
+ EXPECT_EQ("00:00:02", durationToText(total_addrs->getMaxSampleAge().second, 0));
+}
+
+// This test verifies that once the configuration is merged into the current
+// configuration, statistics are updated appropriately.
+TEST_F(CfgMgrTest, mergeIntoCurrentStats4) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ StatsMgr& stats_mgr = StatsMgr::instance();
+ startBackend(AF_INET);
+
+ // Let's prepare the "old" configuration: a subnet with id 123
+ // and pretend there were addresses assigned, so statistics are non-zero.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.1.2.0"), 24, 1, 2, 3, 123));
+ CfgSubnets4Ptr subnets = cfg_mgr.getStagingCfg()->getCfgSubnets4();
+ subnets->add(subnet1);
+ cfg_mgr.commit();
+ stats_mgr.addValue("subnet[123].total-addresses", static_cast<int64_t>(256));
+ stats_mgr.setValue("subnet[123].assigned-addresses", static_cast<int64_t>(150));
+
+ // There should be no stats for subnet 42 at this point.
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[42].total-addresses"));
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[42].assigned-addresses"));
+
+ // Now, let's create new configuration with updates.
+
+ // There's a subnet 192.1.3.0/24 with ID=42
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.1.3.0"), 24, 1, 2, 3, 42));
+
+ // Let's make a pool with 128 addresses available.
+ PoolPtr pool(new Pool4(IOAddress("192.1.3.0"), 25)); // 128 addrs
+ subnet2->addPool(pool);
+
+ // Create external configuration to be merged into current one.
+ auto external_cfg = CfgMgr::instance().createExternalCfg();
+ subnets = external_cfg->getCfgSubnets4();
+ subnets->add(subnet2);
+
+ external_cfg->addConfiguredGlobal("statistic-default-sample-count",
+ Element::create(16));
+ external_cfg->addConfiguredGlobal("statistic-default-sample-age",
+ Element::create(3));
+
+ // Let's merge it.
+ cfg_mgr.mergeIntoCurrentCfg(external_cfg->getSequence());
+
+ // The stats should have been updated and so we should be able to get
+ // observations for subnet 42.
+ EXPECT_EQ(16, stats_mgr.getMaxSampleCountDefault());
+ EXPECT_EQ("00:00:03", durationToText(stats_mgr.getMaxSampleAgeDefault(), 0));
+
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[42].total-addresses"));
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[42].assigned-addresses"));
+
+ // And also for 123
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[123].total-addresses"));
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[123].assigned-addresses"));
+
+ ObservationPtr total_addrs;
+ EXPECT_NO_THROW(total_addrs = stats_mgr.getObservation("subnet[42].total-addresses"));
+ ASSERT_TRUE(total_addrs);
+ EXPECT_EQ(128, total_addrs->getInteger().first);
+}
+
+// This test verifies that once the configuration is cleared, the statistics
+// are removed.
+TEST_F(CfgMgrTest, clearStats4) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ StatsMgr& stats_mgr = StatsMgr::instance();
+
+ // Let's prepare the "old" configuration: a subnet with id 123
+ // and pretend there were addresses assigned, so statistics are non-zero.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.1.2.0"), 24, 1, 2, 3, 123));
+ CfgSubnets4Ptr subnets = cfg_mgr.getStagingCfg()->getCfgSubnets4();
+ subnets->add(subnet1);
+ cfg_mgr.commit();
+ stats_mgr.addValue("subnet[123].total-addresses", static_cast<int64_t>(256));
+ stats_mgr.setValue("subnet[123].assigned-addresses", static_cast<int64_t>(150));
+
+ // The stats should be there.
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[123].total-addresses"));
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[123].assigned-addresses"));
+
+ // Let's remove all configurations
+ cfg_mgr.clear();
+
+ // The stats should not be there anymore.
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[123].total-addresses"));
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[123].assigned-addresses"));
+}
+
+// This test verifies that once the configuration is committed, statistics
+// are updated appropriately.
+TEST_F(CfgMgrTest, commitStats6) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ StatsMgr& stats_mgr = StatsMgr::instance();
+ startBackend(AF_INET6);
+
+ // Let's prepare the "old" configuration: a subnet with id 123
+ // and pretend there were addresses assigned, so statistics are non-zero.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4, 123));
+ CfgSubnets6Ptr subnets = cfg_mgr.getStagingCfg()->getCfgSubnets6();
+ subnets->add(subnet1);
+ cfg_mgr.commit();
+ stats_mgr.addValue("subnet[123].total-nas", static_cast<int64_t>(256));
+ stats_mgr.setValue("subnet[123].assigned-nas", static_cast<int64_t>(150));
+
+ stats_mgr.addValue("subnet[123].total-pds", static_cast<int64_t>(256));
+ stats_mgr.setValue("subnet[123].assigned-pds", static_cast<int64_t>(150));
+
+ // Now, let's change the configuration to something new.
+
+ // There's a subnet 2001:db8:2::/48 with ID=42
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4, 42));
+
+ // Let's make pools with 128 addresses and 65536 prefixes available.
+ PoolPtr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:2::"), 121)); // 128 addrs
+ PoolPtr pool2(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:3::"), 96, 112)); // 65536 prefixes
+ subnet2->addPool(pool1);
+ subnet2->addPool(pool2);
+
+ subnets = cfg_mgr.getStagingCfg()->getCfgSubnets6();
+ subnets->add(subnet2);
+
+ // Change the stats default limits.
+ cfg_mgr.getStagingCfg()->addConfiguredGlobal("statistic-default-sample-count",
+ Element::create(14));
+ cfg_mgr.getStagingCfg()->addConfiguredGlobal("statistic-default-sample-age",
+ Element::create(10));
+
+ // Let's commit it
+ cfg_mgr.commit();
+
+ EXPECT_EQ(14, stats_mgr.getMaxSampleCountDefault());
+ EXPECT_EQ("00:00:10", durationToText(stats_mgr.getMaxSampleAgeDefault(), 0));
+
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[123].total-nas"));
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[123].assigned-nas"));
+
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[123].total-pds"));
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[123].assigned-pds"));
+
+ ObservationPtr total_addrs;
+ EXPECT_NO_THROW(total_addrs = stats_mgr.getObservation("subnet[42].total-nas"));
+ ASSERT_TRUE(total_addrs);
+ EXPECT_EQ(128, total_addrs->getInteger().first);
+ EXPECT_TRUE(total_addrs->getMaxSampleCount().first);
+ EXPECT_EQ(14, total_addrs->getMaxSampleCount().second);
+ EXPECT_FALSE(total_addrs->getMaxSampleAge().first);
+ EXPECT_EQ("00:00:10", durationToText(total_addrs->getMaxSampleAge().second, 0));
+
+ ObservationPtr total_prfx;
+ EXPECT_NO_THROW(total_prfx = stats_mgr.getObservation("subnet[42].total-pds"));
+ ASSERT_TRUE(total_prfx);
+ EXPECT_EQ(65536, total_prfx->getInteger().first);
+ EXPECT_TRUE(total_prfx->getMaxSampleCount().first);
+ EXPECT_EQ(14, total_prfx->getMaxSampleCount().second);
+ EXPECT_FALSE(total_prfx->getMaxSampleAge().first);
+ EXPECT_EQ("00:00:10", durationToText(total_prfx->getMaxSampleAge().second, 0));
+}
+
+// This test verifies that once the configuration is merged into the current
+// configuration, statistics are updated appropriately.
+/// @todo Enable this test once merging v6 configuration is enabled.
+TEST_F(CfgMgrTest, DISABLED_mergeIntoCurrentStats6) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ StatsMgr& stats_mgr = StatsMgr::instance();
+ startBackend(AF_INET6);
+
+ // Let's prepare the "old" configuration: a subnet with id 123
+ // and pretend there were addresses assigned, so statistics are non-zero.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4, 123));
+ CfgSubnets6Ptr subnets = cfg_mgr.getStagingCfg()->getCfgSubnets6();
+ subnets->add(subnet1);
+ cfg_mgr.commit();
+ stats_mgr.addValue("subnet[123].total-nas", static_cast<int64_t>(256));
+ stats_mgr.setValue("subnet[123].assigned-nas", static_cast<int64_t>(150));
+
+ stats_mgr.addValue("subnet[123].total-pds", static_cast<int64_t>(256));
+ stats_mgr.setValue("subnet[123].assigned-pds", static_cast<int64_t>(150));
+
+ // There should be no stats for subnet 42 at this point.
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[42].total-nas"));
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[42].assigned-nas"));
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[42].total-pds"));
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[42].assigned-pds"));
+
+ // Now, let's create new configuration with updates.
+
+ // There's a subnet 2001:db8:2::/48 with ID=42
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4, 42));
+
+ // Let's make pools with 128 addresses and 65536 prefixes available.
+ PoolPtr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:2::"), 121)); // 128 addrs
+ PoolPtr pool2(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:3::"), 96, 112)); // 65536 prefixes
+ subnet2->addPool(pool1);
+ subnet2->addPool(pool2);
+
+ // Create external configuration to be merged into current one.
+ auto external_cfg = CfgMgr::instance().createExternalCfg();
+ subnets = external_cfg->getCfgSubnets6();
+ subnets->add(subnet2);
+
+ external_cfg->addConfiguredGlobal("statistic-default-sample-count",
+ Element::create(17));
+ external_cfg->addConfiguredGlobal("statistic-default-sample-age",
+ Element::create(4));
+
+ // Let's merge it.
+ cfg_mgr.mergeIntoCurrentCfg(external_cfg->getSequence());
+
+ EXPECT_EQ(17, stats_mgr.getMaxSampleCountDefault());
+ EXPECT_EQ("00:00:04", durationToText(stats_mgr.getMaxSampleAgeDefault(), 0));
+
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[42].total-nas"));
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[42].assigned-nas"));
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[42].total-pds"));
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[42].assigned-pds"));
+
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[123].total-nas"));
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[123].assigned-nas"));
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[123].total-pds"));
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[123].assigned-pds"));
+
+ ObservationPtr total_addrs;
+ EXPECT_NO_THROW(total_addrs = stats_mgr.getObservation("subnet[42].total-nas"));
+ ASSERT_TRUE(total_addrs);
+ EXPECT_EQ(128, total_addrs->getInteger().first);
+
+ EXPECT_NO_THROW(total_addrs = stats_mgr.getObservation("subnet[42].total-pds"));
+ ASSERT_TRUE(total_addrs);
+ EXPECT_EQ(65536, total_addrs->getInteger().first);
+}
+
+// This test verifies that once the configuration is cleared, the v6 statistics
+// are removed.
+TEST_F(CfgMgrTest, clearStats6) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ StatsMgr& stats_mgr = StatsMgr::instance();
+
+ // Let's prepare the "old" configuration: a subnet with id 123
+ // and pretend there were addresses assigned, so statistics are non-zero.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4, 123));
+ CfgSubnets6Ptr subnets = cfg_mgr.getStagingCfg()->getCfgSubnets6();
+ subnets->add(subnet1);
+ cfg_mgr.commit();
+ stats_mgr.addValue("subnet[123].total-nas", static_cast<int64_t>(256));
+ stats_mgr.setValue("subnet[123].assigned-nas", static_cast<int64_t>(150));
+
+ stats_mgr.addValue("subnet[123].total-pds", static_cast<int64_t>(256));
+ stats_mgr.setValue("subnet[123].assigned-pds", static_cast<int64_t>(150));
+
+ // The stats should be there.
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[123].total-nas"));
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[123].assigned-nas"));
+
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[123].total-pds"));
+ EXPECT_TRUE(stats_mgr.getObservation("subnet[123].assigned-pds"));
+
+ // Let's remove all configurations
+ cfg_mgr.clear();
+
+ // The stats should not be there anymore.
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[123].total-nas"));
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[123].assigned-nas"));
+
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[123].total-pds"));
+ EXPECT_FALSE(stats_mgr.getObservation("subnet[123].assigned-pds"));
+}
+
+// This test verifies that the external configuration can be merged into
+// the staging configuration via CfgMgr.
+TEST_F(CfgMgrTest, mergeIntoStagingCfg) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+
+ // Create first external configuration.
+ SrvConfigPtr ext_cfg1;
+ ASSERT_NO_THROW(ext_cfg1 = cfg_mgr.createExternalCfg());
+ ASSERT_TRUE(ext_cfg1);
+ // It should pick the first available sequence number.
+ EXPECT_EQ(0, ext_cfg1->getSequence());
+
+ // Create second external configuration.
+ SrvConfigPtr ext_cfg2;
+ ASSERT_NO_THROW(ext_cfg2 = cfg_mgr.createExternalCfg());
+ ASSERT_TRUE(ext_cfg2);
+ // It should pick the next available sequence number.
+ EXPECT_EQ(1, ext_cfg2->getSequence());
+
+ // Those must be two separate instances.
+ ASSERT_FALSE(ext_cfg1 == ext_cfg2);
+
+ // Add a subnet which will be merged from first configuration.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.1.2.0"), 24, 1, 2, 3, 123));
+ ext_cfg1->getCfgSubnets4()->add(subnet1);
+
+ // Add a subnet which will be merged from the second configuration.
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.1.3.0"), 24, 1, 2, 3, 124));
+ ext_cfg2->getCfgSubnets4()->add(subnet2);
+
+ // Merge first configuration.
+ ASSERT_NO_THROW(cfg_mgr.mergeIntoStagingCfg(ext_cfg1->getSequence()));
+ // Second attempt should fail because the configuration is discarded after
+ // the merge.
+ ASSERT_THROW(cfg_mgr.mergeIntoStagingCfg(ext_cfg1->getSequence()), BadValue);
+
+ // Check that the subnet from first configuration has been merged but not
+ // from the second configuration.
+ ASSERT_TRUE(cfg_mgr.getStagingCfg()->getCfgSubnets4()->getBySubnetId(123));
+ ASSERT_FALSE(cfg_mgr.getStagingCfg()->getCfgSubnets4()->getBySubnetId(124));
+
+ // Create another configuration instance to check what sequence it would
+ // pick. It should pick the first available one.
+ SrvConfigPtr ext_cfg3;
+ ASSERT_NO_THROW(ext_cfg3 = cfg_mgr.createExternalCfg());
+ ASSERT_TRUE(ext_cfg3);
+ EXPECT_EQ(2, ext_cfg3->getSequence());
+
+ // Merge the second and third (empty) configuration.
+ ASSERT_NO_THROW(cfg_mgr.mergeIntoStagingCfg(ext_cfg2->getSequence()));
+ ASSERT_NO_THROW(cfg_mgr.mergeIntoStagingCfg(ext_cfg3->getSequence()));
+
+ // Make sure that both subnets have been merged.
+ ASSERT_TRUE(cfg_mgr.getStagingCfg()->getCfgSubnets4()->getBySubnetId(123));
+ ASSERT_TRUE(cfg_mgr.getStagingCfg()->getCfgSubnets4()->getBySubnetId(124));
+
+ // The next configuration instance should reset the sequence to 0 because
+ // there are no other configurations in CfgMgr.
+ SrvConfigPtr ext_cfg4;
+ ASSERT_NO_THROW(ext_cfg4 = cfg_mgr.createExternalCfg());
+ ASSERT_TRUE(ext_cfg4);
+ EXPECT_EQ(0, ext_cfg4->getSequence());
+
+ // Try to commit the staging configuration.
+ ASSERT_NO_THROW(cfg_mgr.commit());
+
+ // Make sure that both subnets are present in the current configuration.
+ EXPECT_TRUE(cfg_mgr.getCurrentCfg()->getCfgSubnets4()->getBySubnetId(123));
+ EXPECT_TRUE(cfg_mgr.getCurrentCfg()->getCfgSubnets4()->getBySubnetId(124));
+
+ // The staging configuration should not include them.
+ EXPECT_FALSE(cfg_mgr.getStagingCfg()->getCfgSubnets4()->getBySubnetId(123));
+ EXPECT_FALSE(cfg_mgr.getStagingCfg()->getCfgSubnets4()->getBySubnetId(124));
+}
+
+// This test verifies that the external configuration can be merged into
+// the current configuration via CfgMgr.
+TEST_F(CfgMgrTest, mergeIntoCurrentCfg) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+
+ // Create first external configuration.
+ SrvConfigPtr ext_cfg1;
+ ASSERT_NO_THROW(ext_cfg1 = cfg_mgr.createExternalCfg());
+ ASSERT_TRUE(ext_cfg1);
+ // It should pick the first available sequence number.
+ EXPECT_EQ(0, ext_cfg1->getSequence());
+
+ // Create second external configuration.
+ SrvConfigPtr ext_cfg2;
+ ASSERT_NO_THROW(ext_cfg2 = cfg_mgr.createExternalCfg());
+ ASSERT_TRUE(ext_cfg2);
+ // It should pick the next available sequence number.
+ EXPECT_EQ(1, ext_cfg2->getSequence());
+
+ // Those must be two separate instances.
+ ASSERT_FALSE(ext_cfg1 == ext_cfg2);
+
+ // Add a subnet which will be merged from first configuration.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.1.2.0"), 24, 1, 2, 3, 123));
+ ext_cfg1->getCfgSubnets4()->add(subnet1);
+
+ // Add a subnet which will be merged from the second configuration.
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.1.3.0"), 24, 1, 2, 3, 124));
+ ext_cfg2->getCfgSubnets4()->add(subnet2);
+
+ // Merge first configuration.
+ ASSERT_NO_THROW(cfg_mgr.mergeIntoCurrentCfg(ext_cfg1->getSequence()));
+ // Second attempt should fail because the configuration is discarded after
+ // the merge.
+ ASSERT_THROW(cfg_mgr.mergeIntoCurrentCfg(ext_cfg1->getSequence()), BadValue);
+
+ // Check that the subnet from first configuration has been merged but not
+ // from the second configuration.
+ ASSERT_TRUE(cfg_mgr.getCurrentCfg()->getCfgSubnets4()->getBySubnetId(123));
+ ASSERT_FALSE(cfg_mgr.getCurrentCfg()->getCfgSubnets4()->getBySubnetId(124));
+
+ // Create another configuration instance to check what sequence it would
+ // pick. It should pick the first available one.
+ SrvConfigPtr ext_cfg3;
+ ASSERT_NO_THROW(ext_cfg3 = cfg_mgr.createExternalCfg());
+ ASSERT_TRUE(ext_cfg3);
+ EXPECT_EQ(2, ext_cfg3->getSequence());
+
+ // Merge the second and third (empty) configuration.
+ ASSERT_NO_THROW(cfg_mgr.mergeIntoCurrentCfg(ext_cfg2->getSequence()));
+ ASSERT_NO_THROW(cfg_mgr.mergeIntoCurrentCfg(ext_cfg3->getSequence()));
+
+ // Make sure that both subnets have been merged.
+ ASSERT_TRUE(cfg_mgr.getCurrentCfg()->getCfgSubnets4()->getBySubnetId(123));
+ ASSERT_TRUE(cfg_mgr.getCurrentCfg()->getCfgSubnets4()->getBySubnetId(124));
+
+ // The next configuration instance should reset the sequence to 0 because
+ // there are no other configurations in CfgMgr.
+ SrvConfigPtr ext_cfg4;
+ ASSERT_NO_THROW(ext_cfg4 = cfg_mgr.createExternalCfg());
+ ASSERT_TRUE(ext_cfg4);
+ EXPECT_EQ(0, ext_cfg4->getSequence());
+}
+
+/// @todo Add unit-tests for testing:
+/// - addActiveIface() with invalid interface name
+/// - addActiveIface() with the same interface twice
+/// - addActiveIface() with a bogus address
+///
+/// This is somewhat tricky. Care should be taken here, because it is rather
+/// difficult to decide if interface name is valid or not. Some servers, e.g.
+/// dibbler, allow to specify interface names that are not currently present in
+/// the system. The server accepts them, but upon discovering that they are
+/// yet available (for different definitions of not being available), adds
+/// the to to-be-activated list.
+///
+/// Cases covered by dibbler are:
+/// - missing interface (e.g. PPP connection that is not established yet)
+/// - downed interface (no link local address, no way to open sockets)
+/// - up, but not running interface (wifi up, but not associated)
+/// - tentative addresses (interface up and running, but DAD procedure is
+/// still in progress)
+/// - weird interfaces without link-local addresses (don't ask, 6rd tunnels
+/// look weird to me as well)
+
+// No specific tests for getSubnet6. That method (2 overloaded versions) is tested
+// in Dhcpv6SrvTest.selectSubnetAddr and Dhcpv6SrvTest.selectSubnetIface
+// (see src/bin/dhcp6/tests/dhcp6_srv_unittest.cc)
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/client_class_def_parser_unittest.cc b/src/lib/dhcpsrv/tests/client_class_def_parser_unittest.cc
new file mode 100644
index 0000000..7751591
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/client_class_def_parser_unittest.cc
@@ -0,0 +1,1454 @@
+// Copyright (C) 2015-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <cc/data.h>
+#include <dhcp/libdhcp++.h>
+#include <dhcp/option.h>
+#include <dhcp/option_string.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/parsers/client_class_def_parser.h>
+#include <dhcpsrv/parsers/dhcp_parsers.h>
+#include <asiolink/io_address.h>
+#include <eval/evaluate.h>
+#include <testutils/gtest_utils.h>
+#include <gtest/gtest.h>
+#include <sstream>
+#include <stdint.h>
+#include <string>
+
+/// @file client_class_def_parser_unittest.cc Unit tests for client class
+/// definition parsing.
+
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::asiolink;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Test fixture class for @c ExpressionParser.
+class ExpressionParserTest : public ::testing::Test {
+protected:
+
+ /// @brief Test that validate expression can be evaluated against v4 or
+ /// v6 packet.
+ ///
+ /// Verifies that given a valid expression, the ExpressionParser
+ /// produces an Expression which can be evaluated against a v4 or v6
+ /// packet.
+ ///
+ /// @param family AF_INET or AF_INET6
+ /// @param expression Textual representation of the expression to be
+ /// evaluated.
+ /// @param option_string String data to be placed in the hostname
+ /// option, being placed in the packet used for evaluation.
+ /// @tparam Type of the packet: @c Pkt4 or @c Pkt6.
+ template<typename PktType>
+ void testValidExpression(uint16_t family,
+ const std::string& expression,
+ const std::string& option_string) {
+ ExpressionPtr parsed_expr;
+ ExpressionParser parser;
+
+ // Turn config into elements. This may emit exceptions.
+ ElementPtr config_element = Element::fromJSON(expression);
+
+ // Expression should parse.
+ ASSERT_NO_THROW(parser.parse(parsed_expr, config_element, family));
+
+ // Parsed expression should exist.
+ ASSERT_TRUE(parsed_expr);
+
+ // Build a packet that will fail evaluation.
+ uint8_t message_type;
+ if (family == AF_INET) {
+ message_type = DHCPDISCOVER;
+ } else {
+ message_type = DHCPV6_SOLICIT;
+ }
+ boost::shared_ptr<PktType> pkt(new PktType(message_type, 123));
+ EXPECT_FALSE(evaluateBool(*parsed_expr, *pkt));
+
+ // Now add the option so it will pass. Use a standard option carrying a
+ // single string value, i.e. hostname for DHCPv4 and bootfile url for
+ // DHCPv6.
+ Option::Universe universe(family == AF_INET ? Option::V4 : Option::V6);
+ uint16_t option_type;
+ if (family == AF_INET) {
+ option_type = DHO_HOST_NAME;
+ } else {
+ option_type = D6O_BOOTFILE_URL;
+ }
+ OptionPtr opt(new OptionString(universe, option_type, option_string));
+ pkt->addOption(opt);
+ EXPECT_TRUE(evaluateBool(*parsed_expr, *pkt));
+ }
+};
+
+/// @brief Test fixture class for @c ClientClassDefParser.
+class ClientClassDefParserTest : public ::testing::Test {
+protected:
+
+ /// @brief Convenience method for parsing a configuration
+ ///
+ /// Attempt to parse a given client class definition.
+ ///
+ /// @param config - JSON string containing the client class configuration
+ /// to parse.
+ /// @param family - the address family in which the parsing should
+ /// occur.
+ /// @return Returns a pointer to class instance created, or NULL if
+ /// for some unforeseen reason it wasn't created in the local dictionary
+ /// @throw indirectly, exceptions converting the JSON text to elements,
+ /// or by the parsing itself are not caught
+ ClientClassDefPtr parseClientClassDef(const std::string& config,
+ uint16_t family) {
+ // Create local dictionary to which the parser add the class.
+ ClientClassDictionaryPtr dictionary(new ClientClassDictionary());
+
+ // Turn config into elements. This may emit exceptions.
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse the configuration. This may emit exceptions.
+ ClientClassDefParser parser;
+ parser.parse(dictionary, config_element, family);
+
+ // If we didn't throw, then return the first and only class
+ ClientClassDefListPtr classes = dictionary->getClasses();
+ ClientClassDefList::const_iterator it = classes->cbegin();
+ if (it != classes->cend()) {
+ return (*it);
+ }
+
+ // Return NULL if for some reason the class doesn't exist.
+ return (ClientClassDefPtr());
+ }
+
+ /// @brief Test that client class parser throws when unspported parameter
+ /// is specified.
+ ///
+ /// @param config JSON string containing the client class configuration.
+ /// @param family The address family indicating whether the DHCPv4 or
+ /// DHCPv6 client class is parsed.
+ void testClassParamsUnsupported(const std::string& config,
+ const uint16_t family) {
+ ElementPtr config_element = Element::fromJSON(config);
+
+ ClientClassDefParser parser;
+ EXPECT_THROW(parser.checkParametersSupported(config_element, family),
+ DhcpConfigError);
+ }
+};
+
+/// @brief Test fixture class for @c ClientClassDefListParser.
+class ClientClassDefListParserTest : public ::testing::Test {
+protected:
+
+ /// @brief Convenience method for parsing a list of client class
+ /// definitions.
+ ///
+ /// Attempt to parse a given list of client class definitions into a
+ /// ClientClassDictionary.
+ ///
+ /// @param config - JSON string containing the list of definitions to parse.
+ /// @param family - the address family in which the parsing should
+ /// occur.
+ /// @param check_dependencies - indicates if the parser should check whether
+ /// referenced classes exist.
+ /// @return Returns a pointer to class dictionary created
+ /// @throw indirectly, exceptions converting the JSON text to elements,
+ /// or by the parsing itself are not caught
+ ClientClassDictionaryPtr parseClientClassDefList(const std::string& config,
+ uint16_t family,
+ bool check_dependencies = true)
+ {
+ // Turn config into elements. This may emit exceptions.
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse the configuration. This may emit exceptions.
+ ClientClassDefListParser parser;
+ return (parser.parse(config_element, family, check_dependencies));
+ }
+};
+
+// Verifies that given a valid expression, the ExpressionParser
+// produces an Expression which can be evaluated against a v4 packet.
+TEST_F(ExpressionParserTest, validExpression4) {
+ testValidExpression<Pkt4>(AF_INET,
+ "\"option[12].text == 'hundred4'\"",
+ "hundred4");
+}
+
+// Verifies that the option name can be used in the evaluated expression.
+TEST_F(ExpressionParserTest, validExpressionWithOptionName4) {
+ testValidExpression<Pkt4>(AF_INET,
+ "\"option[host-name].text == 'hundred4'\"",
+ "hundred4");
+}
+
+// Verifies that given a valid expression using .hex operator for option, the
+// ExpressionParser produces an Expression which can be evaluated against
+// a v4 packet.
+TEST_F(ExpressionParserTest, validExpressionWithHex4) {
+ testValidExpression<Pkt4>(AF_INET,
+ "\"option[12].hex == 0x68756E6472656434\"",
+ "hundred4");
+}
+
+// Verifies that the option name can be used together with .hex operator in
+// the evaluated expression.
+TEST_F(ExpressionParserTest, validExpressionWithOptionNameAndHex4) {
+ testValidExpression<Pkt6>(AF_INET,
+ "\"option[host-name].text == 0x68756E6472656434\"",
+ "hundred4");
+}
+
+// Verifies that given a valid expression, the ExpressionParser
+// produces an Expression which can be evaluated against a v6 packet.
+TEST_F(ExpressionParserTest, validExpression6) {
+ testValidExpression<Pkt6>(AF_INET6,
+ "\"option[59].text == 'hundred6'\"",
+ "hundred6");
+}
+
+// Verifies that the option name can be used in the evaluated expression.
+TEST_F(ExpressionParserTest, validExpressionWithOptionName6) {
+ testValidExpression<Pkt6>(AF_INET6,
+ "\"option[bootfile-url].text == 'hundred6'\"",
+ "hundred6");
+}
+
+// Verifies that given a valid expression using .hex operator for option, the
+// ExpressionParser produces an Expression which can be evaluated against
+// a v6 packet.
+TEST_F(ExpressionParserTest, validExpressionWithHex6) {
+ testValidExpression<Pkt6>(AF_INET6,
+ "\"option[59].hex == 0x68756E6472656436\"",
+ "hundred6");
+}
+
+// Verifies that the option name can be used together with .hex operator in
+// the evaluated expression.
+TEST_F(ExpressionParserTest, validExpressionWithOptionNameAndHex6) {
+ testValidExpression<Pkt6>(AF_INET6,
+ "\"option[bootfile-url].text == 0x68756E6472656436\"",
+ "hundred6");
+}
+
+// Verifies that an the ExpressionParser only accepts StringElements.
+TEST_F(ExpressionParserTest, invalidExpressionElement) {
+ // This will create an integer element should fail.
+ std::string cfg_txt = "777";
+ ElementPtr config_element = Element::fromJSON(cfg_txt);
+
+ // Create the parser.
+ ExpressionPtr parsed_expr;
+ ExpressionParser parser;
+
+ // Expression parsing should fail.
+ ASSERT_THROW(parser.parse(parsed_expr, config_element, AF_INET6),
+ DhcpConfigError);
+}
+
+// Verifies that given an invalid expression with a syntax error,
+// the Expression parser will throw a DhdpConfigError. Note this
+// is not intended to be an exhaustive test or expression syntax.
+// It is simply to ensure that if the parser fails, it does so
+// Properly.
+TEST_F(ExpressionParserTest, expressionSyntaxError) {
+ // Turn config into elements.
+ std::string cfg_txt = "\"option 'bogus'\"";
+ ElementPtr config_element = Element::fromJSON(cfg_txt);
+
+ // Create the parser.
+ ExpressionPtr parsed_expr;
+ ExpressionParser parser;
+
+ // Expression parsing should fail.
+ ASSERT_THROW(parser.parse(parsed_expr, config_element, AF_INET),
+ DhcpConfigError);
+}
+
+// Verifies that the name parameter is required and must not be empty
+TEST_F(ExpressionParserTest, nameEmpty) {
+ std::string cfg_txt = "{ \"name\": \"\" }";
+ ElementPtr config_element = Element::fromJSON(cfg_txt);
+
+ // Create the parser.
+ ExpressionPtr parsed_expr;
+ ExpressionParser parser;
+
+ // Expression parsing should fail.
+ ASSERT_THROW(parser.parse(parsed_expr, config_element, AF_INET6),
+ DhcpConfigError);
+}
+
+// Verifies that the function checking if specified client class parameters
+// are supported does not throw if all specified DHCPv4 client class
+// parameters are recognized.
+TEST_F(ClientClassDefParserTest, checkAllSupported4) {
+ std::string cfg_text =
+ "{\n"
+ " \"name\": \"foo\","
+ " \"test\": \"member('ALL')\","
+ " \"option-def\": [ ],\n"
+ " \"option-data\": [ ],\n"
+ " \"user-context\": { },\n"
+ " \"only-if-required\": false,\n"
+ " \"next-server\": \"192.0.2.3\",\n"
+ " \"server-hostname\": \"myhost\",\n"
+ " \"boot-file-name\": \"efi\""
+ "}\n";
+
+ ElementPtr config_element = Element::fromJSON(cfg_text);
+
+ ClientClassDefParser parser;
+ EXPECT_NO_THROW(parser.checkParametersSupported(config_element, AF_INET));
+}
+
+// Verifies that the function checking if specified client class parameters
+// are supported does not throw if all specified DHCPv6 client class
+// parameters are recognized.
+TEST_F(ClientClassDefParserTest, checkAllSupported6) {
+ std::string cfg_text =
+ "{\n"
+ " \"name\": \"foo\","
+ " \"test\": \"member('ALL')\","
+ " \"option-data\": [ ],\n"
+ " \"user-context\": { },\n"
+ " \"only-if-required\": false\n"
+ "}\n";
+
+ ElementPtr config_element = Element::fromJSON(cfg_text);
+
+ ClientClassDefParser parser;
+ EXPECT_NO_THROW(parser.checkParametersSupported(config_element, AF_INET6));
+}
+
+// Verifies that the function checking if specified client class parameters
+// are supported throws if DHCPv4 specific parameters are specified for the
+// DHCPv6 client class.
+TEST_F(ClientClassDefParserTest, checkParams4Unsupported6) {
+ std::string cfg_text;
+
+ {
+ SCOPED_TRACE("option-def");
+ cfg_text =
+ "{\n"
+ " \"name\": \"foo\","
+ " \"test\": \"member('ALL')\","
+ " \"option-def\": [ ],\n"
+ " \"option-data\": [ ],\n"
+ " \"user-context\": { },\n"
+ " \"only-if-required\": false\n"
+ "}\n";
+
+ testClassParamsUnsupported(cfg_text, AF_INET6);
+ }
+
+ {
+ SCOPED_TRACE("next-server");
+ cfg_text =
+ "{\n"
+ " \"name\": \"foo\","
+ " \"test\": \"member('ALL')\","
+ " \"option-data\": [ ],\n"
+ " \"user-context\": { },\n"
+ " \"only-if-required\": false,\n"
+ " \"next-server\": \"192.0.2.3\"\n"
+ "}\n";
+
+ testClassParamsUnsupported(cfg_text, AF_INET6);
+ }
+
+ {
+ SCOPED_TRACE("server-hostname");
+ cfg_text =
+ "{\n"
+ " \"name\": \"foo\","
+ " \"test\": \"member('ALL')\","
+ " \"option-data\": [ ],\n"
+ " \"user-context\": { },\n"
+ " \"only-if-required\": false,\n"
+ " \"server-hostname\": \"myhost\"\n"
+ "}\n";
+
+ testClassParamsUnsupported(cfg_text, AF_INET6);
+ }
+
+ {
+ SCOPED_TRACE("boot-file-name");
+ cfg_text =
+ "{\n"
+ " \"name\": \"foo\","
+ " \"test\": \"member('ALL')\","
+ " \"option-data\": [ ],\n"
+ " \"user-context\": { },\n"
+ " \"only-if-required\": false,\n"
+ " \"boot-file-name\": \"efi\""
+ "}\n";
+
+ testClassParamsUnsupported(cfg_text, AF_INET6);
+ }
+}
+
+// Verifies that the function checking if specified DHCPv4 client class
+// parameters are supported throws if one of the parameters is not recognized.
+TEST_F(ClientClassDefParserTest, checkParams4Unsupported) {
+ std::string cfg_text =
+ "{\n"
+ " \"name\": \"foo\","
+ " \"unsupported\": \"member('ALL')\""
+ "}\n";
+
+ testClassParamsUnsupported(cfg_text, AF_INET);
+}
+
+// Verifies that the function checking if specified DHCPv6 client class
+// parameters are supported throws if one of the parameters is not recognized.
+TEST_F(ClientClassDefParserTest, checkParams6Unsupported) {
+ std::string cfg_text =
+ "{\n"
+ " \"name\": \"foo\","
+ " \"unsupported\": \"member('ALL')\""
+ "}\n";
+
+ testClassParamsUnsupported(cfg_text, AF_INET6);
+}
+
+// Verifies you can create a class with only a name
+// Whether that's useful or not, remains to be seen.
+// For now the class allows it.
+TEST_F(ClientClassDefParserTest, nameOnlyValid) {
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"MICROSOFT\" \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET));
+
+ // We should find our class.
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("MICROSOFT", cclass->getName());
+
+ // CfgOption should be a non-null pointer but there
+ // should be no options. Currently there's no good
+ // way to test that there no options.
+ CfgOptionPtr cfg_option;
+ cfg_option = cclass->getCfgOption();
+ ASSERT_TRUE(cfg_option);
+ OptionContainerPtr oc;
+ ASSERT_TRUE(oc = cclass->getCfgOption()->getAll(DHCP4_OPTION_SPACE));
+ EXPECT_EQ(0, oc->size());
+
+ // Verify we have no expression.
+ ASSERT_FALSE(cclass->getMatchExpr());
+}
+
+// Verifies you can create a class with a name, expression,
+// but no options.
+// @todo same with AF_INET6
+TEST_F(ClientClassDefParserTest, nameAndExpressionClass) {
+
+ std::string test = "option[100].text == 'works right'";
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"class_one\", \n"
+ " \"test\": \"" + test + "\" \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET));
+
+ // We should find our class.
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("class_one", cclass->getName());
+
+ // CfgOption should be a non-null pointer but there
+ // should be no options. Currently there's no good
+ // way to test that there no options.
+ CfgOptionPtr cfg_option;
+ cfg_option = cclass->getCfgOption();
+ ASSERT_TRUE(cfg_option);
+ OptionContainerPtr oc;
+ ASSERT_TRUE(oc = cclass->getCfgOption()->getAll(DHCP4_OPTION_SPACE));
+ EXPECT_EQ(0, oc->size());
+
+ // Verify we can retrieve the expression
+ ExpressionPtr match_expr = cclass->getMatchExpr();
+ ASSERT_TRUE(match_expr);
+
+ // Verify the original expression was saved.
+ EXPECT_EQ(test, cclass->getTest());
+
+ // Build a packet that will fail evaluation.
+ Pkt4Ptr pkt4(new Pkt4(DHCPDISCOVER, 123));
+ EXPECT_FALSE(evaluateBool(*match_expr, *pkt4));
+
+ // Now add the option so it will pass.
+ OptionPtr opt(new OptionString(Option::V4, 100, "works right"));
+ pkt4->addOption(opt);
+ EXPECT_TRUE(evaluateBool(*match_expr, *pkt4));
+}
+
+// Verifies you can create a class with a name and options,
+// but no expression.
+// @todo same with AF_INET6
+TEST_F(ClientClassDefParserTest, nameAndOptionsClass) {
+
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"MICROSOFT\", \n"
+ " \"option-data\": [ \n"
+ " { \n"
+ " \"name\": \"domain-name-servers\", \n"
+ " \"code\": 6, \n"
+ " \"space\": \"dhcp4\", \n"
+ " \"csv-format\": true, \n"
+ " \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET));
+
+ // We should find our class.
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("MICROSOFT", cclass->getName());
+
+ // Our one option should exist.
+ OptionDescriptor od = cclass->getCfgOption()->get(DHCP4_OPTION_SPACE, 6);
+ ASSERT_TRUE(od.option_);
+ EXPECT_EQ(6, od.option_->getType());
+
+ // Verify we have no expression
+ ASSERT_FALSE(cclass->getMatchExpr());
+}
+
+
+// Verifies you can create a class with a name, expression,
+// and options.
+// @todo same with AF_INET6
+TEST_F(ClientClassDefParserTest, basicValidClass) {
+
+ std::string test = "option[100].text == 'booya'";
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"MICROSOFT\", \n"
+ " \"test\": \"" + test + "\", \n"
+ " \"option-data\": [ \n"
+ " { \n"
+ " \"name\": \"domain-name-servers\", \n"
+ " \"code\": 6, \n"
+ " \"space\": \"dhcp4\", \n"
+ " \"csv-format\": true, \n"
+ " \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET));
+
+ // We should find our class.
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("MICROSOFT", cclass->getName());
+
+ // Our one option should exist.
+ OptionDescriptor od = cclass->getCfgOption()->get(DHCP4_OPTION_SPACE, 6);
+ ASSERT_TRUE(od.option_);
+ EXPECT_EQ(6, od.option_->getType());
+
+ // Verify we can retrieve the expression
+ ExpressionPtr match_expr = cclass->getMatchExpr();
+ ASSERT_TRUE(match_expr);
+
+ // Verify the original expression was saved.
+ EXPECT_EQ(test, cclass->getTest());
+
+ // Build a packet that will fail evaluation.
+ Pkt4Ptr pkt4(new Pkt4(DHCPDISCOVER, 123));
+ EXPECT_FALSE(evaluateBool(*match_expr, *pkt4));
+
+ // Now add the option so it will pass.
+ OptionPtr opt(new OptionString(Option::V4, 100, "booya"));
+ pkt4->addOption(opt);
+ EXPECT_TRUE(evaluateBool(*match_expr, *pkt4));
+}
+
+// Verifies that a class with no name, fails to parse.
+TEST_F(ClientClassDefParserTest, noClassName) {
+
+ std::string cfg_text =
+ "{ \n"
+ " \"test\": \"option[123].text == 'abc'\", \n"
+ " \"option-data\": [ \n"
+ " { \n"
+ " \"name\": \"domain-name-servers\", \n"
+ " \"code\": 6, \n"
+ " \"space\": \"dhcp4\", \n"
+ " \"csv-format\": true, \n"
+ " \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_THROW(cclass = parseClientClassDef(cfg_text, AF_INET),
+ DhcpConfigError);
+}
+
+// Verifies that a class with a blank name, fails to parse.
+TEST_F(ClientClassDefParserTest, blankClassName) {
+
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"\", \n"
+ " \"test\": \"option[123].text == 'abc'\", \n"
+ " \"option-data\": [ \n"
+ " { \n"
+ " \"name\": \"domain-name-servers\", \n"
+ " \"code\": 6, \n"
+ " \"space\": \"dhcp4\", \n"
+ " \"csv-format\": true, \n"
+ " \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_THROW(cclass = parseClientClassDef(cfg_text, AF_INET),
+ DhcpConfigError);
+}
+
+// Verifies that a class with an invalid expression, fails to parse.
+TEST_F(ClientClassDefParserTest, invalidExpression) {
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"one\", \n"
+ " \"test\": 777 \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_THROW(cclass = parseClientClassDef(cfg_text, AF_INET6),
+ DhcpConfigError);
+}
+
+// Verifies that a class with invalid option-def, fails to parse.
+TEST_F(ClientClassDefParserTest, invalidOptionDef) {
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"one\", \n"
+ " \"option-def\": [ \n"
+ " { \"bogus\": \"bad\" } \n"
+ " ] \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_THROW(cclass = parseClientClassDef(cfg_text, AF_INET),
+ DhcpConfigError);
+}
+
+// Verifies that a class with invalid option-data, fails to parse.
+TEST_F(ClientClassDefParserTest, invalidOptionData) {
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"one\", \n"
+ " \"option-data\": [ \n"
+ " { \"bogus\": \"bad\" } \n"
+ " ] \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_THROW(cclass = parseClientClassDef(cfg_text, AF_INET),
+ DhcpConfigError);
+}
+
+
+// Verifies that a valid list of client classes will parse.
+TEST_F(ClientClassDefListParserTest, simpleValidList) {
+ std::string cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"one\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"two\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"three\" \n"
+ " } \n"
+ "] \n";
+
+ // Parsing the list should succeed.
+ ClientClassDictionaryPtr dictionary;
+ ASSERT_NO_THROW(dictionary = parseClientClassDefList(cfg_text, AF_INET6));
+ ASSERT_TRUE(dictionary);
+
+ // We should have three classes in the dictionary.
+ EXPECT_EQ(3, dictionary->getClasses()->size());
+
+ // Make sure we can find all three.
+ ClientClassDefPtr cclass;
+ ASSERT_NO_THROW(cclass = dictionary->findClass("one"));
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("one", cclass->getName());
+
+ ASSERT_NO_THROW(cclass = dictionary->findClass("two"));
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("two", cclass->getName());
+
+ ASSERT_NO_THROW(cclass = dictionary->findClass("three"));
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("three", cclass->getName());
+
+ // For good measure, make sure we can't find a non-existent class.
+ ASSERT_NO_THROW(cclass = dictionary->findClass("bogus"));
+ EXPECT_FALSE(cclass);
+}
+
+// Verifies that class list containing a duplicate class entries, fails
+// to parse.
+TEST_F(ClientClassDefListParserTest, duplicateClass) {
+ std::string cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"one\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"two\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"two\" \n"
+ " } \n"
+ "] \n";
+
+ ClientClassDictionaryPtr dictionary;
+ ASSERT_THROW(dictionary = parseClientClassDefList(cfg_text, AF_INET),
+ DhcpConfigError);
+}
+
+// Test verifies that without any class specified, the fixed fields have their
+// default, empty value.
+// @todo same with AF_INET6
+TEST_F(ClientClassDefParserTest, noFixedFields) {
+
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"MICROSOFT\", \n"
+ " \"option-data\": [ \n"
+ " { \n"
+ " \"name\": \"domain-name-servers\", \n"
+ " \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET));
+
+ // We should find our class.
+ ASSERT_TRUE(cclass);
+
+ // And it should not have any fixed fields set
+ EXPECT_EQ(IOAddress("0.0.0.0"), cclass->getNextServer());
+ EXPECT_EQ(0, cclass->getSname().size());
+ EXPECT_EQ(0, cclass->getFilename().size());
+
+ // Nor option definitions
+ CfgOptionDefPtr cfg = cclass->getCfgOptionDef();
+ ASSERT_TRUE(cfg->getAll(DHCP4_OPTION_SPACE)->empty());
+}
+
+// Test verifies option-def for a bad option fails to parse.
+TEST_F(ClientClassDefParserTest, badOptionDef) {
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"MICROSOFT\", \n"
+ " \"option-def\": [ \n"
+ " { \n"
+ " \"name\": \"foo\", \n"
+ " \"code\": 222, \n"
+ " \"type\": \"uint32\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_THROW(cclass = parseClientClassDef(cfg_text, AF_INET),
+ DhcpConfigError);
+}
+
+// Test verifies option-def works for private options (224-254).
+TEST_F(ClientClassDefParserTest, privateOptionDef) {
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"MICROSOFT\", \n"
+ " \"option-def\": [ \n"
+ " { \n"
+ " \"name\": \"foo\", \n"
+ " \"code\": 232, \n"
+ " \"type\": \"uint32\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET));
+
+ // We should find our class.
+ ASSERT_TRUE(cclass);
+
+ // And the option definition.
+ CfgOptionDefPtr cfg = cclass->getCfgOptionDef();
+ ASSERT_TRUE(cfg);
+ EXPECT_TRUE(cfg->get(DHCP4_OPTION_SPACE, 232));
+ EXPECT_FALSE(cfg->get(DHCP6_OPTION_SPACE, 232));
+ EXPECT_FALSE(cfg->get(DHCP4_OPTION_SPACE, 233));
+}
+
+// Test verifies option-def works for option 43.
+TEST_F(ClientClassDefParserTest, option43Def) {
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"MICROSOFT\", \n"
+ " \"test\": \"option[60].text == 'MICROSOFT'\", \n"
+ " \"option-def\": [ \n"
+ " { \n"
+ " \"name\": \"vendor-encapsulated-options\", \n"
+ " \"code\": 43, \n"
+ " \"space\": \"dhcp4\", \n"
+ " \"type\": \"empty\", \n"
+ " \"encapsulate\": \"vsi\" \n"
+ " } \n"
+ " ], \n"
+ " \"option-data\": [ \n"
+ " { \n"
+ " \"name\": \"vendor-encapsulated-options\" \n"
+ " }, \n"
+ " { \n"
+ " \"code\": 1, \n"
+ " \"space\": \"vsi\", \n"
+ " \"csv-format\": false, \n"
+ " \"data\": \"C0000200\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET));
+
+ // We should find our class.
+ ASSERT_TRUE(cclass);
+
+ // And the option definition.
+ CfgOptionDefPtr cfg_def = cclass->getCfgOptionDef();
+ ASSERT_TRUE(cfg_def);
+ EXPECT_TRUE(cfg_def->get(DHCP4_OPTION_SPACE, 43));
+
+ // Verify the option data.
+ OptionDescriptor od = cclass->getCfgOption()->get(DHCP4_OPTION_SPACE, 43);
+ ASSERT_TRUE(od.option_);
+ EXPECT_EQ(43, od.option_->getType());
+ const OptionCollection& oc = od.option_->getOptions();
+ ASSERT_EQ(1, oc.size());
+ OptionPtr opt = od.option_->getOption(1);
+ ASSERT_TRUE(opt);
+ EXPECT_EQ(1, opt->getType());
+ ASSERT_EQ(4, opt->getData().size());
+ const uint8_t expected[4] = { 0xc0, 0x00, 0x02, 0x00 };
+ EXPECT_EQ(0, std::memcmp(expected, &opt->getData()[0], 4));
+}
+
+
+// Test verifies that it is possible to define next-server field and it
+// is actually set in the class properly.
+TEST_F(ClientClassDefParserTest, nextServer) {
+
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"MICROSOFT\", \n"
+ " \"next-server\": \"192.0.2.254\",\n"
+ " \"option-data\": [ \n"
+ " { \n"
+ " \"name\": \"domain-name-servers\", \n"
+ " \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET));
+
+ // We should find our class.
+ ASSERT_TRUE(cclass);
+
+ // And it should have next-server set, but everything else not set.
+ EXPECT_EQ(IOAddress("192.0.2.254"), cclass->getNextServer());
+ EXPECT_EQ(0, cclass->getSname().size());
+ EXPECT_EQ(0, cclass->getFilename().size());
+}
+
+// Test verifies that the parser rejects bogus next-server value.
+TEST_F(ClientClassDefParserTest, nextServerBogus) {
+
+ std::string bogus_v6 =
+ "{ \n"
+ " \"name\": \"MICROSOFT\", \n"
+ " \"next-server\": \"2001:db8::1\",\n"
+ " \"option-data\": [ \n"
+ " { \n"
+ " \"name\": \"domain-name-servers\", \n"
+ " \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+ std::string bogus_junk =
+ "{ \n"
+ " \"name\": \"MICROSOFT\", \n"
+ " \"next-server\": \"not-an-address\",\n"
+ " \"option-data\": [ \n"
+ " { \n"
+ " \"name\": \"domain-name-servers\", \n"
+ " \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+ std::string bogus_broadcast =
+ "{ \n"
+ " \"name\": \"MICROSOFT\", \n"
+ " \"next-server\": \"255.255.255.255\",\n"
+ " \"option-data\": [ \n"
+ " { \n"
+ " \"name\": \"domain-name-servers\", \n"
+ " \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+
+ EXPECT_THROW(parseClientClassDef(bogus_v6, AF_INET), DhcpConfigError);
+ EXPECT_THROW(parseClientClassDef(bogus_junk, AF_INET), DhcpConfigError);
+ EXPECT_THROW(parseClientClassDef(bogus_broadcast, AF_INET), DhcpConfigError);
+}
+
+// Test verifies that it is possible to define server-hostname field and it
+// is actually set in the class properly.
+TEST_F(ClientClassDefParserTest, serverName) {
+
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"MICROSOFT\", \n"
+ " \"server-hostname\": \"hal9000\",\n"
+ " \"option-data\": [ \n"
+ " { \n"
+ " \"name\": \"domain-name-servers\", \n"
+ " \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET));
+
+ // We should find our class.
+ ASSERT_TRUE(cclass);
+
+ // And it should not have any fixed fields set
+ std::string exp_sname("hal9000");
+
+ EXPECT_EQ(exp_sname, cclass->getSname());
+}
+
+// Test verifies that the parser rejects bogus server-hostname value.
+TEST_F(ClientClassDefParserTest, serverNameInvalid) {
+
+ std::string cfg_too_long =
+ "{ \n"
+ " \"name\": \"MICROSOFT\", \n"
+ " \"server-hostname\": \"1234567890123456789012345678901234567890"
+ "1234567890123456789012345\", \n"
+ " \"option-data\": [ \n"
+ " { \n"
+ " \"name\": \"domain-name-servers\", \n"
+ " \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+
+ EXPECT_THROW(parseClientClassDef(cfg_too_long, AF_INET), DhcpConfigError);
+}
+
+
+// Test verifies that it is possible to define boot-file-name field and it
+// is actually set in the class properly.
+TEST_F(ClientClassDefParserTest, filename) {
+
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"MICROSOFT\", \n"
+ " \"boot-file-name\": \"ipxe.efi\", \n"
+ " \"option-data\": [ \n"
+ " { \n"
+ " \"name\": \"domain-name-servers\", \n"
+ " \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET));
+
+ // We should find our class.
+ ASSERT_TRUE(cclass);
+
+ // And it should not have any fixed fields set
+ std::string exp_filename("ipxe.efi");
+ EXPECT_EQ(exp_filename, cclass->getFilename());
+}
+
+// Test verifies that the parser rejects bogus boot-file-name value.
+TEST_F(ClientClassDefParserTest, filenameBogus) {
+
+ // boot-file-name is allowed up to 128 bytes, this one is 129.
+ std::string cfg_too_long =
+ "{ \n"
+ " \"name\": \"MICROSOFT\", \n"
+ " \"boot-file-name\": \"1234567890123456789012345678901234567890"
+ "1234567890123456789012345678901234567890"
+ "1234567890123456789012345678901234567890"
+ "1234567890123456789012345678901234567890"
+ "1234567890123456789012345678901234567890"
+ "1234567890123456789012345678901234567890"
+ "123456789\", \n"
+ " \"option-data\": [ \n"
+ " { \n"
+ " \"name\": \"domain-name-servers\", \n"
+ " \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+ " } \n"
+ " ] \n"
+ "} \n";
+
+ EXPECT_THROW(parseClientClassDef(cfg_too_long, AF_INET), DhcpConfigError);
+}
+
+// Verifies that backward and built-in dependencies will parse.
+TEST_F(ClientClassDefListParserTest, dependentList) {
+ std::string cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"one\", \n"
+ " \"test\": \"member('VENDOR_CLASS_foo')\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"two\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"three\", \n"
+ " \"test\": \"member('two')\" \n"
+ " } \n"
+ "] \n";
+
+ // Parsing the list should succeed.
+ ClientClassDictionaryPtr dictionary;
+ ASSERT_NO_THROW(dictionary = parseClientClassDefList(cfg_text, AF_INET));
+ ASSERT_TRUE(dictionary);
+
+ // We should have three classes in the dictionary.
+ EXPECT_EQ(3, dictionary->getClasses()->size());
+
+ // Make sure we can find all three.
+ ClientClassDefPtr cclass;
+ ASSERT_NO_THROW(cclass = dictionary->findClass("one"));
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("one", cclass->getName());
+
+ ASSERT_NO_THROW(cclass = dictionary->findClass("two"));
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("two", cclass->getName());
+
+ ASSERT_NO_THROW(cclass = dictionary->findClass("three"));
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("three", cclass->getName());
+}
+
+// Verifies that not defined dependencies will not parse.
+TEST_F(ClientClassDefListParserTest, dependentNotDefined) {
+ std::string cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"one\", \n"
+ " \"test\": \"member('foo')\" \n"
+ " } \n"
+ "] \n";
+
+ EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET6), DhcpConfigError);
+}
+
+// Verifies that error is not reported when a class references another
+// not defined class, but dependency checking is disabled.
+TEST_F(ClientClassDefListParserTest, dependencyCheckingDisabled) {
+ std::string cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"one\", \n"
+ " \"test\": \"member('foo')\" \n"
+ " } \n"
+ "] \n";
+ try {
+ parseClientClassDefList(cfg_text, AF_INET6, false);
+ } catch ( const std::exception& ex) {
+ std::cout << ex.what() << std::endl;
+ }
+ EXPECT_NO_THROW(parseClientClassDefList(cfg_text, AF_INET6, false));
+}
+
+// Verifies that forward dependencies will not parse.
+TEST_F(ClientClassDefListParserTest, dependentForwardError) {
+ std::string cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"one\", \n"
+ " \"test\": \"member('foo')\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"foo\" \n"
+ " } \n"
+ "] \n";
+
+ EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET6), DhcpConfigError);
+}
+
+// Verifies that backward dependencies will parse.
+TEST_F(ClientClassDefListParserTest, dependentBackward) {
+ std::string cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"foo\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"one\", \n"
+ " \"test\": \"member('foo')\" \n"
+ " } \n"
+ "] \n";
+
+ EXPECT_NO_THROW(parseClientClassDefList(cfg_text, AF_INET6));
+}
+
+// Verifies that the depend on known flag is correctly handled.
+TEST_F(ClientClassDefListParserTest, dependOnKnown) {
+ std::string cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"alpha\", \n"
+ " \"test\": \"member('ALL')\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"beta\", \n"
+ " \"test\": \"member('alpha')\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"gamma\", \n"
+ " \"test\": \"member('KNOWN') and member('alpha')\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"delta\", \n"
+ " \"test\": \"member('beta') and member('gamma')\" \n"
+ " }, \n"
+ " { \n"
+ " \"name\": \"zeta\", \n"
+ " \"test\": \"not member('UNKNOWN') and member('alpha')\" \n"
+ " } \n"
+ "] \n";
+
+ // Parsing the list should succeed.
+ ClientClassDictionaryPtr dictionary;
+ EXPECT_NO_THROW(dictionary = parseClientClassDefList(cfg_text, AF_INET6));
+ ASSERT_TRUE(dictionary);
+
+ // We should have five classes in the dictionary.
+ EXPECT_EQ(5, dictionary->getClasses()->size());
+
+ // Check alpha.
+ ClientClassDefPtr cclass;
+ ASSERT_NO_THROW(cclass = dictionary->findClass("alpha"));
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("alpha", cclass->getName());
+ EXPECT_FALSE(cclass->getDependOnKnown());
+
+ // Check beta.
+ ASSERT_NO_THROW(cclass = dictionary->findClass("beta"));
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("beta", cclass->getName());
+ EXPECT_FALSE(cclass->getDependOnKnown());
+
+ // Check gamma which directly depends on KNOWN.
+ ASSERT_NO_THROW(cclass = dictionary->findClass("gamma"));
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("gamma", cclass->getName());
+ EXPECT_TRUE(cclass->getDependOnKnown());
+
+ // Check delta which indirectly depends on KNOWN.
+ ASSERT_NO_THROW(cclass = dictionary->findClass("delta"));
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("delta", cclass->getName());
+ EXPECT_TRUE(cclass->getDependOnKnown());
+
+ // Check that zeta which directly depends on UNKNOWN.
+ // (and yes I know that I skipped epsilon)
+ ASSERT_NO_THROW(cclass = dictionary->findClass("zeta"));
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("zeta", cclass->getName());
+ EXPECT_TRUE(cclass->getDependOnKnown());
+}
+
+// Verifies that a built-in class can't be required or evaluated.
+TEST_F(ClientClassDefListParserTest, builtinCheckError) {
+ std::string cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"ALL\" \n"
+ " } \n"
+ "] \n";
+
+ EXPECT_NO_THROW(parseClientClassDefList(cfg_text, AF_INET6));
+
+ cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"ALL\", \n"
+ " \"only-if-required\": true \n"
+ " } \n"
+ "] \n";
+
+ EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET), DhcpConfigError);
+
+ cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"ALL\", \n"
+ " \"test\": \"'aa' == 'aa'\" \n"
+ " } \n"
+ "] \n";
+
+ EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET6), DhcpConfigError);
+
+ cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"KNOWN\", \n"
+ " \"only-if-required\": true \n"
+ " } \n"
+ "] \n";
+
+ EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET), DhcpConfigError);
+
+ cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"KNOWN\", \n"
+ " \"test\": \"'aa' == 'aa'\" \n"
+ " } \n"
+ "] \n";
+
+ EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET6), DhcpConfigError);
+
+ cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"UNKNOWN\", \n"
+ " \"only-if-required\": true \n"
+ " } \n"
+ "] \n";
+
+ EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET), DhcpConfigError);
+
+ cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"UNKNOWN\", \n"
+ " \"test\": \"'aa' == 'aa'\" \n"
+ " } \n"
+ "] \n";
+
+ EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET6), DhcpConfigError);
+}
+
+// Verifies that the special DROP class can't be required.
+TEST_F(ClientClassDefListParserTest, dropCheckError) {
+ std::string cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"DROP\", \n"
+ " \"test\": \"option[123].text == 'abc'\" \n"
+ " } \n"
+ "] \n";
+
+ EXPECT_NO_THROW(parseClientClassDefList(cfg_text, AF_INET6));
+
+ cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"DROP\", \n"
+ " \"only-if-required\": true \n"
+ " } \n"
+ "] \n";
+
+ EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET), DhcpConfigError);
+
+ // This constraint was relaxed in #1815.
+ cfg_text =
+ "[ \n"
+ " { \n"
+ " \"name\": \"DROP\", \n"
+ " \"test\": \"member('KNOWN')\" \n"
+ " } \n"
+ "] \n";
+
+ EXPECT_NO_THROW(parseClientClassDefList(cfg_text, AF_INET6));
+}
+
+// Verify the ability to configure valid lifetime triplet.
+TEST_F(ClientClassDefParserTest, validLifetimeTests) {
+
+ struct Scenario {
+ std::string desc_;
+ std::string cfg_txt_;
+ Triplet<uint32_t> exp_triplet_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "unspecified",
+ "",
+ Triplet<uint32_t>()
+ },
+ {
+ "valid only",
+ "\"valid-lifetime\": 100",
+ Triplet<uint32_t>(100)
+ },
+ {
+ "min only",
+ "\"min-valid-lifetime\": 50",
+ Triplet<uint32_t>(50, 50, 50)
+ },
+ {
+ "max only",
+ "\"max-valid-lifetime\": 75",
+ Triplet<uint32_t>(75, 75, 75)
+ },
+ {
+ "all three",
+ "\"min-valid-lifetime\": 25, \"valid-lifetime\": 50, \"max-valid-lifetime\": 75",
+ Triplet<uint32_t>(25, 50, 75)
+ }
+ };
+
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.desc_); {
+ std::stringstream oss;
+ oss << "{ \"name\": \"foo\"";
+ if (!scenario.cfg_txt_.empty()) {
+ oss << ",\n" << scenario.cfg_txt_;
+ }
+ oss << "\n}\n";
+
+ ClientClassDefPtr class_def;
+ ASSERT_NO_THROW_LOG(class_def = parseClientClassDef(oss.str(), AF_INET));
+ ASSERT_TRUE(class_def);
+ if (scenario.exp_triplet_.unspecified()) {
+ EXPECT_TRUE(class_def->getValid().unspecified());
+ } else {
+ EXPECT_EQ(class_def->getValid(), scenario.exp_triplet_);
+ EXPECT_EQ(class_def->getValid().getMin(), scenario.exp_triplet_.getMin());
+ EXPECT_EQ(class_def->getValid().get(), scenario.exp_triplet_.get());
+ EXPECT_EQ(class_def->getValid().getMax(), scenario.exp_triplet_.getMax());
+ }
+ }
+ }
+}
+
+// Verify the ability to configure lease preferred lifetime triplet.
+TEST_F(ClientClassDefParserTest, preferredLifetimeTests) {
+
+ struct Scenario {
+ std::string desc_;
+ std::string cfg_txt_;
+ Triplet<uint32_t> exp_triplet_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "unspecified",
+ "",
+ Triplet<uint32_t>()
+ },
+ {
+ "preferred only",
+ "\"preferred-lifetime\": 100",
+ Triplet<uint32_t>(100)
+ },
+ {
+ "min only",
+ "\"min-preferred-lifetime\": 50",
+ Triplet<uint32_t>(50, 50, 50)
+ },
+ {
+ "max only",
+ "\"max-preferred-lifetime\": 75",
+ Triplet<uint32_t>(75, 75, 75)
+ },
+ {
+ "all three",
+ "\"min-preferred-lifetime\": 25,"
+ "\"preferred-lifetime\": 50,"
+ "\"max-preferred-lifetime\": 75",
+ Triplet<uint32_t>(25, 50, 75)
+ }
+ };
+
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.desc_); {
+ std::stringstream oss;
+ oss << "{ \"name\": \"foo\"";
+ if (!scenario.cfg_txt_.empty()) {
+ oss << ",\n" << scenario.cfg_txt_;
+ }
+ oss << "\n}\n";
+
+ ClientClassDefPtr class_def;
+ ASSERT_NO_THROW_LOG(class_def = parseClientClassDef(oss.str(), AF_INET6));
+ ASSERT_TRUE(class_def);
+ if (scenario.exp_triplet_.unspecified()) {
+ EXPECT_TRUE(class_def->getPreferred().unspecified());
+ } else {
+ EXPECT_EQ(class_def->getPreferred(), scenario.exp_triplet_);
+ EXPECT_EQ(class_def->getPreferred().getMin(), scenario.exp_triplet_.getMin());
+ EXPECT_EQ(class_def->getPreferred().get(), scenario.exp_triplet_.get());
+ EXPECT_EQ(class_def->getPreferred().getMax(), scenario.exp_triplet_.getMax());
+ }
+ }
+ }
+}
+
+// Verifies that an invalid user-context fails to parse.
+TEST_F(ClientClassDefParserTest, invalidUserContext) {
+ std::string cfg_text =
+ "{ \n"
+ " \"name\": \"one\", \n"
+ " \"user-context\": \"i am not a map\" \n"
+ "} \n";
+
+ ClientClassDefPtr cclass;
+ ASSERT_THROW_MSG(cclass = parseClientClassDef(cfg_text, AF_INET),
+ DhcpConfigError, "User context has to be a map (<string>:3:20)");
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/client_class_def_unittest.cc b/src/lib/dhcpsrv/tests/client_class_def_unittest.cc
new file mode 100644
index 0000000..21c2e07
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/client_class_def_unittest.cc
@@ -0,0 +1,806 @@
+// Copyright (C) 2015-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <cc/data.h>
+#include <dhcpsrv/client_class_def.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcp/libdhcp++.h>
+#include <dhcp/option_space.h>
+#include <testutils/test_to_element.h>
+#include <exceptions/exceptions.h>
+#include <boost/scoped_ptr.hpp>
+#include <asiolink/io_address.h>
+
+#include <boost/make_shared.hpp>
+#include <gtest/gtest.h>
+
+/// @file client_class_def_unittest.cc Unit tests for client class storage
+/// classes.
+
+using namespace std;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::util;
+using namespace isc::asiolink;
+using namespace isc::test;
+using namespace isc;
+
+namespace {
+
+// Tests basic construction of ClientClassDef
+TEST(ClientClassDef, construction) {
+ boost::scoped_ptr<ClientClassDef> cclass;
+
+ std::string name = "class1";
+ ExpressionPtr expr;
+ CfgOptionPtr cfg_option;
+
+ // Classes cannot have blank names
+ ASSERT_THROW(cclass.reset(new ClientClassDef("", expr, cfg_option)),
+ BadValue);
+
+ // Verify we can create a class with a name, expression, and no cfg_option
+ ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr)));
+ EXPECT_EQ(name, cclass->getName());
+ ASSERT_FALSE(cclass->getMatchExpr());
+ EXPECT_FALSE(cclass->getCfgOptionDef());
+
+ // Verify we get an empty collection of cfg_option
+ cfg_option = cclass->getCfgOption();
+ ASSERT_TRUE(cfg_option);
+ EXPECT_TRUE(cfg_option->empty());
+
+ // Verify we don't depend on something.
+ EXPECT_FALSE(cclass->dependOnClass("foobar"));
+ EXPECT_FALSE(cclass->dependOnClass(""));
+}
+
+// Test that client class is copied using the copy constructor.
+TEST(ClientClassDef, copyConstruction) {
+ auto expr = boost::make_shared<Expression>();
+
+ auto cfg_option = boost::make_shared<CfgOption>();
+ auto option = boost::make_shared<Option>(Option::V6, 1024);
+ cfg_option->add(option, false, DHCP6_OPTION_SPACE);
+
+ auto option_def = boost::make_shared<OptionDefinition>("foo", 1024, "dhcp6", "empty");
+ CfgOptionDefPtr cfg_option_def = boost::make_shared<CfgOptionDef>();
+ cfg_option_def->add(option_def);
+
+ boost::scoped_ptr<ClientClassDef> cclass;
+ ASSERT_NO_THROW(cclass.reset(new ClientClassDef("class1", expr, cfg_option)));
+ cclass->setId(123);
+ cclass->setContext(data::Element::create("my-context"));
+ cclass->setCfgOptionDef(cfg_option_def);
+ cclass->setTest("member('KNOWN')");
+ cclass->setRequired(true);
+ cclass->setDependOnKnown(true);
+ cclass->setNextServer(IOAddress("1.2.3.4"));
+ cclass->setSname("ufo");
+ cclass->setFilename("ufo.efi");
+ cclass->setValid(Triplet<uint32_t>(10, 20, 30));
+ cclass->setPreferred(Triplet<uint32_t>(11, 21, 31));
+
+ // Copy the client class.
+ boost::scoped_ptr<ClientClassDef> cclass_copy;
+ ASSERT_NO_THROW(cclass_copy.reset(new ClientClassDef(*cclass)));
+
+ // Ensure that class data was copied.
+ EXPECT_EQ(cclass->getName(), cclass_copy->getName());
+ EXPECT_EQ(cclass->getId(), cclass_copy->getId());
+ ASSERT_TRUE(cclass_copy->getContext());
+ ASSERT_EQ(data::Element::string, cclass_copy->getContext()->getType());
+ EXPECT_EQ("my-context", cclass_copy->getContext()->stringValue());
+ ASSERT_TRUE(cclass->getMatchExpr());
+ EXPECT_NE(cclass_copy->getMatchExpr(), cclass->getMatchExpr());
+ EXPECT_EQ(cclass->getTest(), cclass_copy->getTest());
+ EXPECT_EQ(cclass->getRequired(), cclass_copy->getRequired());
+ EXPECT_EQ(cclass->getDependOnKnown(), cclass_copy->getDependOnKnown());
+ EXPECT_EQ(cclass->getNextServer().toText(), cclass_copy->getNextServer().toText());
+ EXPECT_EQ(cclass->getSname(), cclass_copy->getSname());
+ EXPECT_EQ(cclass->getFilename(), cclass_copy->getFilename());
+ EXPECT_EQ(cclass->getValid().get(), cclass_copy->getValid().get());
+ EXPECT_EQ(cclass->getValid().getMin(), cclass_copy->getValid().getMin());
+ EXPECT_EQ(cclass->getValid().getMax(), cclass_copy->getValid().getMax());
+ EXPECT_EQ(cclass->getPreferred().get(), cclass_copy->getPreferred().get());
+ EXPECT_EQ(cclass->getPreferred().getMin(), cclass_copy->getPreferred().getMin());
+ EXPECT_EQ(cclass->getPreferred().getMax(), cclass_copy->getPreferred().getMax());
+
+ // Ensure that the option was copied into a new structure.
+ ASSERT_TRUE(cclass_copy->getCfgOption());
+ EXPECT_NE(cclass_copy->getCfgOption(), cclass->getCfgOption());
+ EXPECT_TRUE(cclass_copy->getCfgOption()->get("dhcp6", 1024).option_);
+
+ // Ensure that the option definition was copied into a new structure.
+ ASSERT_TRUE(cclass_copy->getCfgOptionDef());
+ EXPECT_NE(cclass_copy->getCfgOptionDef(), cclass->getCfgOptionDef());
+ EXPECT_TRUE(cclass_copy->getCfgOptionDef()->get("dhcp6", 1024));
+}
+
+// Tests options operations. Note we just do the basics
+// as CfgOption is heavily tested elsewhere.
+TEST(ClientClassDef, cfgOptionBasics) {
+ boost::scoped_ptr<ClientClassDef> cclass;
+
+ std::string name = "class1";
+ ExpressionPtr expr;
+ CfgOptionPtr test_options;
+ CfgOptionPtr class_options;
+ OptionPtr opt;
+
+ // First construct the class with empty option pointer
+ ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr, test_options)));
+
+ // We should get back a collection with no entries,
+ // not an empty collection pointer
+ class_options = cclass->getCfgOption();
+ ASSERT_TRUE(class_options);
+
+ // Create an option container and add some options
+ OptionPtr option;
+ test_options.reset(new CfgOption());
+ option.reset(new Option(Option::V4, 17, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(test_options->add(option, false, DHCP4_OPTION_SPACE));
+
+ option.reset(new Option(Option::V6, 101, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(test_options->add(option, false, "isc"));
+
+ option.reset(new Option(Option::V6, 100, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(test_options->add(option, false, DHCP6_OPTION_SPACE));
+
+ // Now remake the client class with cfg_option
+ ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr, test_options)));
+ class_options = cclass->getCfgOption();
+ ASSERT_TRUE(class_options);
+
+ // Now make sure we can find all the options
+ OptionDescriptor opt_desc = class_options->get(DHCP4_OPTION_SPACE,17);
+ ASSERT_TRUE(opt_desc.option_);
+ EXPECT_EQ(17, opt_desc.option_->getType());
+
+ opt_desc = class_options->get("isc",101);
+ ASSERT_TRUE(opt_desc.option_);
+ EXPECT_EQ(101, opt_desc.option_->getType());
+
+ opt_desc = class_options->get(DHCP6_OPTION_SPACE,100);
+ ASSERT_TRUE(opt_desc.option_);
+ EXPECT_EQ(100, opt_desc.option_->getType());
+}
+
+// Verifies copy constructor and equality tools (methods/operators)
+TEST(ClientClassDef, copyAndEquality) {
+
+ boost::scoped_ptr<ClientClassDef> cclass;
+ ExpressionPtr expr;
+ CfgOptionPtr test_options;
+ OptionPtr opt;
+
+ // Make an expression
+ expr.reset(new Expression());
+ TokenPtr token(new TokenString("boo"));
+ expr->push_back(token);
+
+ // Create an option container with an option
+ OptionPtr option;
+ test_options.reset(new CfgOption());
+ option.reset(new Option(Option::V4, 17, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(test_options->add(option, false, DHCP4_OPTION_SPACE));
+
+ // Now remake the client class with cfg_option
+ ASSERT_NO_THROW(cclass.reset(new ClientClassDef("class_one", expr,
+ test_options)));
+
+ // Now lets make a copy of it.
+ boost::scoped_ptr<ClientClassDef> cclass2;
+ ASSERT_NO_THROW(cclass2.reset(new ClientClassDef(*cclass)));
+
+ // The allocated Expression pointers should not match
+ EXPECT_TRUE(cclass->getMatchExpr().get() !=
+ cclass2->getMatchExpr().get());
+
+ // The allocated CfgOption pointers should not match
+ EXPECT_TRUE(cclass->getCfgOption().get() !=
+ cclass2->getCfgOption().get());
+
+ // Verify the equality tools reflect that the classes are equal.
+ EXPECT_TRUE(cclass->equals(*cclass2));
+ EXPECT_TRUE(*cclass == *cclass2);
+ EXPECT_FALSE(*cclass != *cclass2);
+
+ // Verify the required flag is enough to make classes not equal.
+ EXPECT_FALSE(cclass->getRequired());
+ cclass2->setRequired(true);
+ EXPECT_TRUE(cclass2->getRequired());
+ EXPECT_FALSE(*cclass == *cclass2);
+ EXPECT_TRUE(*cclass != *cclass2);
+ cclass2->setRequired(false);
+ EXPECT_TRUE(*cclass == *cclass2);
+
+ // Verify the depend on known flag is enough to make classes not equal.
+ EXPECT_FALSE(cclass->getDependOnKnown());
+ cclass2->setDependOnKnown(true);
+ EXPECT_TRUE(cclass2->getDependOnKnown());
+ EXPECT_FALSE(*cclass == *cclass2);
+ EXPECT_TRUE(*cclass != *cclass2);
+
+ // Make a class that differs from the first class only by name and
+ // verify that the equality tools reflect that the classes are not equal.
+ ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_two", expr,
+ test_options)));
+ EXPECT_FALSE(cclass->equals(*cclass2));
+ EXPECT_FALSE(*cclass == *cclass2);
+ EXPECT_TRUE(*cclass != *cclass2);
+
+ // Make a class with the same name and options, but no expression
+ // verify that the equality tools reflect that the classes are not equal.
+ expr.reset();
+ ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_one", expr,
+ test_options)));
+ EXPECT_FALSE(cclass->equals(*cclass2));
+ EXPECT_FALSE(*cclass == *cclass2);
+ EXPECT_TRUE(*cclass != *cclass2);
+
+ // Make a class with the same name and options, but different expression,
+ // verify that the equality tools reflect that the classes are not equal.
+ expr.reset(new Expression());
+ token.reset(new TokenString("yah"));
+ expr->push_back(token);
+ ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_one", expr,
+ test_options)));
+ EXPECT_FALSE(cclass->equals(*cclass2));
+ EXPECT_FALSE(*cclass == *cclass2);
+ EXPECT_TRUE(*cclass != *cclass2);
+
+ // Make a class that with same name, expression and options, but
+ // different option definitions, verify that the equality tools reflect
+ // that the equality tools reflect that the classes are not equal.
+ ASSERT_NO_THROW(cclass2.reset(new ClientClassDef(*cclass)));
+ EXPECT_TRUE(cclass->equals(*cclass2));
+ OptionDefinitionPtr def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ EXPECT_FALSE(def);
+ def = LibDHCP::getLastResortOptionDef(DHCP4_OPTION_SPACE, DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ EXPECT_TRUE(def);
+ CfgOptionDefPtr cfg(new CfgOptionDef());
+ ASSERT_NO_THROW(cfg->add(def));
+ cclass2->setCfgOptionDef(cfg);
+ EXPECT_FALSE(cclass->equals(*cclass2));
+ EXPECT_FALSE(*cclass == *cclass2);
+ EXPECT_TRUE(*cclass != *cclass2);
+
+ // Make a class with same name and expression, but no options
+ // verify that the equality tools reflect that the classes are not equal.
+ test_options.reset(new CfgOption());
+ ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_one", expr,
+ test_options)));
+ EXPECT_FALSE(cclass->equals(*cclass2));
+ EXPECT_FALSE(*cclass == *cclass2);
+ EXPECT_TRUE(*cclass != *cclass2);
+
+ // Make a class that with same name and expression, but different options
+ // verify that the equality tools reflect that the classes are not equal.
+ option.reset(new Option(Option::V4, 20, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(test_options->add(option, false, DHCP4_OPTION_SPACE));
+ ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_one", expr,
+ test_options)));
+ EXPECT_FALSE(cclass->equals(*cclass2));
+ EXPECT_FALSE(*cclass == *cclass2);
+ EXPECT_TRUE(*cclass != *cclass2);
+}
+
+// Tests dependency.
+TEST(ClientClassDef, dependency) {
+ boost::scoped_ptr<ClientClassDef> cclass;
+
+ ExpressionPtr expr;
+
+ // Make an expression
+ expr.reset(new Expression());
+ TokenPtr token(new TokenMember("foo"));
+ expr->push_back(token);
+
+ ASSERT_NO_THROW(cclass.reset(new ClientClassDef("class", expr)));
+ EXPECT_TRUE(cclass->dependOnClass("foo"));
+ EXPECT_FALSE(cclass->dependOnClass("bar"));
+}
+
+
+// Tests the basic operation of ClientClassDictionary
+// This includes adding, finding, and removing classes
+TEST(ClientClassDictionary, basics) {
+ ClientClassDictionaryPtr dictionary;
+ ClientClassDefPtr cclass;
+ ExpressionPtr expr;
+ CfgOptionPtr cfg_option;
+
+ // Verify constructor doesn't throw
+ ASSERT_NO_THROW(dictionary.reset(new ClientClassDictionary()));
+
+ // Verify we can fetch a pointer the list of classes and
+ // that we start with no classes defined
+ const ClientClassDefListPtr classes = dictionary->getClasses();
+ ASSERT_TRUE(classes);
+ EXPECT_EQ(0, classes->size());
+ EXPECT_TRUE(classes->empty());
+
+ // Verify that we can add classes with both addClass variants
+ // First addClass(name, expression, cfg_option)
+ ASSERT_NO_THROW(dictionary->addClass("cc1", expr, "", false,
+ false, cfg_option));
+ ASSERT_NO_THROW(dictionary->addClass("cc2", expr, "", false,
+ false, cfg_option));
+
+ // Verify duplicate add attempt throws
+ ASSERT_THROW(dictionary->addClass("cc2", expr, "", false,
+ false, cfg_option),
+ DuplicateClientClassDef);
+
+ // Verify that you cannot add a class with no name.
+ ASSERT_THROW(dictionary->addClass("", expr, "", false,
+ false, cfg_option),
+ BadValue);
+
+ // Now with addClass(class pointer)
+ ASSERT_NO_THROW(cclass.reset(new ClientClassDef("cc3", expr, cfg_option)));
+ ASSERT_NO_THROW(dictionary->addClass(cclass));
+
+ // Verify duplicate add attempt throws
+ ASSERT_THROW(dictionary->addClass(cclass), DuplicateClientClassDef);
+
+ // Verify that you cannot add empty class pointer
+ cclass.reset();
+ ASSERT_THROW(dictionary->addClass(cclass), BadValue);
+
+ // Map should show 3 entries.
+ EXPECT_EQ(3, classes->size());
+ EXPECT_FALSE(classes->empty());
+
+ // Removing client class by id of 0 should be no-op.
+ ASSERT_NO_THROW(dictionary->removeClass(0));
+ EXPECT_EQ(3, classes->size());
+ EXPECT_FALSE(classes->empty());
+
+ // Verify we can find them all.
+ ASSERT_NO_THROW(cclass = dictionary->findClass("cc1"));
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("cc1", cclass->getName());
+ cclass->setId(1);
+
+ ASSERT_NO_THROW(cclass = dictionary->findClass("cc2"));
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("cc2", cclass->getName());
+ cclass->setId(2);
+
+ ASSERT_NO_THROW(cclass = dictionary->findClass("cc3"));
+ ASSERT_TRUE(cclass);
+ EXPECT_EQ("cc3", cclass->getName());
+ cclass->setId(3);
+
+ // Verify the looking for non-existing returns empty pointer
+ ASSERT_NO_THROW(cclass = dictionary->findClass("bogus"));
+ EXPECT_FALSE(cclass);
+
+ // Verify that we can remove a class
+ ASSERT_NO_THROW(dictionary->removeClass("cc3"));
+ EXPECT_EQ(2, classes->size());
+ EXPECT_FALSE(classes->empty());
+
+ // Shouldn't be able to find anymore
+ ASSERT_NO_THROW(cclass = dictionary->findClass("cc3"));
+ EXPECT_FALSE(cclass);
+
+ // Verify that we can attempt to remove a non-existing class
+ // without harm.
+ ASSERT_NO_THROW(dictionary->removeClass("cc3"));
+ EXPECT_EQ(2, classes->size());
+ EXPECT_FALSE(classes->empty());
+
+ // Verify that we can remove client class by id.
+ ASSERT_NO_THROW(dictionary->removeClass(2));
+ EXPECT_EQ(1, classes->size());
+ EXPECT_FALSE(classes->empty());
+ ASSERT_NO_THROW(cclass = dictionary->findClass("cc2"));
+ EXPECT_FALSE(cclass);
+}
+
+// Verifies copy constructor and equality tools (methods/operators)
+TEST(ClientClassDictionary, copyAndEquality) {
+ ClientClassDictionaryPtr dictionary;
+ ClientClassDictionaryPtr dictionary2;
+ ClientClassDefPtr cclass;
+ ExpressionPtr expr;
+ CfgOptionPtr options;
+
+ dictionary.reset(new ClientClassDictionary());
+ ASSERT_NO_THROW(dictionary->addClass("one", expr, "", false,
+ false, options));
+ ASSERT_NO_THROW(dictionary->addClass("two", expr, "", false,
+ false, options));
+ ASSERT_NO_THROW(dictionary->addClass("three", expr, "", false,
+ false, options));
+
+ // Copy constructor should succeed.
+ ASSERT_NO_THROW(dictionary2.reset(new ClientClassDictionary(*dictionary)));
+
+ // Allocated class list pointers should not be equal
+ EXPECT_NE(dictionary->getClasses().get(), dictionary2->getClasses().get());
+
+ // Equality tools should reflect that the dictionaries are equal.
+ EXPECT_TRUE(dictionary->equals(*dictionary2));
+ EXPECT_TRUE(*dictionary == *dictionary2);
+ EXPECT_FALSE(*dictionary != *dictionary2);
+
+ // Remove a class from dictionary2.
+ ASSERT_NO_THROW(dictionary2->removeClass("two"));
+
+ // Equality tools should reflect that the dictionaries are not equal.
+ EXPECT_FALSE(dictionary->equals(*dictionary2));
+ EXPECT_FALSE(*dictionary == *dictionary2);
+ EXPECT_TRUE(*dictionary != *dictionary2);
+
+ // Create an empty dictionary.
+ dictionary2.reset(new ClientClassDictionary());
+
+ // Equality tools should reflect that the dictionaries are not equal.
+ EXPECT_FALSE(dictionary->equals(*dictionary2));
+ EXPECT_FALSE(*dictionary == *dictionary2);
+ EXPECT_TRUE(*dictionary != *dictionary2);
+}
+
+// Verify that client class dictionaries are deep-copied.
+TEST(ClientClassDictionary, copy) {
+ ClientClassDictionary dictionary;
+ ExpressionPtr expr;
+ CfgOptionPtr options;
+
+ // Get a client class dictionary and fill it.
+ ASSERT_NO_THROW(dictionary.addClass("one", expr, "", false,
+ false, options));
+ ASSERT_NO_THROW(dictionary.addClass("two", expr, "", false,
+ false, options));
+ ASSERT_NO_THROW(dictionary.addClass("three", expr, "", false,
+ false, options));
+
+ // Make a copy with a copy constructor. Expect it to be a deep copy.
+ ClientClassDictionary dictionary_copy(dictionary);
+ ASSERT_NO_THROW(dictionary.removeClass("one"));
+ ASSERT_NO_THROW(dictionary.removeClass("two"));
+ ASSERT_NO_THROW(dictionary.removeClass("three"));
+ EXPECT_TRUE(dictionary.empty());
+ EXPECT_FALSE(dictionary_copy.empty());
+
+ // Refill the client class dictionary.
+ ASSERT_NO_THROW(dictionary.addClass("one", expr, "", false,
+ false, options));
+ ASSERT_NO_THROW(dictionary.addClass("two", expr, "", false,
+ false, options));
+ ASSERT_NO_THROW(dictionary.addClass("three", expr, "", false,
+ false, options));
+
+ // Make a copy with operator=. Expect it to be a deep copy.
+ dictionary_copy = dictionary;
+ ASSERT_NO_THROW(dictionary.removeClass("one"));
+ ASSERT_NO_THROW(dictionary.removeClass("two"));
+ ASSERT_NO_THROW(dictionary.removeClass("three"));
+ EXPECT_TRUE(dictionary.empty());
+ EXPECT_FALSE(dictionary_copy.empty());
+}
+
+// Tests dependency.
+TEST(ClientClassDictionary, dependency) {
+ ClientClassDictionaryPtr dictionary(new ClientClassDictionary());
+ ExpressionPtr expr;
+ CfgOptionPtr cfg_option;
+
+ // Make an expression depending on forward class.
+ ExpressionPtr expr1;
+ expr1.reset(new Expression());
+ TokenPtr token1(new TokenMember("cc2"));
+ expr1->push_back(token1);
+
+ ASSERT_NO_THROW(dictionary->addClass("cc1", expr1, "", false,
+ false, cfg_option));
+
+ // Make an expression depending on first class.
+ ExpressionPtr expr2;
+ expr2.reset(new Expression());
+ TokenPtr token2(new TokenMember("cc1"));
+ expr2->push_back(token2);
+
+ ASSERT_NO_THROW(dictionary->addClass("cc2", expr2, "", false,
+ false, cfg_option));
+
+ // Make expression with dependency.
+ ASSERT_NO_THROW(dictionary->addClass("cc3", expr, "", false,
+ false, cfg_option));
+
+ ExpressionPtr expr3;
+ expr3.reset(new Expression());
+ TokenPtr token3(new TokenMember("cc3"));
+ expr3->push_back(token3);
+
+ ASSERT_NO_THROW(dictionary->addClass("cc4", expr3, "", false,
+ false, cfg_option));
+
+ // Not matching dependency does not match.
+ string depend;
+ EXPECT_FALSE(dictionary->dependOnClass("foobar", depend));
+ EXPECT_TRUE(depend.empty());
+
+ // Forward dependency is ignored.
+ depend = "";
+ EXPECT_FALSE(dictionary->dependOnClass("cc2", depend));
+ EXPECT_TRUE(depend.empty());
+
+ // Backward dependency is detected.
+ depend = "";
+ EXPECT_TRUE(dictionary->dependOnClass("cc3", depend));
+ EXPECT_EQ("cc4", depend);
+}
+
+// Tests that match expressions are set for all client classes in the
+// dictionary.
+TEST(ClientClassDictionary, initMatchExpr) {
+ ClientClassDictionaryPtr dictionary(new ClientClassDictionary());
+ ExpressionPtr expr;
+ CfgOptionPtr cfg_option;
+
+ // Add several classes.
+ ASSERT_NO_THROW(dictionary->addClass("foo", expr, "", false,
+ false, cfg_option));
+ ASSERT_NO_THROW(dictionary->addClass("bar", expr, "member('KNOWN') or member('foo')", false,
+ false, cfg_option));
+ ASSERT_NO_THROW(dictionary->addClass("baz", expr, "substring(option[61].hex,0,3) == 'foo'", false,
+ false, cfg_option));
+
+ // Create match expressions for all of them.
+ ASSERT_NO_THROW(dictionary->initMatchExpr(AF_INET));
+
+ // Ensure that the expressions were created only if 'test' is not empty.
+ auto classes = *(dictionary->getClasses());
+ EXPECT_FALSE(classes[0]->getMatchExpr());
+
+ EXPECT_TRUE(classes[1]->getMatchExpr());
+ EXPECT_EQ(3, classes[1]->getMatchExpr()->size());
+
+ EXPECT_TRUE(classes[2]->getMatchExpr());
+ EXPECT_EQ(6, classes[2]->getMatchExpr()->size());
+}
+
+// Tests that an error is returned when any of the test expressions is
+// invalid, and that no expressions are initialized if there is an error
+// for a single expression.
+TEST(ClientClassDictionary, initMatchExprError) {
+ ClientClassDictionaryPtr dictionary(new ClientClassDictionary());
+ ExpressionPtr expr;
+ CfgOptionPtr cfg_option;
+
+ // Add several classes. One of them has invalid test expression.
+ ASSERT_NO_THROW(dictionary->addClass("foo", expr, "member('KNOWN')", false,
+ false, cfg_option));
+ ASSERT_NO_THROW(dictionary->addClass("bar", expr, "wrong expression", false,
+ false, cfg_option));
+ ASSERT_NO_THROW(dictionary->addClass("baz", expr, "substring(option[61].hex,0,3) == 'foo'", false,
+ false, cfg_option));
+
+ // An attempt to initialize match expressions should fail because the
+ // test expression for the second class is invalid.
+ ASSERT_THROW(dictionary->initMatchExpr(AF_INET), std::exception);
+
+ // Ensure that no classes have their match expressions modified.
+ for (auto c : (*dictionary->getClasses())) {
+ EXPECT_FALSE(c->getMatchExpr());
+ }
+}
+
+// Tests the default constructor regarding fixed fields
+TEST(ClientClassDef, fixedFieldsDefaults) {
+ boost::scoped_ptr<ClientClassDef> cclass;
+
+ std::string name = "class1";
+ ExpressionPtr expr;
+ CfgOptionPtr cfg_option;
+
+ // Classes cannot have blank names
+ ASSERT_THROW(cclass.reset(new ClientClassDef("", expr, cfg_option)),
+ BadValue);
+
+ // Verify we can create a class with a name, expression, and no cfg_option
+ ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr)));
+
+ // Let's checks that it doesn't return any nonsense
+ EXPECT_FALSE(cclass->getRequired());
+ EXPECT_FALSE(cclass->getDependOnKnown());
+ EXPECT_FALSE(cclass->getCfgOptionDef());
+ string empty;
+ ASSERT_EQ(IOAddress("0.0.0.0"), cclass->getNextServer());
+ EXPECT_EQ(empty, cclass->getSname());
+ EXPECT_EQ(empty, cclass->getFilename());
+}
+
+// Tests basic operations of fixed fields
+TEST(ClientClassDef, fixedFieldsBasics) {
+ boost::scoped_ptr<ClientClassDef> cclass;
+
+ std::string name = "class1";
+ ExpressionPtr expr;
+ CfgOptionPtr cfg_option;
+
+ // Classes cannot have blank names
+ ASSERT_THROW(cclass.reset(new ClientClassDef("", expr, cfg_option)),
+ BadValue);
+
+ // Verify we can create a class with a name, expression, and no cfg_option
+ ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr)));
+
+ cclass->setRequired(true);
+ cclass->setDependOnKnown(true);
+
+ string sname = "This is a very long string that can be a server name";
+ string filename = "this-is-a-slightly-longish-name-of-a-file.txt";
+
+ cclass->setNextServer(IOAddress("1.2.3.4"));
+ cclass->setSname(sname);
+ cclass->setFilename(filename);
+
+ // Let's checks that it doesn't return any nonsense
+ EXPECT_TRUE(cclass->getRequired());
+ EXPECT_TRUE(cclass->getDependOnKnown());
+ EXPECT_EQ(IOAddress("1.2.3.4"), cclass->getNextServer());
+ EXPECT_EQ(sname, cclass->getSname());
+ EXPECT_EQ(filename, cclass->getFilename());
+}
+
+
+// Verifies the unparse method of option class definitions
+TEST(ClientClassDef, unparseDef) {
+ CfgMgr::instance().setFamily(AF_INET);
+ boost::scoped_ptr<ClientClassDef> cclass;
+
+ // Get a client class definition and fill it
+ std::string name = "class1";
+ ExpressionPtr expr;
+ 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);
+ // The depend on known flag in not visible
+ cclass->setDependOnKnown(true);
+ std::string next_server = "1.2.3.4";
+ cclass->setNextServer(IOAddress(next_server));
+ std::string sname = "my-server.example.com";
+ cclass->setSname(sname);
+ std::string filename = "/boot/kernel";
+ cclass->setFilename(filename);
+
+ // Unparse it
+ std::string expected = "{\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"
+ "\"option-data\": [ ],\n"
+ "\"user-context\": { \"bar\": 1,\n"
+ "\"comment\": \"" + comment + "\" } }\n";
+ runToElementTest<ClientClassDef>(expected, *cclass);
+}
+
+// Verifies the unparse method of client class dictionaries
+TEST(ClientClassDictionary, unparseDict) {
+ CfgMgr::instance().setFamily(AF_INET);
+ ClientClassDictionaryPtr dictionary;
+ ExpressionPtr expr;
+ CfgOptionPtr options;
+
+ // Get a client class dictionary and fill it
+ dictionary.reset(new ClientClassDictionary());
+ ASSERT_NO_THROW(dictionary->addClass("one", expr, "", false,
+ false, options));
+ ASSERT_NO_THROW(dictionary->addClass("two", expr, "", false,
+ false, options));
+ ASSERT_NO_THROW(dictionary->addClass("three", expr, "", false,
+ false, options));
+
+ // Unparse it
+ auto add_defaults =
+ [](std::string name) {
+ return ("{\n"
+ "\"name\": \"" + name + "\",\n"
+ "\"next-server\": \"0.0.0.0\",\n"
+ "\"server-hostname\": \"\",\n"
+ "\"boot-file-name\": \"\",\n"
+ "\"option-data\": [ ] }");
+ };
+
+ std::string expected = "[\n" +
+ add_defaults("one") + ",\n" +
+ add_defaults("two") + ",\n" +
+ add_defaults("three") + "]\n";
+
+ runToElementTest<ClientClassDictionary>(expected, *dictionary);
+}
+
+// Tests that options have been created for all client classes in the
+// dictionary.
+TEST(ClientClassDictionary, createOptions) {
+ ClientClassDictionaryPtr dictionary(new ClientClassDictionary());
+ ExpressionPtr expr;
+ CfgOptionPtr cfg_option;
+
+ // First class has no options.
+ ASSERT_NO_THROW(dictionary->addClass("foo", expr, "", false,
+ false, cfg_option));
+
+ // Make some options for the second class.
+ cfg_option.reset(new CfgOption());
+ OptionPtr option = Option::create(Option::V4, DHO_BOOT_FILE_NAME);
+ OptionDescriptorPtr desc = OptionDescriptor::create(option, true, "bogus-file.txt");
+ desc->space_name_ = DHCP4_OPTION_SPACE;
+ cfg_option->add(*desc, desc->space_name_);
+
+ option = Option::create(Option::V4, DHO_TFTP_SERVER_NAME);
+ desc = OptionDescriptor::create(option, true, "bogus-tftp-server");
+ desc->space_name_ = DHCP4_OPTION_SPACE;
+ cfg_option->add(*desc, desc->space_name_);
+
+ // Add the second class with options.
+ ASSERT_NO_THROW(dictionary->addClass("bar", expr, "", false,
+ false, cfg_option));
+
+ // Make sure first class has no options.
+ ASSERT_TRUE(dictionary->getClasses());
+ auto classes = *(dictionary->getClasses());
+ auto options = classes[0]->getCfgOption();
+ ASSERT_TRUE(options->empty());
+
+ // Make sure second class has both options but their
+ // data buffers are empty.
+ options = classes[1]->getCfgOption();
+ ASSERT_FALSE(options->empty());
+
+ auto option_desc = options->get("dhcp4", DHO_BOOT_FILE_NAME);
+ ASSERT_TRUE(option_desc.option_);
+ ASSERT_TRUE(option_desc.option_->getData().empty());
+
+ option_desc = options->get("dhcp4", DHO_TFTP_SERVER_NAME);
+ ASSERT_TRUE(option_desc.option_);
+ ASSERT_TRUE(option_desc.option_->getData().empty());
+
+ // Now create match expressions for all of them.
+ auto cfg_def = CfgMgr::instance().getCurrentCfg()->getCfgOptionDef();
+ ASSERT_NO_THROW(dictionary->createOptions(cfg_def));
+
+ // Make sure first class still has no options.
+ classes = *(dictionary->getClasses());
+ options = classes[0]->getCfgOption();
+ ASSERT_TRUE(options->empty());
+
+ // Make sure second class has both options and that their
+ // data buffers are now correctly populated.
+ options = classes[1]->getCfgOption();
+ ASSERT_FALSE(options->empty());
+
+ option_desc = options->get("dhcp4", DHO_BOOT_FILE_NAME);
+ option = option_desc.option_;
+ ASSERT_TRUE(option);
+ EXPECT_EQ(OptionBuffer(option_desc.formatted_value_.begin(), option_desc.formatted_value_.end()),
+ option->getData());
+
+ option_desc = options->get("dhcp4", DHO_TFTP_SERVER_NAME);
+ option = option_desc.option_;
+ ASSERT_TRUE(option);
+ EXPECT_EQ(OptionBuffer(option_desc.formatted_value_.begin(), option_desc.formatted_value_.end()),
+ option->getData());
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/csv_lease_file4_unittest.cc b/src/lib/dhcpsrv/tests/csv_lease_file4_unittest.cc
new file mode 100644
index 0000000..cbb423d
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/csv_lease_file4_unittest.cc
@@ -0,0 +1,650 @@
+// Copyright (C) 2014-2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcp/duid.h>
+#include <dhcpsrv/csv_lease_file4.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/testutils/lease_file_io.h>
+#include <gtest/gtest.h>
+#include <ctime>
+#include <sstream>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::util;
+
+namespace {
+
+// HWADDR values used by unit tests.
+const uint8_t HWADDR0[] = { 0, 1, 2, 3, 4, 5 };
+const uint8_t HWADDR1[] = { 0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf };
+
+const uint8_t CLIENTID[] = { 1, 2, 3, 4 };
+
+/// @brief Test fixture class for @c CSVLeaseFile4 validation.
+class CSVLeaseFile4Test : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Initializes IO for lease file used by unit tests.
+ CSVLeaseFile4Test();
+
+ /// @brief Prepends the absolute path to the file specified
+ /// as an argument.
+ ///
+ /// @param filename Name of the file.
+ /// @return Absolute path to the test file.
+ static std::string absolutePath(const std::string& filename);
+
+ /// @brief Creates the lease file to be parsed by unit tests.
+ void writeSampleFile() const;
+
+ /// @brief Checks the stats for the file
+ ///
+ /// This method is passed a leasefile and the values for the statistics it
+ /// should have for comparison.
+ ///
+ /// @param lease_file A reference to the file we are using
+ /// @param reads the number of attempted reads
+ /// @param read_leases the number of valid leases read
+ /// @param read_errs the number of errors while reading leases
+ /// @param writes the number of attempted writes
+ /// @param write_leases the number of leases successfully written
+ /// @param write_errs the number of errors while writing
+ void checkStats(CSVLeaseFile4& lease_file,
+ uint32_t reads, uint32_t read_leases,
+ uint32_t read_errs, uint32_t writes,
+ uint32_t write_leases, uint32_t write_errs) const {
+ EXPECT_EQ(reads, lease_file.getReads());
+ EXPECT_EQ(read_leases, lease_file.getReadLeases());
+ EXPECT_EQ(read_errs, lease_file.getReadErrs());
+ EXPECT_EQ(writes, lease_file.getWrites());
+ EXPECT_EQ(write_leases, lease_file.getWriteLeases());
+ EXPECT_EQ(write_errs, lease_file.getWriteErrs());
+ }
+
+ /// @brief Name of the test lease file.
+ std::string filename_;
+
+ /// @brief Object providing access to lease file IO.
+ LeaseFileIO io_;
+
+ /// @brief hardware address 0 (corresponds to HWADDR0 const)
+ HWAddrPtr hwaddr0_;
+
+ /// @brief hardware address 1 (corresponds to HWADDR1 const)
+ HWAddrPtr hwaddr1_;
+
+};
+
+CSVLeaseFile4Test::CSVLeaseFile4Test()
+ : filename_(absolutePath("leases4.csv")), io_(filename_) {
+ hwaddr0_.reset(new HWAddr(HWADDR0, sizeof(HWADDR0), HTYPE_ETHER));
+ hwaddr1_.reset(new HWAddr(HWADDR1, sizeof(HWADDR1), HTYPE_ETHER));
+}
+
+std::string
+CSVLeaseFile4Test::absolutePath(const std::string& filename) {
+ std::ostringstream s;
+ s << DHCP_DATA_DIR << "/" << filename;
+ return (s.str());
+}
+
+void
+CSVLeaseFile4Test::writeSampleFile() const {
+ io_.writeFile("address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname,state,user_context\n"
+ "192.0.2.1,06:07:08:09:0a:bc,,200,200,8,1,1,"
+ "host.example.com,0,\n"
+ "192.0.2.2,,,200,200,8,1,1,host.example.com,0,\n"
+ "192.0.2.3,dd:de:ba:0d:1b:2e:3e:4f,0a:00:01:04,100,100,7,"
+ "0,0,,1,{ \"foobar\": true }\n"
+ "192.0.2.4,,11:22:33:44:55:66,200,200,8,1,1,host.example.com,0,\n"
+ "192.0.2.5,,,200,200,8,1,1,,1,\n");
+}
+
+// This test checks the capability to read and parse leases from the file.
+TEST_F(CSVLeaseFile4Test, parse) {
+ // Create a file to be parsed.
+ writeSampleFile();
+
+ // Open the lease file.
+ CSVLeaseFile4 lf(filename_);
+ ASSERT_NO_THROW(lf.open());
+
+ // Verify the counters are cleared
+ {
+ SCOPED_TRACE("Check stats are empty");
+ checkStats(lf, 0, 0, 0, 0, 0, 0);
+ }
+
+ Lease4Ptr lease;
+ // Reading first read should be successful.
+ {
+ SCOPED_TRACE("First lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+ checkStats(lf, 1, 1, 0, 0, 0, 0);
+
+ // Verify that the lease attributes are correct.
+ EXPECT_EQ("192.0.2.1", lease->addr_.toText());
+ HWAddr hwaddr1(*lease->hwaddr_);
+ EXPECT_EQ("06:07:08:09:0a:bc", hwaddr1.toText(false));
+ EXPECT_FALSE(lease->client_id_);
+ EXPECT_EQ(200, lease->valid_lft_);
+ EXPECT_EQ(0, lease->cltt_);
+ EXPECT_EQ(8, lease->subnet_id_);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("host.example.com", lease->hostname_);
+ EXPECT_EQ(Lease::STATE_DEFAULT, lease->state_);
+ EXPECT_FALSE(lease->getContext());
+ }
+
+ // Second lease is malformed - has no HW address or client id and state
+ // is not declined.
+ {
+ SCOPED_TRACE("Second lease malformed");
+ EXPECT_FALSE(lf.next(lease));
+ EXPECT_FALSE(lease);
+ checkStats(lf, 2, 1, 1, 0, 0, 0);
+ }
+
+ // Even though parsing previous lease failed, reading the next lease should be
+ // successful.
+ {
+ SCOPED_TRACE("Third lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+ checkStats(lf, 3, 2, 1, 0, 0, 0);
+
+ // Verify that the third lease is correct.
+ EXPECT_EQ("192.0.2.3", lease->addr_.toText());
+ HWAddr hwaddr3(*lease->hwaddr_);
+ EXPECT_EQ("dd:de:ba:0d:1b:2e:3e:4f", hwaddr3.toText(false));
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_EQ("0a:00:01:04", lease->client_id_->toText());
+ EXPECT_EQ(100, lease->valid_lft_);
+ EXPECT_EQ(0, lease->cltt_);
+ EXPECT_EQ(7, lease->subnet_id_);
+ EXPECT_FALSE(lease->fqdn_fwd_);
+ EXPECT_FALSE(lease->fqdn_rev_);
+ EXPECT_TRUE(lease->hostname_.empty());
+ EXPECT_EQ(Lease::STATE_DECLINED, lease->state_);
+ ASSERT_TRUE(lease->getContext());
+ EXPECT_EQ("{ \"foobar\": true }", lease->getContext()->str());
+ }
+
+ // Fourth lease has no hardware address but has client id
+ {
+ SCOPED_TRACE("Fourth lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+ checkStats(lf, 4, 3, 1, 0, 0, 0);
+
+ EXPECT_EQ("192.0.2.4", lease->addr_.toText());
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(lease->hwaddr_->hwaddr_.empty());
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_EQ("11:22:33:44:55:66", lease->client_id_->toText());
+ }
+
+ // Fifth lease has no hardware address or client id but is declined
+ {
+ SCOPED_TRACE("Fifth lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+ checkStats(lf, 5, 4, 1, 0, 0, 0);
+
+ EXPECT_EQ("192.0.2.5", lease->addr_.toText());
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_TRUE(lease->hwaddr_->hwaddr_.empty());
+ ASSERT_FALSE(lease->client_id_);
+ EXPECT_EQ(lease->state_, Lease::STATE_DECLINED);
+ }
+
+ // There are no more leases. Reading should cause no error, but the returned
+ // lease pointer should be NULL.
+ {
+ SCOPED_TRACE("Sixth read empty");
+ EXPECT_TRUE(lf.next(lease));
+ EXPECT_FALSE(lease);
+ checkStats(lf, 6, 4, 1, 0, 0, 0);
+ }
+
+ // We should be able to do it again.
+ {
+ SCOPED_TRACE("Seventh read empty");
+ EXPECT_TRUE(lf.next(lease));
+ EXPECT_FALSE(lease);
+ checkStats(lf, 7, 4, 1, 0, 0, 0);
+ }
+}
+
+// This test checks creation of the lease file and writing leases.
+TEST_F(CSVLeaseFile4Test, recreate) {
+ CSVLeaseFile4 lf(filename_);
+ ASSERT_NO_THROW(lf.recreate());
+ ASSERT_TRUE(io_.exists());
+
+ // Verify the counters are cleared
+ checkStats(lf, 0, 0, 0, 0, 0, 0);
+
+ // Create first lease, with NULL client id.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.3.2"),
+ hwaddr0_,
+ NULL, 0,
+ 200, 0, 8, true, true,
+ "host.example.com"));
+ lease->state_ = Lease::STATE_EXPIRED_RECLAIMED;
+ {
+ SCOPED_TRACE("First write");
+ ASSERT_NO_THROW(lf.append(*lease));
+ checkStats(lf, 0, 0, 0, 1, 1, 0);
+ }
+
+ // Create second lease, with non-NULL client id and user context.
+ lease.reset(new Lease4(IOAddress("192.0.3.10"),
+ hwaddr1_,
+ CLIENTID, sizeof(CLIENTID),
+ 100, 0, 7));
+ lease->setContext(Element::fromJSON("{ \"foobar\": true }"));
+ {
+ SCOPED_TRACE("Second write");
+ ASSERT_NO_THROW(lf.append(*lease));
+ checkStats(lf, 0, 0, 0, 2, 2, 0);
+ }
+
+ // Close the lease file.
+ lf.close();
+ // Check that the contents of the csv file are correct.
+ EXPECT_EQ("address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname,state,user_context\n"
+ "192.0.3.2,00:01:02:03:04:05,,200,200,8,1,1,host.example.com,"
+ "2,\n"
+ "192.0.3.10,0d:0e:0a:0d:0b:0e:0e:0f,01:02:03:04,100,100,7,0,"
+ "0,,0,{ \"foobar\": true }\n",
+ io_.readFile());
+}
+
+// Verifies that a schema 1.0 file with records from
+// schema 1.0 and 2.0 loads correctly.
+TEST_F(CSVLeaseFile4Test, mixedSchemaload) {
+ // Create mixed schema file
+ io_.writeFile(
+ // schema 1.0 header
+ "address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname\n"
+ // schema 1.0 record
+ "192.0.2.1,06:07:08:09:1a:bc,,200,200,8,1,1,"
+ "one.example.com\n"
+ // schema 2.0 record - has state
+ "192.0.2.2,06:07:08:09:2a:bc,,200,200,8,1,1,"
+ "two.example.com,1\n"
+ // schema 2.1 record - has state and user context
+ "192.0.2.3,06:07:08:09:3a:bc,,200,200,8,1,1,"
+ "three.example.com,2,{ \"foobar\": true }\n"
+ );
+
+ // Open the lease file.
+ CSVLeaseFile4 lf(filename_);
+ ASSERT_NO_THROW(lf.open());
+
+ Lease4Ptr lease;
+
+ // Reading first read should be successful.
+ {
+ SCOPED_TRACE("First lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+
+ // Verify that the lease attributes are correct.
+ EXPECT_EQ("192.0.2.1", lease->addr_.toText());
+ HWAddr hwaddr1(*lease->hwaddr_);
+ EXPECT_EQ("06:07:08:09:1a:bc", hwaddr1.toText(false));
+ EXPECT_FALSE(lease->client_id_);
+ EXPECT_EQ(200, lease->valid_lft_);
+ EXPECT_EQ(0, lease->cltt_);
+ EXPECT_EQ(8, lease->subnet_id_);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("one.example.com", lease->hostname_);
+ // Verify that added state is DEFAULT
+ EXPECT_EQ(Lease::STATE_DEFAULT, lease->state_);
+ EXPECT_FALSE(lease->getContext());
+ }
+
+ {
+ SCOPED_TRACE("Second lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+
+ // Verify that the lease attributes are correct.
+ EXPECT_EQ("192.0.2.2", lease->addr_.toText());
+ HWAddr hwaddr1(*lease->hwaddr_);
+ EXPECT_EQ("06:07:08:09:2a:bc", hwaddr1.toText(false));
+ EXPECT_FALSE(lease->client_id_);
+ EXPECT_EQ(200, lease->valid_lft_);
+ EXPECT_EQ(0, lease->cltt_);
+ EXPECT_EQ(8, lease->subnet_id_);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("two.example.com", lease->hostname_);
+ EXPECT_EQ(Lease::STATE_DECLINED, lease->state_);
+ EXPECT_FALSE(lease->getContext());
+ }
+
+ {
+ SCOPED_TRACE("Third lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+
+ // Verify that the third lease is correct.
+ EXPECT_EQ("192.0.2.3", lease->addr_.toText());
+ HWAddr hwaddr1(*lease->hwaddr_);
+ EXPECT_EQ("06:07:08:09:3a:bc", hwaddr1.toText(false));
+ EXPECT_FALSE(lease->client_id_);
+ EXPECT_EQ(200, lease->valid_lft_);
+ EXPECT_EQ(0, lease->cltt_);
+ EXPECT_EQ(8, lease->subnet_id_);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("three.example.com", lease->hostname_);
+ EXPECT_EQ(Lease::STATE_EXPIRED_RECLAIMED, lease->state_);
+ ASSERT_TRUE(lease->getContext());
+ EXPECT_EQ("{ \"foobar\": true }", lease->getContext()->str());
+ }
+}
+
+
+// Verifies that a lease file with fewer header columns than the
+// minimum allowed will not open.
+TEST_F(CSVLeaseFile4Test, tooFewHeaderColumns) {
+ // Create 1.0 file
+ io_.writeFile("address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev\n");
+
+ // Open the lease file.
+ CSVLeaseFile4 lf(filename_);
+ ASSERT_THROW(lf.open(), CSVFileError);
+}
+
+// Verifies that a lease file with an unrecognized column header
+// will not open.
+TEST_F(CSVLeaseFile4Test, invalidHeaderColumn) {
+ // Create 1.0 file
+ io_.writeFile("address,hwaddr,BOGUS,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname,state,user_context\n");
+
+ // Open the lease file.
+ CSVLeaseFile4 lf(filename_);
+ ASSERT_THROW(lf.open(), CSVFileError);
+}
+
+// Verifies that a lease file with more header columns than defined
+// columns will downgrade.
+TEST_F(CSVLeaseFile4Test, downGrade) {
+ // Create 2.0 PLUS a column file
+ io_.writeFile("address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname,state,user_context,FUTURE_COL\n"
+
+ "192.0.2.3,06:07:08:09:3a:bc,,200,200,8,1,1,"
+ "three.example.com,2,,BOGUS\n");
+
+ // Lease file should open and report as needing downgrade.
+ CSVLeaseFile4 lf(filename_);
+ ASSERT_NO_THROW(lf.open());
+ EXPECT_TRUE(lf.needsConversion());
+ EXPECT_EQ(util::VersionedCSVFile::NEEDS_DOWNGRADE,
+ lf.getInputSchemaState());
+ Lease4Ptr lease;
+
+ {
+ SCOPED_TRACE("First lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+
+ // Verify that the third lease is correct.
+ EXPECT_EQ("192.0.2.3", lease->addr_.toText());
+ HWAddr hwaddr1(*lease->hwaddr_);
+ EXPECT_EQ("06:07:08:09:3a:bc", hwaddr1.toText(false));
+ EXPECT_FALSE(lease->client_id_);
+ EXPECT_EQ(200, lease->valid_lft_);
+ EXPECT_EQ(0, lease->cltt_);
+ EXPECT_EQ(8, lease->subnet_id_);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("three.example.com", lease->hostname_);
+ EXPECT_EQ(Lease::STATE_EXPIRED_RECLAIMED, lease->state_);
+ EXPECT_FALSE(lease->getContext());
+ }
+}
+
+// Verifies that leases with no hardware address are only permitted
+// if they are in the declined state.
+TEST_F(CSVLeaseFile4Test, declinedLeaseTest) {
+ io_.writeFile("address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname,state,user_context\n"
+ "192.0.2.1,,,200,200,8,1,1,host.example.com,0,\n"
+ "192.0.2.1,,,200,200,8,1,1,host.example.com,1,\n");
+
+ CSVLeaseFile4 lf(filename_);
+ ASSERT_NO_THROW(lf.open());
+ EXPECT_FALSE(lf.needsConversion());
+ EXPECT_EQ(util::VersionedCSVFile::CURRENT, lf.getInputSchemaState());
+ Lease4Ptr lease;
+
+ {
+ SCOPED_TRACE("No hardware and not declined, invalid");
+ EXPECT_FALSE(lf.next(lease));
+ ASSERT_FALSE(lease);
+ EXPECT_EQ(lf.getReadErrs(),1);
+ }
+
+ {
+ SCOPED_TRACE("No hardware and declined, valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(lf.getReadErrs(),1);
+ }
+}
+
+// Verifies that it is possible to output a lease with very high valid
+// lifetime (infinite in RFC2131 terms) and current time, and then read
+// back this lease.
+TEST_F(CSVLeaseFile4Test, highLeaseLifetime) {
+ CSVLeaseFile4 lf(filename_);
+ ASSERT_NO_THROW(lf.recreate());
+ ASSERT_TRUE(io_.exists());
+
+ // Write lease with very high lease lifetime and current time.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.3.2"),
+ hwaddr0_,
+ NULL, 0,
+ 0xFFFFFFFF, time(0),
+ 8, true, true,
+ "host.example.com"));
+ // Write this lease out to the lease file.
+ ASSERT_NO_THROW(lf.append(*lease));
+
+ // Close the lease file.
+ lf.close();
+
+ Lease4Ptr lease_read;
+
+ // Re-open the file for reading.
+ ASSERT_NO_THROW(lf.open());
+
+ // Read the lease and make sure it is successful.
+ EXPECT_TRUE(lf.next(lease_read));
+ ASSERT_TRUE(lease_read);
+
+ // The valid lifetime and the cltt should match with the original lease.
+ EXPECT_EQ(lease->valid_lft_, lease_read->valid_lft_);
+ EXPECT_EQ(lease->cltt_, lease_read->cltt_);
+}
+
+// Verifies that it is not possible to output a lease with empty hwaddr in other
+// than the declined state
+TEST_F(CSVLeaseFile4Test, emptyHWAddrDefaultStateOnly) {
+ CSVLeaseFile4 lf(filename_);
+ ASSERT_NO_THROW(lf.recreate());
+ ASSERT_TRUE(io_.exists());
+
+ HWAddrPtr hwaddr;
+
+ // Create lease with null hwaddr and default state
+ Lease4Ptr lease_null_hwaddr(new Lease4(IOAddress("192.0.3.2"),
+ hwaddr,
+ NULL, 0,
+ 0xFFFFFFFF, time(0),
+ 8, true, true,
+ "host.example.com"));
+ // Try to write this lease out to the lease file.
+ ASSERT_THROW(lf.append(*lease_null_hwaddr), BadValue);
+
+ hwaddr.reset(new HWAddr());
+
+ // Create lease with empty hwaddr and default state
+ Lease4Ptr lease_empty_hwaddr(new Lease4(IOAddress("192.0.3.2"),
+ hwaddr,
+ NULL, 0,
+ 0xFFFFFFFF, time(0),
+ 8, true, true,
+ "host.example.com"));
+ // Try to write this lease out to the lease file.
+ ASSERT_THROW(lf.append(*lease_empty_hwaddr), BadValue);
+
+ // Create lease with hwaddr and current time.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.3.2"),
+ hwaddr0_,
+ NULL, 0,
+ 0xFFFFFFFF, time(0),
+ 8, true, true,
+ "host.example.com"));
+
+ // Decline the lease
+ lease->decline(1000);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_EQ(lease->hwaddr_->toText(false), "");
+
+ // Write this lease out to the lease file.
+ ASSERT_NO_THROW(lf.append(*lease));
+
+ // Close the lease file.
+ lf.close();
+
+ Lease4Ptr lease_read;
+
+ // Re-open the file for reading.
+ ASSERT_NO_THROW(lf.open());
+
+ // Read the lease and make sure it is successful.
+ EXPECT_TRUE(lf.next(lease_read));
+ ASSERT_TRUE(lease_read);
+
+ // The valid lifetime and the cltt should match with the original lease.
+ EXPECT_EQ(lease->valid_lft_, lease_read->valid_lft_);
+ EXPECT_EQ(lease->cltt_, lease_read->cltt_);
+}
+
+// Verifies that it is possible to write and read a lease with commas
+// in hostname and user context.
+TEST_F(CSVLeaseFile4Test, embeddedCommas) {
+ CSVLeaseFile4 lf(filename_);
+ ASSERT_NO_THROW(lf.recreate());
+ ASSERT_TRUE(io_.exists());
+
+ std::string hostname("host,example,com");
+ std::string context_str("{ \"bar\": true, \"foo\": false, \"x\": \"factor\" }");
+
+ // Create a lease with commas in the hostname.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.3.2"),
+ hwaddr0_,
+ NULL, 0,
+ 0xFFFFFFFF, time(0),
+ 8, true, true,
+ hostname));
+
+ // Add the user context with commas.
+ lease->setContext(Element::fromJSON(context_str));
+
+ // Write this lease out to the lease file.
+ ASSERT_NO_THROW(lf.append(*lease));
+
+ // Close the lease file.
+ lf.close();
+
+ Lease4Ptr lease_read;
+
+ // Re-open the file for reading.
+ ASSERT_NO_THROW(lf.open());
+
+ // Read the lease and make sure it is successful.
+ EXPECT_TRUE(lf.next(lease_read));
+ ASSERT_TRUE(lease_read);
+
+ // Expect the hostname and user context to retain the commas
+ // they started with.
+ EXPECT_EQ(hostname, lease->hostname_);
+ EXPECT_EQ(context_str, lease->getContext()->str());
+}
+
+// Verifies that it is possible to write and read a lease with
+// escape tags and sequences in hostname and user context.
+TEST_F(CSVLeaseFile4Test, embeddedEscapes) {
+ CSVLeaseFile4 lf(filename_);
+ ASSERT_NO_THROW(lf.recreate());
+ ASSERT_TRUE(io_.exists());
+
+ std::string hostname("host&#xexample&#x2ccom");
+ std::string context_str("{ \"&#xbar\": true, \"foo\": false, \"x\": \"fac&#x2ctor\" }");
+
+ // Create a lease with commas in the hostname.
+ Lease4Ptr lease(new Lease4(IOAddress("192.0.3.2"),
+ hwaddr0_,
+ NULL, 0,
+ 0xFFFFFFFF, time(0),
+ 8, true, true,
+ hostname));
+
+ // Add the user context with commas.
+ lease->setContext(Element::fromJSON(context_str));
+
+ // Write this lease out to the lease file.
+ ASSERT_NO_THROW(lf.append(*lease));
+
+ // Close the lease file.
+ lf.close();
+
+ Lease4Ptr lease_read;
+
+ // Re-open the file for reading.
+ ASSERT_NO_THROW(lf.open());
+
+ // Read the lease and make sure it is successful.
+ EXPECT_TRUE(lf.next(lease_read));
+ ASSERT_TRUE(lease_read);
+
+ // Expect the hostname and user context to retain the commas
+ // they started with.
+ EXPECT_EQ(hostname, lease->hostname_);
+ EXPECT_EQ(context_str, lease->getContext()->str());
+}
+
+/// @todo Currently we don't check invalid lease attributes, such as invalid
+/// lease type, invalid preferred lifetime vs valid lifetime etc. The Lease6
+/// should be extended with the function that validates lease attributes. Once
+/// this is implemented we should provide more tests for malformed leases
+/// in the CSV file. See http://oldkea.isc.org/ticket/2405.
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/csv_lease_file6_unittest.cc b/src/lib/dhcpsrv/tests/csv_lease_file6_unittest.cc
new file mode 100644
index 0000000..35aea7f
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/csv_lease_file6_unittest.cc
@@ -0,0 +1,724 @@
+// Copyright (C) 2014-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcp/duid.h>
+#include <dhcpsrv/csv_lease_file6.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/testutils/lease_file_io.h>
+#include <gtest/gtest.h>
+#include <ctime>
+#include <sstream>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::util;
+
+namespace {
+
+// DUID values used by unit tests.
+const uint8_t DUID0[] = { 0, 1, 2, 3, 4, 5, 6, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf };
+const uint8_t DUID1[] = { 1, 1, 1, 1, 0xa, 1, 2, 3, 4, 5 };
+
+/// @brief Test fixture class for @c CSVLeaseFile6 validation.
+class CSVLeaseFile6Test : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Initializes IO for lease file used by unit tests.
+ CSVLeaseFile6Test();
+
+ /// @brief Prepends the absolute path to the file specified
+ /// as an argument.
+ ///
+ /// @param filename Name of the file.
+ /// @return Absolute path to the test file.
+ static std::string absolutePath(const std::string& filename);
+
+ /// @brief Create DUID object from the binary.
+ ///
+ /// @param duid Binary value representing a DUID.
+ /// @param size Size of the DUID.
+ /// @return Pointer to the @c DUID object.
+ DuidPtr makeDUID(const uint8_t* duid, const unsigned int size) const {
+ return (DuidPtr(new DUID(duid, size)));
+ }
+
+ /// @brief Create lease file that can be parsed by unit tests.
+ void writeSampleFile() const;
+
+ /// @brief Checks the stats for the file
+ ///
+ /// This method is passed a leasefile and the values for the statistics it
+ /// should have for comparison.
+ ///
+ /// @param lease_file A reference to the file we are using
+ /// @param reads the number of attempted reads
+ /// @param read_leases the number of valid leases read
+ /// @param read_errs the number of errors while reading leases
+ /// @param writes the number of attempted writes
+ /// @param write_leases the number of leases successfully written
+ /// @param write_errs the number of errors while writing
+ void checkStats(CSVLeaseFile6& lease_file,
+ uint32_t reads, uint32_t read_leases,
+ uint32_t read_errs, uint32_t writes,
+ uint32_t write_leases, uint32_t write_errs) const {
+ EXPECT_EQ(reads, lease_file.getReads());
+ EXPECT_EQ(read_leases, lease_file.getReadLeases());
+ EXPECT_EQ(read_errs, lease_file.getReadErrs());
+ EXPECT_EQ(writes, lease_file.getWrites());
+ EXPECT_EQ(write_leases, lease_file.getWriteLeases());
+ EXPECT_EQ(write_errs, lease_file.getWriteErrs());
+ }
+
+ /// @brief Name of the test lease file.
+ std::string filename_;
+
+ /// @brief Object providing access to lease file IO.
+ LeaseFileIO io_;
+
+};
+
+CSVLeaseFile6Test::CSVLeaseFile6Test()
+ : filename_(absolutePath("leases6.csv")), io_(filename_) {
+}
+
+std::string
+CSVLeaseFile6Test::absolutePath(const std::string& filename) {
+ std::ostringstream s;
+ s << DHCP_DATA_DIR << "/" << filename;
+ return (s.str());
+}
+
+void
+CSVLeaseFile6Test::writeSampleFile() const {
+ io_.writeFile("address,duid,valid_lifetime,expire,subnet_id,"
+ "pref_lifetime,lease_type,iaid,prefix_len,fqdn_fwd,"
+ "fqdn_rev,hostname,hwaddr,state,user_context,"
+ "hwtype,hwaddr_source\n"
+ "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,"
+ "200,200,8,100,0,7,0,1,1,host.example.com,,1,,"
+ "1,0\n"
+ "2001:db8:1::1,,200,200,8,100,0,7,0,1,1,host.example.com,,1,,"
+ "1,0\n"
+ "2001:db8:2::10,01:01:01:01:0a:01:02:03:04:05,300,300,6,150,"
+ "0,8,0,0,0,,,1,,"
+ "1,0\n"
+ "3000:1::,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,0,200,8,0,2,"
+ "16,64,0,0,,,1,{ \"foobar\": true },,"
+ "1,0\n"
+ "2001:db8:1::2,00,200,200,8,100,0,7,0,1,1,host.example.com,,0,,"
+ "1,0\n"
+ "2001:db8:1::3,00,200,200,8,100,0,7,0,1,1,host.example.com,,1,,"
+ "1,0\n");
+}
+
+// This test checks the capability to read and parse leases from the file.
+TEST_F(CSVLeaseFile6Test, parse) {
+ // Create a file to be parsed.
+ writeSampleFile();
+
+ // Open the lease file.
+ CSVLeaseFile6 lf(filename_);
+ ASSERT_NO_THROW(lf.open());
+
+ // Verify the counters are cleared
+ {
+ SCOPED_TRACE("Check stats are empty");
+ checkStats(lf, 0, 0, 0, 0, 0, 0);
+ }
+
+ Lease6Ptr lease;
+ // Reading first read should be successful.
+ {
+ SCOPED_TRACE("First lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+ checkStats(lf, 1, 1, 0, 0, 0, 0);
+
+ // Verify that the lease attributes are correct.
+ EXPECT_EQ("2001:db8:1::1", lease->addr_.toText());
+ ASSERT_TRUE(lease->duid_);
+ EXPECT_EQ("00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f", lease->duid_->toText());
+ EXPECT_EQ(200, lease->valid_lft_);
+ EXPECT_EQ(0, lease->cltt_);
+ EXPECT_EQ(8, lease->subnet_id_);
+ EXPECT_EQ(100, lease->preferred_lft_);
+ EXPECT_EQ(Lease::TYPE_NA, lease->type_);
+ EXPECT_EQ(7, lease->iaid_);
+ EXPECT_EQ(0, lease->prefixlen_);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("host.example.com", lease->hostname_);
+ EXPECT_EQ(Lease::STATE_DECLINED, lease->state_);
+ EXPECT_FALSE(lease->getContext());
+ }
+
+ // Second lease is malformed - DUID is blank (i.e. ",,")
+ {
+ SCOPED_TRACE("Second lease malformed");
+ EXPECT_FALSE(lf.next(lease));
+ checkStats(lf, 2, 1, 1, 0, 0, 0);
+ }
+
+ // Even, parsing previous lease failed, reading the next lease should be
+ // successful.
+ {
+ SCOPED_TRACE("Third lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+ checkStats(lf, 3, 2, 1, 0, 0, 0);
+
+ // Verify that the third lease is correct.
+ EXPECT_EQ("2001:db8:2::10", lease->addr_.toText());
+ ASSERT_TRUE(lease->duid_);
+ EXPECT_EQ("01:01:01:01:0a:01:02:03:04:05", lease->duid_->toText());
+ EXPECT_EQ(300, lease->valid_lft_);
+ EXPECT_EQ(0, lease->cltt_);
+ EXPECT_EQ(6, lease->subnet_id_);
+ EXPECT_EQ(150, lease->preferred_lft_);
+ EXPECT_EQ(Lease::TYPE_NA, lease->type_);
+ EXPECT_EQ(8, lease->iaid_);
+ EXPECT_EQ(0, lease->prefixlen_);
+ EXPECT_FALSE(lease->fqdn_fwd_);
+ EXPECT_FALSE(lease->fqdn_rev_);
+ EXPECT_TRUE(lease->hostname_.empty());
+ EXPECT_EQ(Lease::STATE_DECLINED, lease->state_);
+ EXPECT_FALSE(lease->getContext());
+ }
+
+ // Reading the fourth lease should be successful.
+ {
+ SCOPED_TRACE("Fourth lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+ checkStats(lf, 4, 3, 1, 0, 0, 0);
+
+ // Verify that the lease is correct.
+ EXPECT_EQ("3000:1::", lease->addr_.toText());
+ ASSERT_TRUE(lease->duid_);
+ EXPECT_EQ("00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f", lease->duid_->toText());
+ EXPECT_EQ(0, lease->valid_lft_);
+ EXPECT_EQ(200, lease->cltt_);
+ EXPECT_EQ(8, lease->subnet_id_);
+ EXPECT_EQ(0, lease->preferred_lft_);
+ EXPECT_EQ(Lease::TYPE_PD, lease->type_);
+ EXPECT_EQ(16, lease->iaid_);
+ EXPECT_EQ(64, lease->prefixlen_);
+ EXPECT_FALSE(lease->fqdn_fwd_);
+ EXPECT_FALSE(lease->fqdn_rev_);
+ EXPECT_TRUE(lease->hostname_.empty());
+ EXPECT_EQ(Lease::STATE_DECLINED, lease->state_);
+ ASSERT_TRUE(lease->getContext());
+ EXPECT_EQ("{ \"foobar\": true }", lease->getContext()->str());
+ }
+
+
+ // Fifth lease is invalid - DUID is empty, state is not DECLINED
+ {
+ SCOPED_TRACE("Fifth lease invalid");
+ EXPECT_FALSE(lf.next(lease));
+ checkStats(lf, 5, 3, 2, 0, 0, 0);
+ }
+
+ // Reading the sixth lease should be successful.
+ {
+ SCOPED_TRACE("sixth lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+ checkStats(lf, 6, 4, 2, 0, 0, 0);
+
+ // Verify that the lease is correct.
+ EXPECT_EQ("2001:db8:1::3", lease->addr_.toText());
+ ASSERT_TRUE(lease->duid_);
+ EXPECT_EQ("00", lease->duid_->toText());
+ EXPECT_EQ(Lease::STATE_DECLINED, lease->state_);
+ }
+
+ // There are no more leases. Reading should cause no error, but the returned
+ // lease pointer should be NULL.
+ {
+ SCOPED_TRACE("Sixth read empty");
+ EXPECT_TRUE(lf.next(lease));
+ EXPECT_FALSE(lease);
+ checkStats(lf, 7, 4, 2, 0, 0, 0);
+ }
+
+ // We should be able to do it again.
+ {
+ SCOPED_TRACE("Seventh read empty");
+ EXPECT_TRUE(lf.next(lease));
+ EXPECT_FALSE(lease);
+ checkStats(lf, 8, 4, 2, 0, 0, 0);
+ }
+}
+
+// This test checks creation of the lease file and writing leases.
+TEST_F(CSVLeaseFile6Test, recreate) {
+ CSVLeaseFile6 lf(filename_);
+ ASSERT_NO_THROW(lf.recreate());
+ ASSERT_TRUE(io_.exists());
+
+ // Verify the counters are cleared
+ {
+ SCOPED_TRACE("Check stats are empty");
+ checkStats(lf, 0, 0, 0, 0, 0, 0);
+ }
+
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ makeDUID(DUID0, sizeof(DUID0)),
+ 7, 100, 200, 8, true, true,
+ "host.example.com"));
+ lease->cltt_ = 0;
+ {
+ SCOPED_TRACE("First write");
+ ASSERT_NO_THROW(lf.append(*lease));
+ checkStats(lf, 0, 0, 0, 1, 1, 0);
+ }
+
+ lease.reset(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:2::10"),
+ makeDUID(DUID1, sizeof(DUID1)),
+ 8, 150, 300, 6, false, false,
+ "", HWAddrPtr(), 128));
+ lease->cltt_ = 0;
+ {
+ SCOPED_TRACE("Second write");
+ ASSERT_NO_THROW(lf.append(*lease));
+ checkStats(lf, 0, 0, 0, 2, 2, 0);
+ }
+
+ lease.reset(new Lease6(Lease::TYPE_PD, IOAddress("3000:1:1::"),
+ makeDUID(DUID0, sizeof(DUID0)),
+ 7, 150, 300, 10, false, false,
+ "", HWAddrPtr(), 64));
+ lease->cltt_ = 0;
+ lease->setContext(Element::fromJSON("{ \"foobar\": true }"));
+ {
+ SCOPED_TRACE("Third write");
+ ASSERT_NO_THROW(lf.append(*lease));
+ checkStats(lf, 0, 0, 0, 3, 3, 0);
+ }
+
+ DuidPtr empty(new DUID(DUID::EMPTY()));
+ lease.reset(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:2::10"),
+ empty, 8, 150, 300, 6, false, false,
+ "", HWAddrPtr(), 128));
+ lease->cltt_ = 0;
+ {
+ SCOPED_TRACE("Fourth write - invalid, no DUID, not declined");
+ ASSERT_THROW(lf.append(*lease), BadValue);
+ checkStats(lf, 0, 0, 0, 4, 3, 1);
+ }
+
+ {
+ SCOPED_TRACE("Fifth write - valid, no DUID, declined");
+ lease->state_ = Lease::STATE_DECLINED;
+ ASSERT_NO_THROW(lf.append(*lease));
+ checkStats(lf, 0, 0, 0, 5, 4, 1);
+ }
+
+
+ EXPECT_EQ("address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,hwaddr,"
+ "state,user_context,hwtype,hwaddr_source\n"
+ "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,"
+ "200,200,8,100,0,7,128,1,1,host.example.com,,0,,,\n"
+ "2001:db8:2::10,01:01:01:01:0a:01:02:03:04:05"
+ ",300,300,6,150,0,8,128,0,0,,,0,,,\n"
+ "3000:1:1::,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,"
+ "300,300,10,150,2,7,64,0,0,,,0,{ \"foobar\": true },,\n"
+ "2001:db8:2::10,00,300,300,6,150,0,8,128,0,0,,,1,,,\n",
+ io_.readFile());
+}
+
+// Verifies that a 1.0 schema file with records from
+// schema 1.0, 2.0, and 3.0 loads correctly.
+TEST_F(CSVLeaseFile6Test, mixedSchemaLoad) {
+ // Create a mixed schema file
+ io_.writeFile(
+ // schema 1.0 header
+ "address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname\n"
+ // schema 1.0 record
+ "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:01,"
+ "200,200,8,100,0,7,0,1,1,one.example.com\n"
+
+ // schema 2.0 record - has hwaddr
+ "2001:db8:1::2,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:02,"
+ "200,200,8,100,0,7,0,1,1,two.example.com,01:02:03:04:05\n"
+
+ // schema 3.0 record - has hwaddr and state
+ "2001:db8:1::3,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:03,"
+ "200,200,8,100,0,7,0,1,1,three.example.com,0a:0b:0c:0d:0e,1\n"
+
+ // schema 3.1 record - has hwaddr, state and user context
+ "2001:db8:1::4,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:03,"
+ "200,200,8,100,0,7,0,1,1,three.example.com,0a:0b:0c:0d:0e,1,"
+ "{ \"foobar\": true }\n");
+
+ // Open the lease file.
+ CSVLeaseFile6 lf(filename_);
+ ASSERT_NO_THROW(lf.open());
+
+ Lease6Ptr lease;
+ {
+ SCOPED_TRACE("First lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+
+ // Verify that the lease attributes are correct.
+ EXPECT_EQ("2001:db8:1::1", lease->addr_.toText());
+ ASSERT_TRUE(lease->duid_);
+ EXPECT_EQ("00:01:02:03:04:05:06:0a:0b:0c:0d:0e:01", lease->duid_->toText());
+ EXPECT_EQ(200, lease->valid_lft_);
+ EXPECT_EQ(0, lease->cltt_);
+ EXPECT_EQ(8, lease->subnet_id_);
+ EXPECT_EQ(100, lease->preferred_lft_);
+ EXPECT_EQ(Lease::TYPE_NA, lease->type_);
+ EXPECT_EQ(7, lease->iaid_);
+ EXPECT_EQ(0, lease->prefixlen_);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("one.example.com", lease->hostname_);
+ // Verify that added HWaddr is empty
+ EXPECT_FALSE(lease->hwaddr_);
+ // Verify that added state is STATE_DEFAULT
+ EXPECT_EQ(Lease::STATE_DEFAULT, lease->state_);
+ EXPECT_FALSE(lease->getContext());
+ }
+
+ {
+ SCOPED_TRACE("Second lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+
+ // Verify that the lease attributes are correct.
+ EXPECT_EQ("2001:db8:1::2", lease->addr_.toText());
+ ASSERT_TRUE(lease->duid_);
+ EXPECT_EQ("00:01:02:03:04:05:06:0a:0b:0c:0d:0e:02", lease->duid_->toText());
+ EXPECT_EQ(200, lease->valid_lft_);
+ EXPECT_EQ(0, lease->cltt_);
+ EXPECT_EQ(8, lease->subnet_id_);
+ EXPECT_EQ(100, lease->preferred_lft_);
+ EXPECT_EQ(Lease::TYPE_NA, lease->type_);
+ EXPECT_EQ(7, lease->iaid_);
+ EXPECT_EQ(0, lease->prefixlen_);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("two.example.com", lease->hostname_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_EQ("01:02:03:04:05", lease->hwaddr_->toText(false));
+ // Verify that added state is STATE_DEFAULT
+ EXPECT_EQ(Lease::STATE_DEFAULT, lease->state_);
+ EXPECT_FALSE(lease->getContext());
+ }
+
+ {
+ SCOPED_TRACE("Third lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+
+ // Verify that the lease attributes are correct.
+ EXPECT_EQ("2001:db8:1::3", lease->addr_.toText());
+ ASSERT_TRUE(lease->duid_);
+ EXPECT_EQ("00:01:02:03:04:05:06:0a:0b:0c:0d:0e:03", lease->duid_->toText());
+ EXPECT_EQ(200, lease->valid_lft_);
+ EXPECT_EQ(0, lease->cltt_);
+ EXPECT_EQ(8, lease->subnet_id_);
+ EXPECT_EQ(100, lease->preferred_lft_);
+ EXPECT_EQ(Lease::TYPE_NA, lease->type_);
+ EXPECT_EQ(7, lease->iaid_);
+ EXPECT_EQ(0, lease->prefixlen_);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("three.example.com", lease->hostname_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_EQ("0a:0b:0c:0d:0e", lease->hwaddr_->toText(false));
+ EXPECT_EQ(Lease::STATE_DECLINED, lease->state_);
+ EXPECT_FALSE(lease->getContext());
+ }
+
+ {
+ SCOPED_TRACE("Forth lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+
+ // Verify that the lease attributes are correct.
+ EXPECT_EQ("2001:db8:1::4", lease->addr_.toText());
+ ASSERT_TRUE(lease->duid_);
+ EXPECT_EQ("00:01:02:03:04:05:06:0a:0b:0c:0d:0e:03", lease->duid_->toText());
+ EXPECT_EQ(200, lease->valid_lft_);
+ EXPECT_EQ(0, lease->cltt_);
+ EXPECT_EQ(8, lease->subnet_id_);
+ EXPECT_EQ(100, lease->preferred_lft_);
+ EXPECT_EQ(Lease::TYPE_NA, lease->type_);
+ EXPECT_EQ(7, lease->iaid_);
+ EXPECT_EQ(0, lease->prefixlen_);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("three.example.com", lease->hostname_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_EQ("0a:0b:0c:0d:0e", lease->hwaddr_->toText(false));
+ EXPECT_EQ(Lease::STATE_DECLINED, lease->state_);
+ ASSERT_TRUE(lease->getContext());
+ EXPECT_EQ("{ \"foobar\": true }", lease->getContext()->str());
+ }
+
+}
+
+// Verifies that a lease file with fewer header columns than the
+// minimum allowed will not open.
+TEST_F(CSVLeaseFile6Test, tooFewHeaderColumns) {
+ io_.writeFile("address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev\n");
+
+ // Open should fail.
+ CSVLeaseFile6 lf(filename_);
+ ASSERT_THROW(lf.open(), CSVFileError);
+}
+
+// Verifies that a lease file with an unrecognized column header
+// will not open.
+TEST_F(CSVLeaseFile6Test, invalidHeaderColumn) {
+ io_.writeFile("address,BOGUS,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context\n");
+
+ // Open should fail.
+ CSVLeaseFile6 lf(filename_);
+ ASSERT_THROW(lf.open(), CSVFileError);
+}
+
+// Verifies that a lease file with more header columns than defined
+// columns will open as needing a downgrade.
+TEST_F(CSVLeaseFile6Test, downGrade) {
+ // Create a mixed schema file
+ io_.writeFile(
+ // schema 4.0 header
+ "address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context,hwtype,hwaddr_source,FUTURE_COLUMN\n"
+
+ // schema 4.0 record
+ "2001:db8:1::3,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:03,"
+ "200,200,8,100,0,7,0,1,1,three.example.com,0a:0b:0c:0d:0e,1,"
+ "{ \"foobar\": true },1,0,FUTURE_VALUE\n");
+
+ // Open should succeed in the event someone is downgrading.
+ CSVLeaseFile6 lf(filename_);
+ ASSERT_NO_THROW(lf.open());
+ EXPECT_TRUE(lf.needsConversion());
+ EXPECT_EQ(util::VersionedCSVFile::NEEDS_DOWNGRADE,
+ lf.getInputSchemaState());
+
+
+ Lease6Ptr lease;
+ {
+ SCOPED_TRACE("First lease valid");
+ EXPECT_TRUE(lf.next(lease));
+ ASSERT_TRUE(lease);
+
+ // Verify that the lease attributes are correct.
+ EXPECT_EQ("2001:db8:1::3", lease->addr_.toText());
+ ASSERT_TRUE(lease->duid_);
+ EXPECT_EQ("00:01:02:03:04:05:06:0a:0b:0c:0d:0e:03", lease->duid_->toText());
+ EXPECT_EQ(200, lease->valid_lft_);
+ EXPECT_EQ(0, lease->cltt_);
+ EXPECT_EQ(8, lease->subnet_id_);
+ EXPECT_EQ(100, lease->preferred_lft_);
+ EXPECT_EQ(Lease::TYPE_NA, lease->type_);
+ EXPECT_EQ(7, lease->iaid_);
+ EXPECT_EQ(0, lease->prefixlen_);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("three.example.com", lease->hostname_);
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_EQ("0a:0b:0c:0d:0e", lease->hwaddr_->toText(false));
+ EXPECT_EQ(Lease::STATE_DECLINED, lease->state_);
+ ASSERT_TRUE(lease->getContext());
+ EXPECT_EQ("{ \"foobar\": true }", lease->getContext()->str());
+ EXPECT_EQ(1, lease->hwaddr_->htype_);
+ EXPECT_EQ(0, lease->hwaddr_->source_);
+ }
+}
+
+// Verifies that leases with no DUID are invalid, and that leases
+// with the "Empty" DUID (1 byte duid = 0x0) are valid only when
+// in the declined state.
+TEST_F(CSVLeaseFile6Test, declinedLeaseTest) {
+ io_.writeFile("address,duid,valid_lifetime,expire,subnet_id,"
+ "pref_lifetime,lease_type,iaid,prefix_len,fqdn_fwd,"
+ "fqdn_rev,hostname,hwaddr,state,user_context,"
+ "hwtype,hwaddr_source\n"
+ "2001:db8:1::1,00,"
+ "200,200,8,100,0,7,0,1,1,host.example.com,,0,,,\n"
+ "2001:db8:1::1,,"
+ "200,200,8,100,0,7,0,1,1,host.example.com,,0,,,\n"
+ "2001:db8:1::1,00,"
+ "200,200,8,100,0,7,0,1,1,host.example.com,,1,,,\n");
+
+ CSVLeaseFile6 lf(filename_);
+ ASSERT_NO_THROW(lf.open());
+ EXPECT_FALSE(lf.needsConversion());
+ EXPECT_EQ(util::VersionedCSVFile::CURRENT, lf.getInputSchemaState());
+ Lease6Ptr lease;
+
+ {
+ SCOPED_TRACE("\"Empty\" DUID and not declined, invalid");
+ EXPECT_FALSE(lf.next(lease));
+ EXPECT_FALSE(lease);
+ EXPECT_EQ(lf.getReadErrs(), 1);
+ EXPECT_EQ(lf.getReadMsg(),
+ "The Empty DUID is only valid for declined leases");
+ }
+
+ {
+ SCOPED_TRACE("Missing (blank) DUID and not declined, invalid");
+ EXPECT_FALSE(lf.next(lease));
+ EXPECT_FALSE(lease);
+ EXPECT_EQ(lf.getReadErrs(), 2);
+ EXPECT_EQ(lf.getReadMsg(), "Empty DUIDs are not allowed");
+ }
+
+ {
+ SCOPED_TRACE("Empty DUID and declined, valid");
+ EXPECT_TRUE(lf.next(lease));
+ EXPECT_TRUE(lease);
+ EXPECT_EQ(lf.getReadErrs(), 2);
+ EXPECT_EQ(lf.getReadMsg(), "validation not started");
+ }
+}
+
+
+// Verifies that it is possible to output a lease with very high valid
+// lifetime (infinite in RFC2131 terms) and current time, and then read
+// back this lease.
+TEST_F(CSVLeaseFile6Test, highLeaseLifetime) {
+ CSVLeaseFile6 lf(filename_);
+ ASSERT_NO_THROW(lf.recreate());
+ ASSERT_TRUE(io_.exists());
+
+ // Write lease with very high lease lifetime and current time.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ makeDUID(DUID0, sizeof(DUID0)),
+ 7, 100, 0xFFFFFFFF, 8, true, true,
+ "host.example.com"));
+
+ // Write this lease out to the lease file.
+ ASSERT_NO_THROW(lf.append(*lease));
+
+ // Close the lease file.
+ lf.close();
+
+ Lease6Ptr lease_read;
+
+ // Re-open the file for reading.
+ ASSERT_NO_THROW(lf.open());
+
+ // Read the lease and make sure it is successful.
+ EXPECT_TRUE(lf.next(lease_read));
+ ASSERT_TRUE(lease_read);
+
+ // The valid lifetime and the cltt should match with the original lease.
+ EXPECT_EQ(lease->valid_lft_, lease_read->valid_lft_);
+ EXPECT_EQ(lease->cltt_, lease_read->cltt_);
+}
+
+// Verifies that it is possible to write and read a lease with commas
+// in hostname and user context.
+TEST_F(CSVLeaseFile6Test, embeddedCommas) {
+ CSVLeaseFile6 lf(filename_);
+ ASSERT_NO_THROW(lf.recreate());
+ ASSERT_TRUE(io_.exists());
+
+ std::string hostname("host,example,com");
+ std::string context_str("{ \"bar\": true, \"foo\": false, \"x\": \"factor\" }");
+
+ // Create a lease with commas in the hostname.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ makeDUID(DUID0, sizeof(DUID0)),
+ 7, 100, 0xFFFFFFFF, 8, true, true,
+ hostname));
+
+ // Add the user context with commas.
+ lease->setContext(Element::fromJSON(context_str));
+
+ // Write this lease out to the lease file.
+ ASSERT_NO_THROW(lf.append(*lease));
+
+ // Close the lease file.
+ lf.close();
+
+ Lease6Ptr lease_read;
+
+ // Re-open the file for reading.
+ ASSERT_NO_THROW(lf.open());
+
+ // Read the lease and make sure it is successful.
+ EXPECT_TRUE(lf.next(lease_read));
+ ASSERT_TRUE(lease_read);
+
+ // Expect the hostname and user context to retain the commas
+ // they started with.
+ EXPECT_EQ(hostname, lease->hostname_);
+ EXPECT_EQ(context_str, lease->getContext()->str());
+}
+
+// Verifies that it is possible to write and read a lease with
+// escape tags and sequences in hostname and user context.
+TEST_F(CSVLeaseFile6Test, embeddedEscapes) {
+ CSVLeaseFile6 lf(filename_);
+ ASSERT_NO_THROW(lf.recreate());
+ ASSERT_TRUE(io_.exists());
+
+ std::string hostname("host&#xexample&#x2ccom");
+ std::string context_str("{ \"&#xbar\": true, \"foo\": false, \"x\": \"fac&#x2ctor\" }");
+
+ // Create a lease with commas in the hostname.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ makeDUID(DUID0, sizeof(DUID0)),
+ 7, 100, 0xFFFFFFFF, 8, true, true,
+ hostname));
+
+ // Add the user context with commas.
+ lease->setContext(Element::fromJSON(context_str));
+
+ // Write this lease out to the lease file.
+ ASSERT_NO_THROW(lf.append(*lease));
+
+ // Close the lease file.
+ lf.close();
+
+ Lease6Ptr lease_read;
+
+ // Re-open the file for reading.
+ ASSERT_NO_THROW(lf.open());
+
+ // Read the lease and make sure it is successful.
+ EXPECT_TRUE(lf.next(lease_read));
+ ASSERT_TRUE(lease_read);
+
+ // Expect the hostname and user context to retain the commas
+ // they started with.
+ EXPECT_EQ(hostname, lease->hostname_);
+ EXPECT_EQ(context_str, lease->getContext()->str());
+}
+
+
+
+
+/// @todo Currently we don't check invalid lease attributes, such as invalid
+/// lease type, invalid preferred lifetime vs valid lifetime etc. The Lease6
+/// should be extended with the function that validates lease attributes. Once
+/// this is implemented we should provide more tests for malformed leases
+/// in the CSV file. See http://oldkea.isc.org/ticket/2405.
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/d2_client_unittest.cc b/src/lib/dhcpsrv/tests/d2_client_unittest.cc
new file mode 100644
index 0000000..c15afc5
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/d2_client_unittest.cc
@@ -0,0 +1,1240 @@
+// Copyright (C) 2012-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcp/option4_client_fqdn.h>
+#include <dhcp/option6_client_fqdn.h>
+#include <dhcpsrv/d2_client_mgr.h>
+#include <testutils/test_to_element.h>
+#include <exceptions/exceptions.h>
+#include <util/strutil.h>
+
+#include <boost/algorithm/string.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::util;
+using namespace isc::test;
+using namespace isc::data;
+using namespace isc;
+
+namespace {
+
+/// @brief Tests conversion of NameChangeFormat between enum and strings.
+TEST(ReplaceClientNameModeTest, formatEnumConversion){
+ ASSERT_EQ(D2ClientConfig::stringToReplaceClientNameMode("never"),
+ D2ClientConfig::RCM_NEVER);
+ ASSERT_EQ(D2ClientConfig::stringToReplaceClientNameMode("always"),
+ D2ClientConfig::RCM_ALWAYS);
+ ASSERT_EQ(D2ClientConfig::stringToReplaceClientNameMode("when-present"),
+ D2ClientConfig::RCM_WHEN_PRESENT);
+ ASSERT_EQ(D2ClientConfig::stringToReplaceClientNameMode("when-not-present"),
+ D2ClientConfig::RCM_WHEN_NOT_PRESENT);
+ ASSERT_THROW(D2ClientConfig::stringToReplaceClientNameMode("BOGUS"),
+ isc::BadValue);
+
+ ASSERT_EQ(D2ClientConfig::
+ replaceClientNameModeToString(D2ClientConfig::RCM_NEVER),
+ "never");
+ ASSERT_EQ(D2ClientConfig::
+ replaceClientNameModeToString(D2ClientConfig::RCM_ALWAYS),
+ "always");
+ ASSERT_EQ(D2ClientConfig::
+ replaceClientNameModeToString(D2ClientConfig::RCM_WHEN_PRESENT),
+ "when-present");
+ ASSERT_EQ(D2ClientConfig::
+ replaceClientNameModeToString(D2ClientConfig::
+ RCM_WHEN_NOT_PRESENT),
+ "when-not-present");
+}
+
+/// @brief Checks constructors and accessors of D2ClientConfig.
+TEST(D2ClientConfigTest, constructorsAndAccessors) {
+ D2ClientConfigPtr d2_client_config;
+
+ // Verify default constructor creates a disabled instance.
+ ASSERT_NO_THROW(d2_client_config.reset(new D2ClientConfig()));
+ EXPECT_FALSE(d2_client_config->getEnableUpdates());
+
+ // Verify the enable-updates can be toggled.
+ d2_client_config->enableUpdates(true);
+ EXPECT_TRUE(d2_client_config->getEnableUpdates());
+ d2_client_config->enableUpdates(false);
+ EXPECT_FALSE(d2_client_config->getEnableUpdates());
+
+ d2_client_config.reset();
+
+ bool enable_updates = true;
+ isc::asiolink::IOAddress server_ip("127.0.0.1");
+ size_t server_port = 477;
+ isc::asiolink::IOAddress sender_ip("127.0.0.1");
+ size_t sender_port = 478;
+ size_t max_queue_size = 2048;
+ dhcp_ddns::NameChangeProtocol ncr_protocol = dhcp_ddns::NCR_UDP;
+ dhcp_ddns::NameChangeFormat ncr_format = dhcp_ddns::FMT_JSON;
+ std::string generated_prefix = "the_prefix";
+ std::string qualifying_suffix = "the.suffix.";
+ std::string hostname_char_set = "[^A-Z]";
+ std::string hostname_char_replacement = "*";
+
+ // Verify that we can construct a valid, enabled instance.
+ ASSERT_NO_THROW(d2_client_config.reset(new
+ D2ClientConfig(enable_updates,
+ server_ip,
+ server_port,
+ sender_ip,
+ sender_port,
+ max_queue_size,
+ ncr_protocol,
+ ncr_format)));
+ ASSERT_TRUE(d2_client_config);
+
+ // Add user context
+ std::string user_context = "{ \"comment\": \"bar\", \"foo\": 1 }";
+ EXPECT_FALSE(d2_client_config->getContext());
+ d2_client_config->setContext(Element::fromJSON(user_context));
+
+ // Verify that the accessors return the expected values.
+ EXPECT_EQ(d2_client_config->getEnableUpdates(), enable_updates);
+
+ EXPECT_EQ(d2_client_config->getServerIp(), server_ip);
+ EXPECT_EQ(d2_client_config->getServerPort(), server_port);
+ EXPECT_EQ(d2_client_config->getSenderIp(), sender_ip);
+ EXPECT_EQ(d2_client_config->getSenderPort(), sender_port);
+ EXPECT_EQ(d2_client_config->getMaxQueueSize(), max_queue_size);
+ EXPECT_EQ(d2_client_config->getNcrProtocol(), ncr_protocol);
+ EXPECT_EQ(d2_client_config->getNcrFormat(), ncr_format);
+ ASSERT_TRUE(d2_client_config->getContext());
+ EXPECT_EQ(d2_client_config->getContext()->str(), user_context);
+
+ // Verify that toText called by << operator doesn't bomb.
+ ASSERT_NO_THROW(std::cout << "toText test:" << std::endl <<
+ *d2_client_config << std::endl);
+
+ // Verify what toElement returns.
+ std::string expected = "{\n"
+ "\"enable-updates\": true,\n"
+ "\"server-ip\": \"127.0.0.1\",\n"
+ "\"server-port\": 477,\n"
+ "\"sender-ip\": \"127.0.0.1\",\n"
+ "\"sender-port\": 478,\n"
+ "\"max-queue-size\": 2048,\n"
+ "\"ncr-protocol\": \"UDP\",\n"
+ "\"ncr-format\": \"JSON\",\n"
+ "\"user-context\": { \"foo\": 1, \"comment\": \"bar\" }\n"
+ "}\n";
+ runToElementTest<D2ClientConfig>(expected, *d2_client_config);
+
+ // Verify that constructor does not allow use of NCR_TCP.
+ /// @todo obviously this becomes invalid once TCP is supported.
+ ASSERT_THROW(d2_client_config.reset(new
+ D2ClientConfig(enable_updates,
+ server_ip,
+ server_port,
+ sender_ip,
+ sender_port,
+ max_queue_size,
+ dhcp_ddns::NCR_TCP,
+ ncr_format)),
+ D2ClientError);
+
+ Optional<std::string> opt_hostname_char_set("", true);
+ Optional<std::string> opt_hostname_char_replacement("", true);
+
+ // Verify that constructor handles optional hostname char stuff.
+ ASSERT_NO_THROW(d2_client_config.reset(new
+ D2ClientConfig(enable_updates,
+ server_ip,
+ server_port,
+ sender_ip,
+ sender_port,
+ max_queue_size,
+ ncr_protocol,
+ ncr_format)));
+ ASSERT_TRUE(d2_client_config);
+
+ // Verify what toElement returns.
+ expected = "{\n"
+ "\"enable-updates\": true,\n"
+ "\"server-ip\": \"127.0.0.1\",\n"
+ "\"server-port\": 477,\n"
+ "\"sender-ip\": \"127.0.0.1\",\n"
+ "\"sender-port\": 478,\n"
+ "\"max-queue-size\": 2048,\n"
+ "\"ncr-protocol\": \"UDP\",\n"
+ "\"ncr-format\": \"JSON\"\n"
+ "}\n";
+ runToElementTest<D2ClientConfig>(expected, *d2_client_config);
+
+ /// @todo if additional validation is added to ctor, this test needs to
+ /// expand accordingly.
+}
+
+/// @brief Tests the equality and inequality operators of D2ClientConfig.
+TEST(D2ClientConfigTest, equalityOperator) {
+ D2ClientConfigPtr ref_config;
+ D2ClientConfigPtr test_config;
+
+ isc::asiolink::IOAddress ref_address("127.0.0.1");
+ isc::asiolink::IOAddress test_address("127.0.0.2");
+
+ // Create an instance to use as a reference.
+ ASSERT_NO_THROW(ref_config.reset(new D2ClientConfig(true,
+ ref_address, 477, ref_address, 478, 1024,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON)));
+ ASSERT_TRUE(ref_config);
+
+ // Check a configuration that is identical to reference configuration.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ ref_address, 477, ref_address, 478, 1024,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON)));
+ ASSERT_TRUE(test_config);
+ EXPECT_TRUE(*ref_config == *test_config);
+ EXPECT_FALSE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by enable flag.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(false,
+ ref_address, 477, ref_address, 478, 1024,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON)));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by server ip.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ test_address, 477, ref_address, 478, 1024,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON)));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by server port.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ ref_address, 333, ref_address, 478, 1024,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON)));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by sender ip.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ ref_address, 477, test_address, 478, 1024,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON)));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by sender port.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ ref_address, 477, ref_address, 333, 1024,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON)));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by max queue size.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ ref_address, 477, ref_address, 478, 2048,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON)));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+}
+
+/// @brief Checks the D2ClientMgr constructor.
+TEST(D2ClientMgr, constructor) {
+ D2ClientMgrPtr d2_client_mgr;
+
+ // Verify we can construct with the default constructor.
+ ASSERT_NO_THROW(d2_client_mgr.reset(new D2ClientMgr()));
+
+ // After construction, D2 configuration should be disabled.
+ // Fetch it and verify this is the case.
+ D2ClientConfigPtr original_config = d2_client_mgr->getD2ClientConfig();
+ ASSERT_TRUE(original_config);
+ EXPECT_FALSE(original_config->getEnableUpdates());
+
+ // Make sure convenience method agrees.
+ EXPECT_FALSE(d2_client_mgr->ddnsEnabled());
+}
+
+/// @brief Checks passing the D2ClientMgr a valid D2 client configuration.
+/// @todo Once NameChangeSender is integrated, this test needs to expand, and
+/// additional scenario tests will need to be written.
+TEST(D2ClientMgr, validConfig) {
+ D2ClientMgrPtr d2_client_mgr;
+
+ // Construct the manager and fetch its initial configuration.
+ ASSERT_NO_THROW(d2_client_mgr.reset(new D2ClientMgr()));
+ D2ClientConfigPtr original_config = d2_client_mgr->getD2ClientConfig();
+ ASSERT_TRUE(original_config);
+
+ // Verify that we cannot set the config to an empty pointer.
+ D2ClientConfigPtr new_cfg;
+ ASSERT_THROW(d2_client_mgr->setD2ClientConfig(new_cfg), D2ClientError);
+
+ // Create a new, enabled config.
+ ASSERT_NO_THROW(new_cfg.reset(new D2ClientConfig(true,
+ isc::asiolink::IOAddress("127.0.0.1"), 477,
+ isc::asiolink::IOAddress("127.0.0.1"), 478,
+ 1024,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON)));
+
+ // Verify that we can assign a new, non-empty configuration.
+ ASSERT_NO_THROW(d2_client_mgr->setD2ClientConfig(new_cfg));
+
+ // Verify that we can fetch the newly assigned configuration.
+ D2ClientConfigPtr updated_config = d2_client_mgr->getD2ClientConfig();
+ ASSERT_TRUE(updated_config);
+ EXPECT_TRUE(updated_config->getEnableUpdates());
+
+ // Make sure convenience method agrees with the updated configuration.
+ EXPECT_TRUE(d2_client_mgr->ddnsEnabled());
+
+ // Make sure the configuration we fetched is the one we assigned,
+ // and not the original configuration.
+ EXPECT_EQ(*new_cfg, *updated_config);
+ EXPECT_NE(*original_config, *updated_config);
+}
+
+/// @brief Checks passing the D2ClientMgr a valid D2 client configuration
+/// using IPv6 service.
+TEST(D2ClientMgr, ipv6Config) {
+ D2ClientMgrPtr d2_client_mgr;
+
+ // Construct the manager and fetch its initial configuration.
+ ASSERT_NO_THROW(d2_client_mgr.reset(new D2ClientMgr()));
+ D2ClientConfigPtr original_config = d2_client_mgr->getD2ClientConfig();
+ ASSERT_TRUE(original_config);
+
+ // Create a new, enabled config.
+ D2ClientConfigPtr new_cfg;
+ ASSERT_NO_THROW(new_cfg.reset(new D2ClientConfig(true,
+ isc::asiolink::IOAddress("::1"), 477,
+ isc::asiolink::IOAddress("::1"), 478,
+ 1024,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON)));
+
+ // Verify that we can assign a new, non-empty configuration.
+ ASSERT_NO_THROW(d2_client_mgr->setD2ClientConfig(new_cfg));
+
+ // Verify that we can fetch the newly assigned configuration.
+ D2ClientConfigPtr updated_config = d2_client_mgr->getD2ClientConfig();
+ ASSERT_TRUE(updated_config);
+ EXPECT_TRUE(updated_config->getEnableUpdates());
+
+ // Make sure convenience method agrees with the updated configuration.
+ EXPECT_TRUE(d2_client_mgr->ddnsEnabled());
+
+ // Make sure the configuration we fetched is the one we assigned,
+ // and not the original configuration.
+ EXPECT_EQ(*new_cfg, *updated_config);
+ EXPECT_NE(*original_config, *updated_config);
+}
+
+/// @brief Test class for execerising manager functions that are
+/// influenced by DDNS parameters.
+class D2ClientMgrParamsTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ D2ClientMgrParamsTest() = default;
+
+ /// @brief Destructor
+ virtual ~D2ClientMgrParamsTest() = default;
+
+private:
+ /// @brief Prepares the class for a test.
+ virtual void SetUp() {
+ // Create a subnet and then a DdnsParams instance.
+ // We'll use the subnet's setters to alter DDNS parameter values.
+ subnet_.reset(new Subnet4(IOAddress("192.0.2.2"), 16, 1, 2, 3, 10));
+ ddns_params_.reset(new DdnsParams(subnet_, true));
+ }
+
+ /// @brief Cleans up after the test.
+ virtual void TearDown() {};
+
+public:
+ /// @brief Acts as the "selected" subnet. It is passed into the
+ /// constructor of ddns_params_. This allows DDNS parameters to
+ /// be modified via setters on subnet_.
+ Subnet4Ptr subnet_;
+ /// @brief Parameter instance based into D2ClientMgr functions
+ DdnsParamsPtr ddns_params_;
+};
+
+/// @brief Tests that analyzeFqdn detects invalid combination of both the
+/// client S and N flags set to true.
+TEST(D2ClientMgr, analyzeFqdnInvalidCombination) {
+ D2ClientMgr mgr;
+ bool server_s = false;
+ bool server_n = false;
+
+ DdnsParams ddns_params;
+
+ // client S=1 N=1 is invalid. analyzeFqdn should throw.
+ ASSERT_THROW(mgr.analyzeFqdn(true, true, server_s, server_n, ddns_params),
+ isc::BadValue);
+}
+
+/// @brief Tests that analyzeFqdn generates correct server S and N flags when
+/// updates are enabled and all overrides are off.
+TEST_F(D2ClientMgrParamsTest, analyzeFqdnEnabledNoOverrides) {
+ D2ClientMgr mgr;
+ bool server_s = false;
+ bool server_n = false;
+
+ // Create enabled configuration with all controls off (no overrides).
+ subnet_->setDdnsSendUpdates(true);
+ subnet_->setDdnsOverrideNoUpdate(false);
+ subnet_->setDdnsOverrideClientUpdate(false);
+ subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER);
+ subnet_->setDdnsGeneratedPrefix("");
+ subnet_->setDdnsQualifyingSuffix("");
+ subnet_->setHostnameCharSet("");
+ subnet_->setHostnameCharReplacement("");
+
+ // client S=0 N=0 means client wants to do forward update.
+ // server S should be 0 (server is not doing forward updates)
+ // and server N should be 0 (server doing reverse updates)
+ mgr.analyzeFqdn(false, false, server_s, server_n, *ddns_params_);
+ EXPECT_FALSE(server_s);
+ EXPECT_FALSE(server_n);
+
+ // client S=1 N=0 means client wants server to do forward update.
+ // server S should be 1 (server is doing forward updates)
+ // and server N should be 0 (server doing updates)
+ mgr.analyzeFqdn(true, false, server_s, server_n, *ddns_params_);
+ EXPECT_TRUE(server_s);
+ EXPECT_FALSE(server_n);
+
+
+ // client S=0 N=1 means client wants no one to do forward updates.
+ // server S should be 0 (server is not forward updates)
+ // and server N should be 1 (server is not doing any updates)
+ mgr.analyzeFqdn(false, true, server_s, server_n, *ddns_params_);
+ EXPECT_FALSE(server_s);
+ EXPECT_TRUE(server_n);
+}
+
+/// @brief Tests that analyzeFqdn generates correct server S and N flags when
+/// updates are enabled and override-no-update is on.
+TEST_F(D2ClientMgrParamsTest, analyzeFqdnEnabledOverrideNoUpdate) {
+ D2ClientMgr mgr;
+ bool server_s = false;
+ bool server_n = false;
+
+ // Create enabled configuration with override-no-update true.
+ subnet_->setDdnsSendUpdates(true);
+ subnet_->setDdnsOverrideNoUpdate(true);
+ subnet_->setDdnsOverrideClientUpdate(false);
+ subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER);
+ subnet_->setDdnsGeneratedPrefix("");
+ subnet_->setDdnsQualifyingSuffix("");
+ subnet_->setHostnameCharSet("");
+ subnet_->setHostnameCharReplacement("");
+
+ // client S=0 N=0 means client wants to do forward update.
+ // server S should be 0 (server is not doing forward updates)
+ // and server N should be 0 (server is doing reverse updates)
+ mgr.analyzeFqdn(false, false, server_s, server_n, *ddns_params_);
+ EXPECT_FALSE(server_s);
+ EXPECT_FALSE(server_n);
+
+ // client S=1 N=0 means client wants server to do forward update.
+ // server S should be 1 (server is doing forward updates)
+ // and server N should be 0 (server doing updates)
+ mgr.analyzeFqdn(true, false, server_s, server_n, *ddns_params_);
+ EXPECT_TRUE(server_s);
+ EXPECT_FALSE(server_n);
+
+ // client S=0 N=1 means client wants no one to do forward updates.
+ // server S should be 1 (server is doing forward updates)
+ // and server N should be 0 (server is doing updates)
+ mgr.analyzeFqdn(false, true, server_s, server_n, *ddns_params_);
+ EXPECT_TRUE(server_s);
+ EXPECT_FALSE(server_n);
+}
+
+/// @brief Tests that analyzeFqdn generates correct server S and N flags when
+/// updates are enabled and override-client-update is on.
+TEST_F(D2ClientMgrParamsTest, analyzeFqdnEnabledOverrideClientUpdate) {
+ D2ClientMgr mgr;
+ bool server_s = false;
+ bool server_n = false;
+
+ // Create enabled configuration with override-client-update true.
+ subnet_->setDdnsSendUpdates(true);
+ subnet_->setDdnsOverrideNoUpdate(false);
+ subnet_->setDdnsOverrideClientUpdate(true);
+ subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER);
+ subnet_->setDdnsGeneratedPrefix("");
+ subnet_->setDdnsQualifyingSuffix("");
+ subnet_->setHostnameCharSet("");
+ subnet_->setHostnameCharReplacement("");
+
+ // client S=0 N=0 means client wants to do forward update.
+ // server S should be 1 (server is doing forward updates)
+ // and server N should be 0 (server doing updates)
+ mgr.analyzeFqdn(false, false, server_s, server_n, *ddns_params_);
+ EXPECT_TRUE(server_s);
+ EXPECT_FALSE(server_n);
+
+ // client S=1 N=0 means client wants server to do forward update.
+ // server S should be 1 (server is doing forward updates)
+ // and server N should be 0 (server doing updates)
+ mgr.analyzeFqdn(true, false, server_s, server_n, *ddns_params_);
+ EXPECT_TRUE(server_s);
+ EXPECT_FALSE(server_n);
+
+ // client S=0 N=1 means client wants no one to do forward updates.
+ // server S should be 0 (server is not forward updates)
+ // and server N should be 1 (server is not doing any updates)
+ mgr.analyzeFqdn(false, true, server_s, server_n, *ddns_params_);
+ EXPECT_FALSE(server_s);
+ EXPECT_TRUE(server_n);
+}
+
+/// @brief Verifies the adustFqdnFlags template with Option4ClientFqdn objects.
+/// Ensures that the method can set the N, S, and O flags properly.
+/// Other permutations are covered by analyzeFqdnFlag tests.
+TEST_F(D2ClientMgrParamsTest, adjustFqdnFlagsV4) {
+ D2ClientMgr mgr;
+ Option4ClientFqdnPtr request;
+ Option4ClientFqdnPtr response;
+
+ // Create enabled configuration with override-no-update true.
+ subnet_->setDdnsSendUpdates(true);
+ subnet_->setDdnsOverrideNoUpdate(true);
+ subnet_->setDdnsOverrideClientUpdate(false);
+ subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER);
+ subnet_->setDdnsGeneratedPrefix("");
+ subnet_->setDdnsQualifyingSuffix("");
+ subnet_->setHostnameCharSet("");
+ subnet_->setHostnameCharReplacement("");
+
+ // client S=0 N=0 means client wants to do forward update.
+ // server S should be 0 (server is not doing forward updates)
+ // and server N should be 0 (server is doing reverse updates)
+ // and server O should be 0
+ request.reset(new Option4ClientFqdn(0, Option4ClientFqdn::RCODE_CLIENT(),
+ "", Option4ClientFqdn::PARTIAL));
+ response.reset(new Option4ClientFqdn(*request));
+ response->resetFlags();
+
+ mgr.adjustFqdnFlags<Option4ClientFqdn>(*request, *response, *ddns_params_);
+ EXPECT_FALSE(response->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_FALSE(response->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_FALSE(response->getFlag(Option4ClientFqdn::FLAG_O));
+
+ // client S=1 N=0 means client wants server to do forward update.
+ // server S should be 1 (server is doing forward updates)
+ // and server N should be 0 (server doing updates)
+ // and server O should be 0
+ request.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_S,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "", Option4ClientFqdn::PARTIAL));
+ response.reset(new Option4ClientFqdn(*request));
+ response->resetFlags();
+
+ mgr.adjustFqdnFlags<Option4ClientFqdn>(*request, *response, *ddns_params_);
+ EXPECT_TRUE(response->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_FALSE(response->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_FALSE(response->getFlag(Option4ClientFqdn::FLAG_O));
+
+ // client S=0 N=1 means client wants no one to do updates
+ // server S should be 1 (server is doing forward updates)
+ // and server N should be 0 (server doing updates)
+ // and O should be 1 (overriding client S)
+ request.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_N,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "", Option4ClientFqdn::PARTIAL));
+ response.reset(new Option4ClientFqdn(*request));
+ response->resetFlags();
+
+ mgr.adjustFqdnFlags<Option4ClientFqdn>(*request, *response, *ddns_params_);
+ EXPECT_TRUE(response->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_FALSE(response->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_TRUE(response->getFlag(Option4ClientFqdn::FLAG_O));
+}
+
+/// @brief Verified the getUpdateDirections template method with
+/// Option4ClientFqdn objects.
+TEST(D2ClientMgr, updateDirectionsV4) {
+ D2ClientMgr mgr;
+ Option4ClientFqdnPtr response;
+
+ bool do_forward = false;
+ bool do_reverse = false;
+
+ // Response S=0, N=0 should mean do reverse only.
+ response.reset(new Option4ClientFqdn(0,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "", Option4ClientFqdn::PARTIAL));
+ mgr.getUpdateDirections(*response, do_forward, do_reverse);
+ EXPECT_FALSE(do_forward);
+ EXPECT_TRUE(do_reverse);
+
+ // Response S=0, N=1 should mean don't do either.
+ response.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_N,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "", Option4ClientFqdn::PARTIAL));
+ mgr.getUpdateDirections(*response, do_forward, do_reverse);
+ EXPECT_FALSE(do_forward);
+ EXPECT_FALSE(do_reverse);
+
+ // Response S=1, N=0 should mean do both.
+ response.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_S,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "", Option4ClientFqdn::PARTIAL));
+ mgr.getUpdateDirections(*response, do_forward, do_reverse);
+ EXPECT_TRUE(do_forward);
+ EXPECT_TRUE(do_reverse);
+
+ // Response S=1, N=1 isn't possible.
+}
+
+/// @brief Tests the qualifyName method's ability to construct FQDNs
+TEST_F(D2ClientMgrParamsTest, qualifyName) {
+ D2ClientMgr mgr;
+ bool do_not_dot = false;
+ bool do_dot = true;
+
+ // Create enabled configuration
+ subnet_->setDdnsSendUpdates(true);
+ subnet_->setDdnsOverrideNoUpdate(false);
+ subnet_->setDdnsOverrideClientUpdate(false);
+ subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER);
+ subnet_->setDdnsGeneratedPrefix("prefix");
+ subnet_->setDdnsQualifyingSuffix("suffix.com");
+ subnet_->setHostnameCharSet("");
+ subnet_->setHostnameCharReplacement("");
+
+ // Verify that the qualifying suffix gets appended with a trailing dot added.
+ std::string partial_name = "somehost";
+ std::string qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot);
+ EXPECT_EQ("somehost.suffix.com.", qualified_name);
+
+ // Verify that the qualifying suffix gets appended without a trailing dot.
+ partial_name = "somehost";
+ qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_not_dot);
+ EXPECT_EQ("somehost.suffix.com", qualified_name);
+
+ // Verify that an empty suffix and false flag, does not change the name
+ subnet_->setDdnsQualifyingSuffix("");
+ partial_name = "somehost";
+ qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_not_dot);
+ EXPECT_EQ("somehost", qualified_name);
+
+ // Verify that a qualifying suffix that already has a trailing
+ // dot gets appended without doubling the dot.
+ subnet_->setDdnsQualifyingSuffix("hasdot.com.");
+ qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot);
+ EXPECT_EQ("somehost.hasdot.com.", qualified_name);
+
+ // Verify that the qualifying suffix gets appended without an
+ // extraneous dot when partial_name ends with a "."
+ qualified_name = mgr.qualifyName("somehost.", *ddns_params_, do_dot);
+ EXPECT_EQ("somehost.hasdot.com.", qualified_name);
+
+ // Verify that a name with a trailing dot does not get an extraneous
+ // dot when the suffix is blank
+ subnet_->setDdnsQualifyingSuffix("");
+ qualified_name = mgr.qualifyName("somehost.", *ddns_params_, do_dot);
+ EXPECT_EQ("somehost.", qualified_name);
+
+ // Verify that a name with no trailing dot gets just a dot when the
+ // suffix is blank
+ qualified_name = mgr.qualifyName("somehost", *ddns_params_, do_dot);
+ EXPECT_EQ("somehost.", qualified_name);
+
+ // Verify that a name with no trailing dot does not get dotted when the
+ // suffix is blank and trailing dot is false
+ qualified_name = mgr.qualifyName("somehost", *ddns_params_, do_not_dot);
+ EXPECT_EQ("somehost", qualified_name);
+
+ // Verify that a name with trailing dot gets "undotted" when the
+ // suffix is blank and trailing dot is false
+ qualified_name = mgr.qualifyName("somehost.", *ddns_params_, do_not_dot);
+ EXPECT_EQ("somehost", qualified_name);
+
+}
+
+/// @brief Tests the qualifyName method's ability to avoid duplicating
+/// qualifying suffix.
+TEST_F(D2ClientMgrParamsTest, qualifyNameWithoutDuplicatingSuffix) {
+ D2ClientMgr mgr;
+ bool do_dot = true;
+
+ // Create enabled configuration
+ subnet_->setDdnsSendUpdates(true);
+ subnet_->setDdnsOverrideNoUpdate(false);
+ subnet_->setDdnsOverrideClientUpdate(false);
+ subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER);
+ subnet_->setDdnsGeneratedPrefix("prefix");
+ subnet_->setDdnsQualifyingSuffix("suffix.com");
+ subnet_->setHostnameCharSet("");
+ subnet_->setHostnameCharReplacement("");
+
+ // Verify that the qualifying suffix does not get appended when the
+ // input name has the suffix but no trailing dot.
+ std::string partial_name = "somehost.suffix.com";
+ std::string qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot);
+ EXPECT_EQ("somehost.suffix.com.", qualified_name);
+
+ // Verify that the qualifying suffix does not get appended when the
+ // input name has the suffix and a trailing dot.
+ partial_name = "somehost.suffix.com.";
+ qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot);
+ EXPECT_EQ("somehost.suffix.com.", qualified_name);
+
+ // Verify that the qualifying suffix does get appended when the
+ // input name has the suffix embedded in it but does not begin
+ // at a label boundary.
+ partial_name = "somehost.almostsuffix.com";
+ qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot);
+ EXPECT_EQ("somehost.almostsuffix.com.suffix.com.", qualified_name);
+
+ // Verify that the qualifying suffix does get appended when the
+ // input name has the suffix embedded in it.
+ partial_name = "somehost.suffix.com.org";
+ qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot);
+ EXPECT_EQ("somehost.suffix.com.org.suffix.com.", qualified_name);
+
+ // Verify that the qualifying suffix does not get appended when the
+ // input name is the suffix itself.
+ partial_name = "suffix.com";
+ qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot);
+ EXPECT_EQ("suffix.com.", qualified_name);
+
+ subnet_->setDdnsQualifyingSuffix("one.two.suffix.com");
+ partial_name = "two.suffix.com";
+ qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot);
+ EXPECT_EQ("two.suffix.com.one.two.suffix.com.", qualified_name);
+}
+
+/// @brief Tests the generateFdqn method's ability to construct FQDNs
+TEST_F(D2ClientMgrParamsTest, generateFqdn) {
+ D2ClientMgr mgr;
+ bool do_dot = true;
+
+ // Create enabled configuration
+ subnet_->setDdnsSendUpdates(true);
+ subnet_->setDdnsOverrideNoUpdate(false);
+ subnet_->setDdnsOverrideClientUpdate(false);
+ subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER);
+ subnet_->setDdnsGeneratedPrefix("prefix");
+ subnet_->setDdnsQualifyingSuffix("suffix.com");
+ subnet_->setHostnameCharSet("");
+ subnet_->setHostnameCharReplacement("");
+
+ // Verify that it works with an IPv4 address.
+ asiolink::IOAddress v4address("192.0.2.75");
+ EXPECT_EQ("prefix-192-0-2-75.suffix.com.",
+ mgr.generateFqdn(v4address, *ddns_params_, do_dot));
+
+ // Verify that it works with an IPv6 address.
+ asiolink::IOAddress v6address("2001:db8::2");
+ EXPECT_EQ("prefix-2001-db8--2.suffix.com.",
+ mgr.generateFqdn(v6address, *ddns_params_, do_dot));
+
+ // Create a disabled config.
+ subnet_->setDdnsSendUpdates(false);
+
+ // Verify names generate properly with a disabled configuration.
+ EXPECT_EQ("prefix-192-0-2-75.suffix.com.",
+ mgr.generateFqdn(v4address, *ddns_params_, do_dot));
+ EXPECT_EQ("prefix-2001-db8--2.suffix.com.",
+ mgr.generateFqdn(v6address, *ddns_params_, do_dot));
+}
+
+/// @brief Tests adjustDomainName template method with Option4ClientFqdn
+TEST_F(D2ClientMgrParamsTest, adjustDomainNameV4) {
+ D2ClientMgr mgr;
+
+ // Create enabled configuration
+ subnet_->setDdnsSendUpdates(true);
+ subnet_->setDdnsOverrideNoUpdate(false);
+ subnet_->setDdnsOverrideClientUpdate(false);
+ subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER);
+ subnet_->setDdnsGeneratedPrefix("prefix");
+ subnet_->setDdnsQualifyingSuffix("suffix.com");
+ subnet_->setHostnameCharSet("");
+ subnet_->setHostnameCharReplacement("");
+
+ struct Scenario {
+ std::string description_;
+ D2ClientConfig::ReplaceClientNameMode mode_;
+ std::string client_name_;
+ Option4ClientFqdn::DomainNameType client_name_type_;
+ std::string expected_name_;
+ Option4ClientFqdn::DomainNameType expected_name_type_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "RCM_NEVER #1, empty client name",
+ D2ClientConfig::RCM_NEVER,
+ "", Option4ClientFqdn::PARTIAL,
+ "", Option4ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_NEVER #2, partial client name",
+ D2ClientConfig::RCM_NEVER,
+ "myhost", Option4ClientFqdn::PARTIAL,
+ "myhost.suffix.com.", Option4ClientFqdn::FULL
+ },
+ {
+ "RCM_NEVER #3, full client name",
+ D2ClientConfig::RCM_NEVER,
+ "myhost.example.com.", Option4ClientFqdn::FULL,
+ "myhost.example.com.", Option4ClientFqdn::FULL
+ },
+ {
+ "RCM_ALWAYS #1, empty client name",
+ D2ClientConfig::RCM_ALWAYS,
+ "", Option4ClientFqdn::PARTIAL,
+ "", Option4ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_ALWAYS #2, partial client name",
+ D2ClientConfig::RCM_ALWAYS,
+ "myhost", Option4ClientFqdn::PARTIAL,
+ "", Option4ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_ALWAYS #3, full client name",
+ D2ClientConfig::RCM_ALWAYS,
+ "myhost.example.com.", Option4ClientFqdn::FULL,
+ "", Option4ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_WHEN_PRESENT #1, empty client name",
+ D2ClientConfig::RCM_WHEN_PRESENT,
+ "", Option4ClientFqdn::PARTIAL,
+ "", Option4ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_WHEN_PRESENT #2, partial client name",
+ D2ClientConfig::RCM_WHEN_PRESENT,
+ "myhost", Option4ClientFqdn::PARTIAL,
+ "", Option4ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_WHEN_PRESENT #3, full client name",
+ D2ClientConfig::RCM_WHEN_PRESENT,
+ "myhost.example.com.", Option4ClientFqdn::FULL,
+ "", Option4ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_WHEN_NOT_PRESENT #1, empty client name",
+ D2ClientConfig::RCM_WHEN_NOT_PRESENT,
+ "", Option4ClientFqdn::PARTIAL,
+ "", Option4ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_WHEN_NOT_PRESENT #2, partial client name",
+ D2ClientConfig::RCM_WHEN_NOT_PRESENT,
+ "myhost", Option4ClientFqdn::PARTIAL,
+ "myhost.suffix.com.", Option4ClientFqdn::FULL
+ },
+ {
+ "RCM_WHEN_NOT_PRESENT #3, full client name",
+ D2ClientConfig::RCM_WHEN_NOT_PRESENT,
+ "myhost.example.com.", Option4ClientFqdn::FULL,
+ "myhost.example.com.", Option4ClientFqdn::FULL,
+ }
+ };
+
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+ {
+ subnet_->setDdnsReplaceClientNameMode(scenario.mode_);
+ Option4ClientFqdn request (0, Option4ClientFqdn::RCODE_CLIENT(),
+ scenario.client_name_,
+ scenario.client_name_type_);
+
+ Option4ClientFqdn response(request);
+ mgr.adjustDomainName<Option4ClientFqdn>(request, response, *ddns_params_);
+ EXPECT_EQ(scenario.expected_name_, response.getDomainName());
+ EXPECT_EQ(scenario.expected_name_type_, response.getDomainNameType());
+ }
+ }
+}
+
+
+/// @brief Tests adjustDomainName template method with Option6ClientFqdn
+TEST_F(D2ClientMgrParamsTest, adjustDomainNameV6) {
+ D2ClientMgr mgr;
+
+ // Create enabled configuration
+ subnet_->setDdnsSendUpdates(true);
+ subnet_->setDdnsOverrideNoUpdate(false);
+ subnet_->setDdnsOverrideClientUpdate(false);
+ subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER);
+ subnet_->setDdnsGeneratedPrefix("prefix");
+ subnet_->setDdnsQualifyingSuffix("suffix.com");
+ subnet_->setHostnameCharSet("");
+ subnet_->setHostnameCharReplacement("");
+
+ struct Scenario {
+ std::string description_;
+ D2ClientConfig::ReplaceClientNameMode mode_;
+ std::string client_name_;
+ Option6ClientFqdn::DomainNameType client_name_type_;
+ std::string expected_name_;
+ Option6ClientFqdn::DomainNameType expected_name_type_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "RCM_NEVER #1, empty client name",
+ D2ClientConfig::RCM_NEVER,
+ "", Option6ClientFqdn::PARTIAL,
+ "", Option6ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_NEVER #2, partial client name",
+ D2ClientConfig::RCM_NEVER,
+ "myhost", Option6ClientFqdn::PARTIAL,
+ "myhost.suffix.com.", Option6ClientFqdn::FULL
+ },
+ {
+ "RCM_NEVER #3, full client name",
+ D2ClientConfig::RCM_NEVER,
+ "myhost.example.com.", Option6ClientFqdn::FULL,
+ "myhost.example.com.", Option6ClientFqdn::FULL
+ },
+ {
+ "RCM_ALWAYS #1, empty client name",
+ D2ClientConfig::RCM_ALWAYS,
+ "", Option6ClientFqdn::PARTIAL,
+ "", Option6ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_ALWAYS #2, partial client name",
+ D2ClientConfig::RCM_ALWAYS,
+ "myhost", Option6ClientFqdn::PARTIAL,
+ "", Option6ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_ALWAYS #3, full client name",
+ D2ClientConfig::RCM_ALWAYS,
+ "myhost.example.com.", Option6ClientFqdn::FULL,
+ "", Option6ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_WHEN_PRESENT #1, empty client name",
+ D2ClientConfig::RCM_WHEN_PRESENT,
+ "", Option6ClientFqdn::PARTIAL,
+ "", Option6ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_WHEN_PRESENT #2, partial client name",
+ D2ClientConfig::RCM_WHEN_PRESENT,
+ "myhost", Option6ClientFqdn::PARTIAL,
+ "", Option6ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_WHEN_PRESENT #3, full client name",
+ D2ClientConfig::RCM_WHEN_PRESENT,
+ "myhost.example.com.", Option6ClientFqdn::FULL,
+ "", Option6ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_WHEN_NOT_PRESENT #1, empty client name",
+ D2ClientConfig::RCM_WHEN_NOT_PRESENT,
+ "", Option6ClientFqdn::PARTIAL,
+ "", Option6ClientFqdn::PARTIAL
+ },
+ {
+ "RCM_WHEN_NOT_PRESENT #2, partial client name",
+ D2ClientConfig::RCM_WHEN_NOT_PRESENT,
+ "myhost", Option6ClientFqdn::PARTIAL,
+ "myhost.suffix.com.", Option6ClientFqdn::FULL
+ },
+ {
+ "RCM_WHEN_NOT_PRESENT #3, full client name",
+ D2ClientConfig::RCM_WHEN_NOT_PRESENT,
+ "myhost.example.com.", Option6ClientFqdn::FULL,
+ "myhost.example.com.", Option6ClientFqdn::FULL,
+ }
+ };
+
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+ {
+ subnet_->setDdnsReplaceClientNameMode(scenario.mode_);
+ Option6ClientFqdn request(0, scenario.client_name_,
+ scenario.client_name_type_);
+
+ Option6ClientFqdn response(request);
+ mgr.adjustDomainName<Option6ClientFqdn>(request, response, *ddns_params_);
+ EXPECT_EQ(scenario.expected_name_, response.getDomainName());
+ EXPECT_EQ(scenario.expected_name_type_, response.getDomainNameType());
+ }
+ }
+}
+
+/// @brief Verifies the adustFqdnFlags template with Option6ClientFqdn objects.
+/// Ensures that the method can set the N, S, and O flags properly.
+/// Other permutations are covered by analyzeFqdnFlags tests.
+TEST_F(D2ClientMgrParamsTest, adjustFqdnFlagsV6) {
+ D2ClientMgr mgr;
+ Option6ClientFqdnPtr request;
+ Option6ClientFqdnPtr response;
+
+ // Create enabled configuration with override-no-update true.
+ subnet_->setDdnsSendUpdates(true);
+ subnet_->setDdnsOverrideNoUpdate(true);
+ subnet_->setDdnsOverrideClientUpdate(false);
+ subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER);
+ subnet_->setDdnsGeneratedPrefix("");
+ subnet_->setDdnsQualifyingSuffix("");
+ subnet_->setHostnameCharSet("");
+ subnet_->setHostnameCharReplacement("");
+
+ // client S=0 N=0 means client wants to do forward update.
+ // server S should be 0 (server is not doing forward updates)
+ // and server N should be 0 (server doing reverse updates)
+ // and server O should be 0
+ request.reset(new Option6ClientFqdn(0, "", Option6ClientFqdn::PARTIAL));
+ response.reset(new Option6ClientFqdn(*request));
+ response->resetFlags();
+
+ mgr.adjustFqdnFlags<Option6ClientFqdn>(*request, *response, *ddns_params_);
+ EXPECT_FALSE(response->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_FALSE(response->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_FALSE(response->getFlag(Option6ClientFqdn::FLAG_O));
+
+ // client S=1 N=0 means client wants server to do forward update.
+ // server S should be 1 (server is doing forward updates)
+ // and server N should be 0 (server doing updates)
+ // and server O should be 0
+ request.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_S,
+ "", Option6ClientFqdn::PARTIAL));
+ response.reset(new Option6ClientFqdn(*request));
+ response->resetFlags();
+
+ mgr.adjustFqdnFlags<Option6ClientFqdn>(*request, *response, *ddns_params_);
+ EXPECT_TRUE(response->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_FALSE(response->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_FALSE(response->getFlag(Option6ClientFqdn::FLAG_O));
+
+ // client S=0 N=1 means client wants no one to do updates
+ // server S should be 1 (server is doing forward updates)
+ // and server N should be 0 (server doing updates)
+ // and O should be 1 (overriding client S)
+ request.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_N,
+ "", Option6ClientFqdn::PARTIAL));
+ response.reset(new Option6ClientFqdn(*request));
+ response->resetFlags();
+
+ mgr.adjustFqdnFlags<Option6ClientFqdn>(*request, *response, *ddns_params_);
+ EXPECT_TRUE(response->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_FALSE(response->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_TRUE(response->getFlag(Option6ClientFqdn::FLAG_O));
+}
+
+
+/// @brief Verified the getUpdateDirections template method with
+/// Option6ClientFqdn objects.
+TEST(D2ClientMgr, updateDirectionsV6) {
+ D2ClientMgr mgr;
+ Option6ClientFqdnPtr response;
+
+ bool do_forward = false;
+ bool do_reverse = false;
+
+ // Response S=0, N=0 should mean do reverse only.
+ response.reset(new Option6ClientFqdn(0,
+ "", Option6ClientFqdn::PARTIAL));
+ mgr.getUpdateDirections(*response, do_forward, do_reverse);
+ EXPECT_FALSE(do_forward);
+ EXPECT_TRUE(do_reverse);
+
+ // Response S=0, N=1 should mean don't do either.
+ response.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_N,
+ "", Option6ClientFqdn::PARTIAL));
+ mgr.getUpdateDirections(*response, do_forward, do_reverse);
+ EXPECT_FALSE(do_forward);
+ EXPECT_FALSE(do_reverse);
+
+ // Response S=1, N=0 should mean do both.
+ response.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_S,
+ "", Option6ClientFqdn::PARTIAL));
+ mgr.getUpdateDirections(*response, do_forward, do_reverse);
+ EXPECT_TRUE(do_forward);
+ EXPECT_TRUE(do_reverse);
+
+ // Response S=1, N=1 isn't possible.
+}
+
+/// @brief Tests v4 FQDN name sanitizing
+TEST_F(D2ClientMgrParamsTest, sanitizeFqdnV4) {
+ D2ClientMgr mgr;
+
+ // Create enabled configuration with override-no-update true.
+ subnet_->setDdnsSendUpdates(true);
+ subnet_->setDdnsOverrideNoUpdate(false);
+ subnet_->setDdnsOverrideClientUpdate(false);
+ subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER);
+ subnet_->setDdnsGeneratedPrefix("prefix");
+ subnet_->setDdnsQualifyingSuffix("suffix.com");
+ subnet_->setHostnameCharSet("[^A-Za-z0-9-]");
+ subnet_->setHostnameCharReplacement("x");
+
+ // Get the sanitizer.
+ str::StringSanitizerPtr hostname_sanitizer;
+ ASSERT_NO_THROW(hostname_sanitizer = ddns_params_->getHostnameSanitizer());
+ ASSERT_TRUE(hostname_sanitizer);
+
+ struct Scenario {
+ std::string description_;
+ std::string client_name_;
+ Option4ClientFqdn::DomainNameType name_type_;
+ std::string expected_name_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "full FQDN, name unchanged",
+ "One.123.example.com.",
+ Option4ClientFqdn::FULL,
+ "one.123.example.com."
+ },
+ {
+ "partial FQDN, name unchanged, but qualified",
+ "One.123",
+ Option4ClientFqdn::PARTIAL,
+ "one.123.suffix.com."
+ },
+ {
+ "full FQDN, scrubbed",
+ "O#n^e.123.ex&a*mple.com.",
+ Option4ClientFqdn::FULL,
+ "oxnxe.123.exxaxmple.com."
+ },
+ {
+ "partial FQDN, scrubbed and qualified",
+ "One.1+2|3",
+ Option4ClientFqdn::PARTIAL,
+ "one.1x2x3.suffix.com."
+ },
+ {
+ "full FQDN with characters that get escaped",
+ "O n e.123.exa(m)ple.com.",
+ Option4ClientFqdn::FULL,
+ "oxnxe.123.exaxmxple.com."
+ },
+ {
+ "full FQDN with escape sequences",
+ "O\032n\032e.123.example.com.",
+ Option4ClientFqdn::FULL,
+ "oxnxe.123.example.com."
+ }
+ };
+
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+ {
+ Option4ClientFqdn request(0, Option4ClientFqdn::RCODE_CLIENT(),
+ scenario.client_name_, scenario.name_type_);
+ Option4ClientFqdn response(request);
+
+ mgr.adjustDomainName<Option4ClientFqdn>(request, response, *ddns_params_);
+ EXPECT_EQ(scenario.expected_name_, response.getDomainName());
+ EXPECT_EQ(Option4ClientFqdn::FULL, response.getDomainNameType());
+ }
+ }
+}
+
+/// @brief Tests v6 FQDN name sanitizing
+/// @todo This test currently verifies that Option6ClientFqdn::DomainName
+/// downcases strings used to construct it. For some reason, currently
+/// unknown, Option4ClientFqdn preserves the case, while Option6ClientFqdn
+/// downcases it (see setDomainName() in both classes. See Trac #5700.
+TEST_F(D2ClientMgrParamsTest, sanitizeFqdnV6) {
+ D2ClientMgr mgr;
+
+ // Create enabled configuration with override-no-update true.
+ subnet_->setDdnsSendUpdates(true);
+ subnet_->setDdnsOverrideNoUpdate(false);
+ subnet_->setDdnsOverrideClientUpdate(false);
+ subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER);
+ subnet_->setDdnsGeneratedPrefix("prefix");
+ subnet_->setDdnsQualifyingSuffix("suffix.com");
+ subnet_->setHostnameCharSet("[^A-Za-z0-9-]");
+ subnet_->setHostnameCharReplacement("x");
+
+ // Get the sanitizer.
+ str::StringSanitizerPtr hostname_sanitizer;
+ ASSERT_NO_THROW(hostname_sanitizer = ddns_params_->getHostnameSanitizer());
+ ASSERT_TRUE(hostname_sanitizer);
+
+ struct Scenario {
+ std::string description_;
+ std::string client_name_;
+ Option6ClientFqdn::DomainNameType name_type_;
+ std::string expected_name_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "full FQDN, name unchanged",
+ "One.123.example.com.",
+ Option6ClientFqdn::FULL,
+ "one.123.example.com."
+ },
+ {
+ "partial FQDN, name unchanged, but qualified",
+ "One.123",
+ Option6ClientFqdn::PARTIAL,
+ "one.123.suffix.com."
+ },
+ {
+ "full FQDN, scrubbed",
+ "O#n^e.123.ex&a*mple.com.",
+ Option6ClientFqdn::FULL,
+ "oxnxe.123.exxaxmple.com."
+ },
+ {
+ "partial FQDN, scrubbed and qualified",
+ "One.1+2|3",
+ Option6ClientFqdn::PARTIAL,
+ "one.1x2x3.suffix.com."
+ },
+ {
+ "full FQDN with characters that get escaped",
+ "O n e.123.exa(m)ple.com.",
+ Option6ClientFqdn::FULL,
+ "oxnxe.123.exaxmxple.com."
+ },
+ {
+ "full FQDN with escape sequences",
+ "O\032n\032e.123.example.com.",
+ Option6ClientFqdn::FULL,
+ "oxnxe.123.example.com."
+ }
+ };
+
+ Option6ClientFqdnPtr response;
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+ {
+ Option6ClientFqdn request(0, scenario.client_name_, scenario.name_type_);
+ Option6ClientFqdn response(request);
+
+ mgr.adjustDomainName<Option6ClientFqdn>(request, response, *ddns_params_);
+ EXPECT_EQ(scenario.expected_name_, response.getDomainName());
+ EXPECT_EQ(Option6ClientFqdn::FULL, response.getDomainNameType());
+ }
+ }
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/d2_udp_unittest.cc b/src/lib/dhcpsrv/tests/d2_udp_unittest.cc
new file mode 100644
index 0000000..2d83e97
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/d2_udp_unittest.cc
@@ -0,0 +1,504 @@
+// Copyright (C) 2014-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+/// @file d2_upd_unittest.cc Unit tests for D2ClientMgr UDP communications.
+/// Note these tests are not intended to verify the actual send and receive
+/// across UDP sockets. This level of testing is done in libdhcp-ddns.
+
+#include <config.h>
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/io_service.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcpsrv/d2_client_mgr.h>
+#include <exceptions/exceptions.h>
+
+#include <gtest/gtest.h>
+
+#include <functional>
+#include <sys/select.h>
+
+using namespace std;
+using namespace isc::dhcp;
+using namespace isc;
+namespace ph = std::placeholders;
+
+namespace {
+
+/// @brief Test fixture for exercising D2ClientMgr send management
+/// services. It inherits from D2ClientMgr to allow overriding various
+/// methods and accessing otherwise restricted member. In particular it
+/// overrides the NameChangeSender completion completion callback, allowing
+/// the injection of send errors.
+class D2ClientMgrTest : public D2ClientMgr, public ::testing::Test {
+public:
+ /// @brief If true simulates a send which completed with a failed status.
+ bool simulate_send_failure_;
+ /// @brief If true causes an exception throw in the client error handler.
+ bool error_handler_throw_;
+ /// @brief Tracks the number times the completion handler is called.
+ int callback_count_;
+ /// @brief Tracks the number of times the client error handler was called.
+ int error_handler_count_;
+
+ /// @brief Constructor
+ D2ClientMgrTest() : simulate_send_failure_(false),
+ error_handler_throw_(false),
+ callback_count_(0), error_handler_count_(0) {
+ }
+
+ /// @brief virtual Destructor
+ virtual ~D2ClientMgrTest(){
+ }
+
+ /// @brief Updates the D2ClientMgr's configuration to DDNS enabled.
+ ///
+ /// @param server_address IP address of kea-dhcp-ddns.
+ /// @param server_port IP port number of kea-dhcp-ddns.
+ /// @param protocol NCR protocol to use. (Currently only UDP is
+ /// supported).
+ void enableDdns(const std::string& server_address,
+ const size_t server_port,
+ const dhcp_ddns::NameChangeProtocol protocol) {
+ // Update the configuration with one that is enabled.
+ D2ClientConfigPtr new_cfg;
+
+ isc::asiolink::IOAddress server_ip(server_address);
+ isc::asiolink::IOAddress sender_ip(server_ip.isV4() ?
+ D2ClientConfig::DFT_V4_SENDER_IP :
+ D2ClientConfig::DFT_V6_SENDER_IP);
+
+ ASSERT_NO_THROW(new_cfg.reset(new D2ClientConfig(true,
+ server_ip, server_port,
+ sender_ip, D2ClientConfig::DFT_SENDER_PORT,
+ D2ClientConfig::DFT_MAX_QUEUE_SIZE,
+ protocol, dhcp_ddns::FMT_JSON)));
+
+ ASSERT_NO_THROW(setD2ClientConfig(new_cfg));
+ ASSERT_TRUE(ddnsEnabled());
+ }
+
+ /// @brief Checks sender's select-fd against an expected state of readiness.
+ ///
+ /// Uses select() to determine if the sender's select_fd is marked as
+ /// ready to read, and compares this against the expected state. The
+ /// select function is called with a timeout of 0.0 (non blocking).
+ ///
+ /// @param expect_ready Expected state of readiness (True if expecting
+ /// a ready to ready result, false if expecting otherwise).
+ void selectCheck(bool expect_ready) {
+ fd_set read_fds;
+ int maxfd = 0;
+
+ FD_ZERO(&read_fds);
+
+ // cppcheck-suppress redundantAssignment
+ int select_fd = -1;
+ ASSERT_NO_THROW(
+ // cppcheck-suppress redundantAssignment
+ select_fd = getSelectFd()
+ );
+
+ FD_SET(select_fd, &read_fds);
+ maxfd = select_fd;
+
+ struct timeval select_timeout;
+ select_timeout.tv_sec = 0;
+ select_timeout.tv_usec = 0;
+
+ int result = (select(maxfd + 1, &read_fds, NULL, NULL,
+ &select_timeout));
+
+ if (result < 0) {
+ const char *errstr = strerror(errno);
+ FAIL() << "select failed :" << errstr;
+ }
+
+ if (expect_ready) {
+ ASSERT_TRUE(result > 0);
+ } else {
+ ASSERT_TRUE(result == 0);
+ }
+ }
+
+ /// @brief Overrides base class completion callback.
+ ///
+ /// This method will be invoked each time a send completes. It allows
+ /// intervention prior to calling the production implementation in the
+ /// base. If simulate_send_failure_ is true, the base call impl will
+ /// be called with an error status, otherwise it will be called with
+ /// the result parameter given.
+ ///
+ /// @param result Result code of the send operation.
+ /// @param ncr NameChangeRequest which failed to send.
+ virtual void operator()(const dhcp_ddns::NameChangeSender::Result result,
+ dhcp_ddns::NameChangeRequestPtr& ncr) {
+ ++callback_count_;
+ if (simulate_send_failure_) {
+ simulate_send_failure_ = false;
+ D2ClientMgr::operator()(dhcp_ddns::NameChangeSender::ERROR, ncr);
+ } else {
+ D2ClientMgr::operator()(result, ncr);
+ }
+ }
+
+ /// @brief Serves as the "application level" client error handler.
+ ///
+ /// This method is passed into calls to startSender as the client error
+ /// handler. It should be invoked whenever the completion callback is
+ /// passed a result other than SUCCESS. If error_handler_throw_
+ /// is true it will throw an exception.
+ ///
+ /// @param result unused - Result code of the send operation.
+ /// @param ncr unused -NameChangeRequest which failed to send.
+ void error_handler(const dhcp_ddns::NameChangeSender::Result /*result*/,
+ dhcp_ddns::NameChangeRequestPtr& /*ncr*/) {
+ if (error_handler_throw_) {
+ error_handler_throw_ = false;
+ isc_throw(isc::InvalidOperation, "Simulated client handler throw");
+ }
+
+ ++error_handler_count_;
+ }
+
+ /// @brief Returns D2ClientErroHandler bound to this::error_handler_.
+ D2ClientErrorHandler getErrorHandler() {
+ return (std::bind(&D2ClientMgrTest::error_handler, this, ph::_1, ph::_2));
+ }
+
+ /// @brief Constructs a NameChangeRequest message from a fixed JSON string.
+ dhcp_ddns::NameChangeRequestPtr buildTestNcr() {
+ // Build an NCR from json string.
+ const char* ncr_str =
+ "{"
+ " \"change-type\" : 0 , "
+ " \"forward-change\" : true , "
+ " \"reverse-change\" : false , "
+ " \"fqdn\" : \"myhost.example.com.\" , "
+ " \"ip-address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease-expires-on\" : \"20140121132405\" , "
+ " \"lease-length\" : 1300, "
+ " \"use-conflict-resolution\" : true "
+ "}";
+
+ return (dhcp_ddns::NameChangeRequest::fromJSON(ncr_str));
+ }
+
+ /// Expose restricted members.
+ using D2ClientMgr::getSelectFd;
+};
+
+
+/// @brief Checks that D2ClientMgr disable and enable a UDP sender.
+TEST_F(D2ClientMgrTest, udpSenderEnableDisable) {
+ // Verify DDNS is disabled by default.
+ ASSERT_FALSE(ddnsEnabled());
+
+ // Verify we are not in send mode.
+ ASSERT_FALSE(amSending());
+
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+ ASSERT_FALSE(amSending());
+
+ ASSERT_NO_THROW(startSender(getErrorHandler()));
+ ASSERT_TRUE(amSending());
+
+ // Verify that we take sender out of send mode.
+ ASSERT_NO_THROW(stopSender());
+ ASSERT_FALSE(amSending());
+}
+
+/// @brief Checks D2ClientMgr queuing methods with a UDP sender.
+TEST_F(D2ClientMgrTest, udpSenderQueing) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+ ASSERT_FALSE(amSending());
+
+ // Queue should be empty.
+ EXPECT_EQ(0, getQueueSize());
+
+ // Trying to peek past the end of the queue should throw.
+ EXPECT_THROW(peekAt(1), dhcp_ddns::NcrSenderError);
+
+ // Trying to send a NCR when not in send mode should fail.
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ EXPECT_THROW(sendRequest(ncr), D2ClientError);
+
+ // Place sender in send mode.
+ ASSERT_NO_THROW(startSender(getErrorHandler()));
+ ASSERT_TRUE(amSending());
+
+ // Send should succeed now.
+ ASSERT_NO_THROW(sendRequest(ncr));
+
+ // Queue should have 1 entry.
+ EXPECT_EQ(1, getQueueSize());
+
+ // Attempt to fetch the entry we just queued.
+ dhcp_ddns::NameChangeRequestPtr ncr2;
+ ASSERT_NO_THROW(ncr2 = peekAt(0));
+
+ // Verify what we queued matches what we fetched.
+ EXPECT_TRUE(*ncr == *ncr2);
+
+ // Clearing the queue while in send mode should fail.
+ ASSERT_THROW(clearQueue(), dhcp_ddns::NcrSenderError);
+
+ // We should still have 1 in the queue.
+ EXPECT_EQ(1, getQueueSize());
+
+ // Get out of send mode.
+ ASSERT_NO_THROW(stopSender());
+ ASSERT_FALSE(amSending());
+
+ // Clear queue should succeed now.
+ ASSERT_NO_THROW(clearQueue());
+ EXPECT_EQ(0, getQueueSize());
+}
+
+/// @brief Checks that D2ClientMgr can send with a UDP sender and
+/// a private IOService.
+TEST_F(D2ClientMgrTest, udpSend) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+
+ // Trying to fetch the select-fd when not sending should fail.
+ ASSERT_THROW(getSelectFd(), D2ClientError);
+
+ // Place sender in send mode.
+ ASSERT_NO_THROW(startSender(getErrorHandler()));
+
+ // select_fd should evaluate to NOT ready to read.
+ selectCheck(false);
+
+ // Build a test request and send it.
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ ASSERT_NO_THROW(sendRequest(ncr));
+
+ // select_fd should evaluate to ready to read.
+ selectCheck(true);
+
+ // Call service handler.
+ runReadyIO();
+
+ // select_fd should evaluate to not ready to read.
+ selectCheck(false);
+}
+
+/// @brief Checks that D2ClientMgr can send with a UDP sender and
+/// an external IOService.
+TEST_F(D2ClientMgrTest, udpSendExternalIOService) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ enableDdns("127.0.0.1", 53001, dhcp_ddns::NCR_UDP);
+
+ // Place sender in send mode using an external IO service.
+ asiolink::IOService io_service;
+ ASSERT_NO_THROW(startSender(getErrorHandler(), io_service));
+
+ // select_fd should evaluate to NOT ready to read.
+ selectCheck(false);
+
+ // Build a test request and send it.
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ ASSERT_NO_THROW(sendRequest(ncr));
+
+ // select_fd should evaluate to ready to read.
+ selectCheck(true);
+
+ // Call service handler.
+ runReadyIO();
+
+ // select_fd should evaluate to not ready to read.
+ selectCheck(false);
+
+ // Explicitly stop the sender. This ensures the sender's
+ // ASIO socket is closed prior to the local io_service
+ // instance goes out of scope.
+ ASSERT_NO_THROW(stopSender());
+}
+
+/// @brief Checks that D2ClientMgr can send with a UDP sender and
+/// an external IOService.
+TEST_F(D2ClientMgrTest, udpSendExternalIOService6) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ enableDdns("::1", 53001, dhcp_ddns::NCR_UDP);
+
+ // Place sender in send mode using an external IO service.
+ asiolink::IOService io_service;
+ ASSERT_NO_THROW(startSender(getErrorHandler(), io_service));
+
+ // select_fd should evaluate to NOT ready to read.
+ selectCheck(false);
+
+ // Build a test request and send it.
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ ASSERT_NO_THROW(sendRequest(ncr));
+
+ // select_fd should evaluate to ready to read.
+ selectCheck(true);
+
+ // Call service handler.
+ runReadyIO();
+
+ // select_fd should evaluate to not ready to read.
+ selectCheck(false);
+
+ // Explicitly stop the sender. This ensures the sender's
+ // ASIO socket is closed prior to the local io_service
+ // instance goes out of scope.
+ ASSERT_NO_THROW(stopSender());
+}
+
+
+/// @brief Checks that D2ClientMgr invokes the client error handler
+/// when send errors occur.
+TEST_F(D2ClientMgrTest, udpSendErrorHandler) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ // Place sender in send mode.
+ enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+ ASSERT_NO_THROW(startSender(getErrorHandler()));
+
+ // Simulate a failed response in the send call back. This should
+ // cause the error handler to get invoked.
+ simulate_send_failure_ = true;
+
+ // Verify error count is zero.
+ ASSERT_EQ(0, error_handler_count_);
+
+ // Send a test request.
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ ASSERT_NO_THROW(sendRequest(ncr));
+
+ // Call the ready handler. This should complete the message with an error.
+ ASSERT_NO_THROW(runReadyIO());
+
+ // If we executed error handler properly, the error count should one.
+ ASSERT_EQ(1, error_handler_count_);
+}
+
+
+/// @brief Checks that client error handler exceptions are handled gracefully.
+TEST_F(D2ClientMgrTest, udpSendErrorHandlerThrow) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ // Place sender in send mode.
+ enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+ ASSERT_NO_THROW(startSender(getErrorHandler()));
+
+ // Simulate a failed response in the send call back and
+ // force a throw in the error handler.
+ simulate_send_failure_ = true;
+ error_handler_throw_ = true;
+
+ // Verify error count is zero.
+ ASSERT_EQ(0, error_handler_count_);
+
+ // Send a test request.
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ ASSERT_NO_THROW(sendRequest(ncr));
+
+ // Call the ready handler. This should complete the message with an error.
+ // The handler should throw but the exception should not escape.
+ ASSERT_NO_THROW(runReadyIO());
+
+ // If throw flag is false, then we were in the error handler should
+ // have thrown.
+ ASSERT_FALSE(error_handler_throw_);
+
+ // If error count is still zero, then we did throw.
+ ASSERT_EQ(0, error_handler_count_);
+}
+
+/// @brief Tests that D2ClientMgr registers and unregisters with IfaceMgr.
+TEST_F(D2ClientMgrTest, ifaceRegister) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+
+ // Place sender in send mode.
+ ASSERT_NO_THROW(startSender(getErrorHandler()));
+
+ // Queue three messages.
+ for (unsigned i = 0; i < 3; ++i) {
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ ASSERT_NO_THROW(sendRequest(ncr));
+ }
+
+ // Make sure queue count is correct.
+ EXPECT_EQ(3, getQueueSize());
+
+ // select_fd should evaluate to ready to read.
+ selectCheck(true);
+
+ // Calling receive should complete the first message and start the second.
+ IfaceMgr::instance().receive4(0, 0);
+
+ // Verify the callback handler was invoked, no errors counted.
+ EXPECT_EQ(2, getQueueSize());
+ ASSERT_EQ(1, callback_count_);
+ ASSERT_EQ(0, error_handler_count_);
+
+ // Stop the sender. This should complete the second message but leave
+ // the third in the queue.
+ ASSERT_NO_THROW(stopSender());
+ EXPECT_EQ(1, getQueueSize());
+ ASSERT_EQ(2, callback_count_);
+ ASSERT_EQ(0, error_handler_count_);
+
+ // Calling receive again should have no affect.
+ IfaceMgr::instance().receive4(0, 0);
+ EXPECT_EQ(1, getQueueSize());
+ ASSERT_EQ(2, callback_count_);
+ ASSERT_EQ(0, error_handler_count_);
+}
+
+/// @brief Checks that D2ClientMgr suspendUpdates works properly.
+TEST_F(D2ClientMgrTest, udpSuspendUpdates) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ // Place sender in send mode.
+ enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+ ASSERT_NO_THROW(startSender(getErrorHandler()));
+
+ // Send a test request.
+ for (unsigned i = 0; i < 3; ++i) {
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ ASSERT_NO_THROW(sendRequest(ncr));
+ }
+ ASSERT_EQ(3, getQueueSize());
+
+ // Call the ready handler. This should complete the first message
+ // and initiate sending the second message.
+ ASSERT_NO_THROW(runReadyIO());
+
+ // Queue count should have gone down by 1.
+ ASSERT_EQ(2, getQueueSize());
+
+ // Suspend updates. This should disable updates and stop the sender.
+ ASSERT_NO_THROW(suspendUpdates());
+
+ EXPECT_FALSE(ddnsEnabled());
+ EXPECT_FALSE(amSending());
+
+ // Stopping the sender should have completed the second message's
+ // in-progress send, so queue size should be 1.
+ ASSERT_EQ(1, getQueueSize());
+}
+
+/// @brief Tests that invokeErrorHandler does not fail if there is no handler.
+TEST_F(D2ClientMgrTest, missingErrorHandler) {
+ // Ensure we aren't in send mode.
+ ASSERT_FALSE(ddnsEnabled());
+ ASSERT_FALSE(amSending());
+
+ // There is no error handler at this point, so invoking should not throw.
+ dhcp_ddns::NameChangeRequestPtr ncr;
+ ASSERT_NO_THROW(invokeClientErrorHandler(dhcp_ddns::NameChangeSender::ERROR,
+ ncr));
+
+ // Verify we didn't invoke the error handler, error count is zero.
+ ASSERT_EQ(0, error_handler_count_);
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/dhcp4o6_ipc_unittest.cc b/src/lib/dhcpsrv/tests/dhcp4o6_ipc_unittest.cc
new file mode 100644
index 0000000..4fbb2a4
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/dhcp4o6_ipc_unittest.cc
@@ -0,0 +1,599 @@
+// Copyright (C) 2015-2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcp/iface_mgr.h>
+#include <dhcp/pkt6.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcp/option6_addrlst.h>
+#include <dhcp/option_string.h>
+#include <dhcp/option_vendor.h>
+#include <dhcp/option_int.h>
+#include <dhcpsrv/dhcp4o6_ipc.h>
+#include <dhcpsrv/testutils/dhcp4o6_test_ipc.h>
+
+#include <gtest/gtest.h>
+#include <functional>
+#include <sstream>
+#include <string>
+
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Test port.
+const uint16_t TEST_PORT = 12345;
+
+/// @brief Number of iterations used by the tests.
+const uint16_t TEST_ITERATIONS = 10;
+
+/// @brief Type definition for the function creating DHCP message.
+typedef std::function<Pkt6Ptr(uint16_t, uint16_t)> CreateMsgFun;
+
+/// @brief Define short name for test IPC class.
+typedef Dhcp4o6TestIpc TestIpc;
+
+/// @brief Test fixture class for @c Dhcp4o6IpcBase.
+class Dhcp4o6IpcBaseTest : public ::testing::Test {
+protected:
+
+ /// @brief Constructor.
+ ///
+ /// Replaces the real configuration of interfaces with a fake configuration.
+ /// The IPC uses the @c IfaceMgr to check whether the interfaces which names
+ /// are carried within DHCP options exist in the system. Providing the fake
+ /// configuration for the @c IfaceMgr guarantees that the configuration is
+ /// consistent on any machine running the unit tests.
+ Dhcp4o6IpcBaseTest();
+
+ /// @brief Concatenates the prefix and postfix.
+ ///
+ /// @param prefix Prefix.
+ /// @param postfix Postfix.
+ /// @return String representing concatenated prefix and postfix.
+ static std::string concatenate(const std::string& prefix, uint16_t postfix);
+
+ /// @brief Creates an instance of the DHCPv4o6 message.
+ ////
+ /// @param msg_type Message type.
+ /// @param postfix Postfix to be appended to the remote address. For example,
+ /// for postfix = 5 the resulting remote address will be 2001:db8:1::5.
+ /// The postfix value is also used to generate the postfix for the interface.
+ /// The possible interface names are "eth0" and "eth1". For even postfix values
+ /// the "eth0" will be used, for odd postfix values "eth1" will be used.
+ ///
+ /// @return Pointer to the created message.
+ static Pkt6Ptr createDHCPv4o6Message(uint16_t msg_type,
+ uint16_t postfix = 0);
+
+ /// @brief Creates an instance of the DHCPv4o6 message with vendor option.
+ ///
+ /// @param msg_type Message type.
+ /// @param postfix Postfix to be appended to the remote address. See the
+ /// documentation of @c createDHCPv4o6Message for details.
+ /// @param enterprise_id Enterprise ID for the vendor option.
+ ///
+ /// @return Pointer to the created message.
+ static Pkt6Ptr createDHCPv4o6MsgWithVendorOption(uint16_t msg_type,
+ uint16_t postfix,
+ uint32_t enterprise_id);
+
+ /// @brief Creates an instance of the DHCPv4o6 message with ISC
+ /// vendor option.
+ ///
+ /// This is useful to test scenarios when the IPC is forwarding messages
+ /// that contain vendor option with ISC enterprise ID.
+ ///
+ /// @param msg_type Message type.
+ /// @param postfix Postfix to be appended to the remote address. See the
+ /// documentation of @c createDHCPv4o6Message for details.
+ ///
+ /// @return Pointer to the created message.
+ static Pkt6Ptr createDHCPv4o6MsgWithISCVendorOption(uint16_t msg_type,
+ uint16_t postfix);
+
+ /// @brief Creates an instance of the DHCPv4o6 message with vendor
+ /// option holding enterprise id of 32000.
+ ///
+ /// This is useful to test scenarios when the IPC is forwarding messages
+ /// that contain some vendor option and when IPC also appends the ISC
+ /// vendor option to carry some DHCPv4o6 specific information.
+ ///
+ /// @param msg_type Message type.
+ /// @param postfix Postfix to be appended to the remote address. See the
+ /// documentation of @c createDHCPv4o6Message for details.
+ ///
+ /// @return Pointer to the created message.
+ static Pkt6Ptr createDHCPv4o6MsgWithAnyVendorOption(uint16_t msg_type,
+ uint16_t postfix);
+
+ /// @brief Creates an instance of the DHCPv4o6 Message option.
+ ///
+ /// @param src Type of the source endpoint. It can be 4 or 6.
+ /// @return Pointer to the instance of the option.
+ static OptionPtr createDHCPv4MsgOption(TestIpc::EndpointType src);
+
+ /// @brief Tests sending and receiving packets over the IPC.
+ ///
+ /// @param iterations_num Number of packets to be sent over the IPC.
+ /// @param src Type of the source IPC endpoint. It can be 4 or 6.
+ /// @param dest Type of the destination IPC endpoint. It can be 4 or 6.
+ /// @param create_msg_fun Function called to create the packet.
+ void testSendReceive(uint16_t iterations_num,
+ TestIpc::EndpointType src,
+ TestIpc::EndpointType dest,
+ const CreateMsgFun& create_msg_fun);
+
+ /// @brief Tests that error is reported when invalid message is received.
+ ///
+ /// @param pkt Pointer to the invalid message.
+ void testReceiveError(const Pkt6Ptr& pkt);
+
+private:
+
+ /// @brief Holds the fake configuration of the interfaces.
+ IfaceMgrTestConfig iface_mgr_test_config_;
+
+};
+
+Dhcp4o6IpcBaseTest::Dhcp4o6IpcBaseTest()
+ : iface_mgr_test_config_(true) {
+}
+
+std::string
+Dhcp4o6IpcBaseTest::concatenate(const std::string& prefix,
+ uint16_t postfix) {
+ std::ostringstream s;
+ s << prefix << postfix;
+ return (s.str());
+}
+
+Pkt6Ptr
+Dhcp4o6IpcBaseTest::createDHCPv4o6Message(uint16_t msg_type,
+ uint16_t postfix) {
+ // Create the DHCPv4o6 message.
+ Pkt6Ptr pkt(new Pkt6(msg_type, 0));
+
+ // The interface name is carried in the dedicated option between
+ // the servers. The receiving server will check that such interface
+ // is present in the system. The fake configuration we're using for
+ // this test includes two interfaces: "eth0" and "eth1". Therefore,
+ // we pick one or another, depending on the index of the iteration.
+ pkt->setIface(concatenate("eth", postfix % 2));
+ pkt->setIndex(ETH0_INDEX + postfix % 2);
+
+ // The remote address of the sender of the DHCPv6 packet is carried
+ // between the servers in the dedicated option. We use different
+ // address for each iteration to make sure that the IPC delivers the
+ // right address.
+ pkt->setRemoteAddr(IOAddress(concatenate("2001:db8:1::", postfix)));
+
+ // The remote port of the sender of the DHCPv6 packet is carried
+ // between the servers in the dedicated option.
+ pkt->setRemotePort(10000 + (postfix % 1000));
+
+ // Determine the endpoint type using the message type.
+ TestIpc::EndpointType src = (msg_type == DHCPV6_DHCPV4_QUERY) ?
+ TestIpc::ENDPOINT_TYPE_V6 : TestIpc::ENDPOINT_TYPE_V4;
+
+ // Add DHCPv4 Message option to make sure it is conveyed by the IPC.
+ pkt->addOption(createDHCPv4MsgOption(src));
+
+ return (pkt);
+}
+
+Pkt6Ptr
+Dhcp4o6IpcBaseTest::createDHCPv4o6MsgWithVendorOption(uint16_t msg_type,
+ uint16_t postfix,
+ uint32_t enterprise_id) {
+ Pkt6Ptr pkt = createDHCPv4o6Message(msg_type, postfix);
+
+ // Create vendor option with ISC enterprise id.
+ OptionVendorPtr option_vendor(new OptionVendor(Option::V6, enterprise_id));
+
+ // Add some option to the vendor option.
+ option_vendor->addOption(OptionPtr(new Option(Option::V6, 100)));
+
+ // Add vendor option to the message.
+ pkt->addOption(option_vendor);
+
+ return (pkt);
+}
+
+Pkt6Ptr
+Dhcp4o6IpcBaseTest::createDHCPv4o6MsgWithISCVendorOption(uint16_t msg_type,
+ uint16_t postfix) {
+ return (createDHCPv4o6MsgWithVendorOption(msg_type, postfix, ENTERPRISE_ID_ISC));
+}
+
+Pkt6Ptr
+Dhcp4o6IpcBaseTest::createDHCPv4o6MsgWithAnyVendorOption(uint16_t msg_type,
+ uint16_t postfix) {
+ return (createDHCPv4o6MsgWithVendorOption(msg_type, postfix, 32000));
+}
+
+OptionPtr
+Dhcp4o6IpcBaseTest::createDHCPv4MsgOption(TestIpc::EndpointType src) {
+ // Create the DHCPv4 message.
+ Pkt4Ptr pkt(new Pkt4(src == TestIpc::ENDPOINT_TYPE_V4 ? DHCPACK : DHCPREQUEST,
+ 1234));
+ // Make a wire representation of the DHCPv4 message.
+ pkt->pack();
+ OutputBuffer& output_buffer = pkt->getBuffer();
+ const uint8_t* data = static_cast<const uint8_t*>(output_buffer.getData());
+ OptionBuffer option_buffer(data, data + output_buffer.getLength());
+
+ // Create the DHCPv4 Message option holding the created message.
+ OptionPtr opt_msg(new Option(Option::V6, D6O_DHCPV4_MSG, option_buffer));
+ return (opt_msg);
+}
+
+void
+Dhcp4o6IpcBaseTest::testSendReceive(uint16_t iterations_num,
+ TestIpc::EndpointType src,
+ TestIpc::EndpointType dest,
+ const CreateMsgFun& create_msg_fun) {
+ // Create IPC instances representing the source and destination endpoints.
+ TestIpc ipc_src(TEST_PORT, src);
+ TestIpc ipc_dest(TEST_PORT, dest);
+
+ // Open the IPC on both ends.
+ ASSERT_NO_THROW(ipc_src.open());
+ ASSERT_NO_THROW(ipc_dest.open());
+
+ // Depending if we're sending from DHCPv6 to DHCPv4 or the opposite
+ // direction we use different message type. This is not really required
+ // for testing IPC, but it better simulates the real use case.
+ uint16_t msg_type = (src == TestIpc::ENDPOINT_TYPE_V6 ? DHCPV6_DHCPV4_QUERY :
+ DHCPV6_DHCPV4_RESPONSE);
+
+ std::vector<bool> has_vendor_option;
+
+ // Send the number of messages configured for the test.
+ for (uint16_t i = 1; i <= iterations_num; ++i) {
+ // Create the DHCPv4o6 message.
+ Pkt6Ptr pkt = create_msg_fun(msg_type, i);
+
+ // Remember if the vendor option exists in the source packet. The
+ // received packet should also contain this option if it exists
+ // in the source packet.
+ has_vendor_option.push_back(static_cast<bool>(pkt->getOption(D6O_VENDOR_OPTS)));
+
+ // Actually send the message through the IPC.
+ ASSERT_NO_THROW(ipc_src.send(pkt))
+ << "Failed to send the message over the IPC for iteration " << i;
+ }
+
+ // Try to receive all messages.
+ for (uint16_t i = 1; i <= iterations_num; ++i) {
+
+ // Call receive with a timeout. The data should appear on the socket
+ // within this time.
+ ASSERT_NO_THROW(IfaceMgr::instance().receive6(1, 0));
+
+ // Pop the received message.
+ Pkt6Ptr pkt_received = ipc_dest.popPktReceived();
+ ASSERT_TRUE(pkt_received);
+
+ // Check that the message type is correct.
+ EXPECT_EQ(msg_type, pkt_received->getType());
+
+ // Check that the interface is correct.
+ EXPECT_EQ(concatenate("eth", i % 2), pkt_received->getIface());
+ EXPECT_EQ(ETH0_INDEX + i % 2, pkt_received->getIndex());
+
+ // Check that the address conveyed is correct.
+ EXPECT_EQ(concatenate("2001:db8:1::", i),
+ pkt_received->getRemoteAddr().toText());
+
+ // Check that the port conveyed is correct.
+ EXPECT_EQ(10000 + (i % 1000), pkt_received->getRemotePort());
+
+ // Check that encapsulated DHCPv4 message has been received.
+ EXPECT_TRUE(pkt_received->getOption(D6O_DHCPV4_MSG));
+
+ if (has_vendor_option[i - 1]) {
+ // Make sure that the vendor option wasn't deleted when the packet was
+ // received.
+ OptionPtr option_vendor = pkt_received->getOption(D6O_VENDOR_OPTS);
+ ASSERT_TRUE(option_vendor)
+ << "vendor option deleted in the received DHCPv4o6 packet for"
+ " iteration " << i;
+
+ // ISC_V6_4O6_INTERFACE shouldn't be present.
+ EXPECT_FALSE(option_vendor->getOption(ISC_V6_4O6_INTERFACE));
+
+ // ISC_V6_4O6_SRC_ADDRESS shouldn't be present.
+ EXPECT_FALSE(option_vendor->getOption(ISC_V6_4O6_SRC_ADDRESS));
+
+ } else {
+ EXPECT_FALSE(pkt_received->getOption(D6O_VENDOR_OPTS));
+ }
+ }
+}
+
+void
+Dhcp4o6IpcBaseTest::testReceiveError(const Pkt6Ptr& pkt) {
+ TestIpc ipc_src(TEST_PORT, TestIpc::ENDPOINT_TYPE_V6);
+ TestIpc ipc_dest(TEST_PORT, TestIpc::ENDPOINT_TYPE_V4);
+
+ // Open the IPC on both ends.
+ ASSERT_NO_THROW(ipc_src.open());
+ ASSERT_NO_THROW(ipc_dest.open());
+
+ pkt->setIface("eth0");
+ pkt->setIndex(ETH0_INDEX);
+ pkt->setRemoteAddr(IOAddress("2001:db8:1::1"));
+ pkt->setRemotePort(TEST_PORT);
+ pkt->addOption(createDHCPv4MsgOption(TestIpc::ENDPOINT_TYPE_V6));
+
+ OutputBuffer& buf = pkt->getBuffer();
+ buf.clear();
+ ASSERT_NO_THROW(pkt->pack());
+
+ ASSERT_NE(-1, ::send(ipc_src.getSocketFd(), buf.getData(),
+ buf.getLength(), 0));
+
+ // Call receive with a timeout. The data should appear on the socket
+ // within this time.
+ ASSERT_THROW(IfaceMgr::instance().receive6(1, 0), Dhcp4o6IpcError);
+}
+
+
+// This test verifies that the IPC can transmit messages between the
+// DHCPv4 and DHCPv6 server.
+TEST_F(Dhcp4o6IpcBaseTest, send4To6) {
+ testSendReceive(TEST_ITERATIONS, TestIpc::ENDPOINT_TYPE_V4,
+ TestIpc::ENDPOINT_TYPE_V6, &createDHCPv4o6Message);
+}
+
+// This test verifies that the IPC can transmit messages between the
+// DHCPv6 and DHCPv4 server.
+TEST_F(Dhcp4o6IpcBaseTest, send6To4) {
+ testSendReceive(TEST_ITERATIONS, TestIpc::ENDPOINT_TYPE_V6,
+ TestIpc::ENDPOINT_TYPE_V4, &createDHCPv4o6Message);
+}
+
+// This test verifies that the IPC will transmit message already containing
+// vendor option with ISC enterprise ID, between the DHCPv6 and DHCPv4
+// server.
+TEST_F(Dhcp4o6IpcBaseTest, send6To4WithISCVendorOption) {
+ testSendReceive(TEST_ITERATIONS, TestIpc::ENDPOINT_TYPE_V6,
+ TestIpc::ENDPOINT_TYPE_V4,
+ &createDHCPv4o6MsgWithISCVendorOption);
+}
+
+// This test verifies that the IPC will transmit message already containing
+// vendor option with ISC enterprise ID, between the DHCPv6 and DHCPv4
+// server.
+TEST_F(Dhcp4o6IpcBaseTest, send4To6WithISCVendorOption) {
+ testSendReceive(TEST_ITERATIONS, TestIpc::ENDPOINT_TYPE_V4,
+ TestIpc::ENDPOINT_TYPE_V6,
+ &createDHCPv4o6MsgWithISCVendorOption);
+}
+
+// This test verifies that the IPC will transmit message already containing
+// vendor option with enterprise id different than ISC, between the DHCPv6
+// and DHCPv4 server.
+TEST_F(Dhcp4o6IpcBaseTest, send6To4WithAnyVendorOption) {
+ testSendReceive(TEST_ITERATIONS, TestIpc::ENDPOINT_TYPE_V6,
+ TestIpc::ENDPOINT_TYPE_V4,
+ &createDHCPv4o6MsgWithAnyVendorOption);
+}
+
+// This test verifies that the IPC will transmit message already containing
+// vendor option with enterprise id different than ISC, between the DHCPv4
+// and DHCPv6 server.
+TEST_F(Dhcp4o6IpcBaseTest, send4To6WithAnyVendorOption) {
+ testSendReceive(TEST_ITERATIONS, TestIpc::ENDPOINT_TYPE_V4,
+ TestIpc::ENDPOINT_TYPE_V6,
+ &createDHCPv4o6MsgWithAnyVendorOption);
+}
+
+// This test checks that the values of the socket descriptor are correct
+// when the socket is opened and then closed.
+TEST_F(Dhcp4o6IpcBaseTest, openClose) {
+ TestIpc ipc(TEST_PORT, TestIpc::ENDPOINT_TYPE_V4);
+ EXPECT_EQ(-1, ipc.getSocketFd());
+
+ ASSERT_NO_THROW(ipc.open());
+ EXPECT_NE(-1, ipc.getSocketFd());
+
+ ASSERT_NO_THROW(ipc.close());
+ EXPECT_EQ(-1, ipc.getSocketFd());
+}
+
+// This test verifies that it is call open() while the socket is already
+// opened. If the port changes, the new socket should be opened.
+TEST_F(Dhcp4o6IpcBaseTest, openMultipleTimes) {
+ TestIpc ipc(TEST_PORT, TestIpc::ENDPOINT_TYPE_V6);
+ ASSERT_NO_THROW(ipc.open());
+ int sock_fd = ipc.getSocketFd();
+ ASSERT_NE(-1, sock_fd);
+ ASSERT_EQ(TEST_PORT, ipc.getPort());
+
+ ASSERT_NO_THROW(ipc.open());
+ ASSERT_EQ(sock_fd, ipc.getSocketFd());
+ ASSERT_EQ(TEST_PORT, ipc.getPort());
+
+ ipc.setDesiredPort(TEST_PORT + 10);
+ ASSERT_NO_THROW(ipc.open());
+ ASSERT_NE(-1, ipc.getSocketFd());
+
+ EXPECT_EQ(TEST_PORT + 10, ipc.getPort());
+}
+
+// This test verifies that the socket remains open if there is a failure
+// to open a new socket.
+TEST_F(Dhcp4o6IpcBaseTest, openError) {
+ TestIpc ipc(TEST_PORT, TestIpc::ENDPOINT_TYPE_V4);
+ ASSERT_NO_THROW(ipc.open());
+ int sock_fd = ipc.getSocketFd();
+ ASSERT_NE(-1, sock_fd);
+
+ TestIpc ipc_bound(TEST_PORT + 10, TestIpc::ENDPOINT_TYPE_V4);
+ ASSERT_NO_THROW(ipc_bound.open());
+ ASSERT_NE(-1, ipc_bound.getSocketFd());
+
+ ipc.setDesiredPort(TEST_PORT + 10);
+ ASSERT_THROW(ipc.open(), isc::dhcp::Dhcp4o6IpcError);
+
+ EXPECT_EQ(sock_fd, ipc.getSocketFd());
+ EXPECT_EQ(TEST_PORT, ipc.getPort());
+
+ ASSERT_NO_THROW(ipc_bound.close());
+ ASSERT_NO_THROW(ipc.open());
+ EXPECT_NE(-1, ipc.getSocketFd());
+ EXPECT_EQ(TEST_PORT + 10, ipc.getPort());
+}
+
+// This test verifies that receiving packet over the IPC fails when there
+// is no vendor option present.
+TEST_F(Dhcp4o6IpcBaseTest, receiveWithoutVendorOption) {
+ Pkt6Ptr pkt(new Pkt6(DHCPV6_DHCPV4_QUERY, 0));
+ testReceiveError(pkt);
+}
+
+// This test verifies that receiving packet over the IPC fails when the
+// enterprise ID carried in the vendor option is invalid.
+TEST_F(Dhcp4o6IpcBaseTest, receiveInvalidEnterpriseId) {
+ Pkt6Ptr pkt(new Pkt6(DHCPV6_DHCPV4_QUERY, 0));
+ OptionVendorPtr option_vendor(new OptionVendor(Option::V6, 1));
+ option_vendor->addOption(
+ OptionStringPtr(new OptionString(Option::V6, ISC_V6_4O6_INTERFACE,
+ "eth0")));
+ option_vendor->addOption(
+ Option6AddrLstPtr(new Option6AddrLst(ISC_V6_4O6_SRC_ADDRESS,
+ IOAddress("2001:db8:1::1")))
+ );
+
+ pkt->addOption(option_vendor);
+ testReceiveError(pkt);
+}
+
+// This test verifies that receiving packet over the IPC fails when the
+// interface option is not present.
+TEST_F(Dhcp4o6IpcBaseTest, receiveWithoutInterfaceOption) {
+ Pkt6Ptr pkt(new Pkt6(DHCPV6_DHCPV4_QUERY, 0));
+ OptionVendorPtr option_vendor(new OptionVendor(Option::V6,
+ ENTERPRISE_ID_ISC));
+ option_vendor->addOption(
+ Option6AddrLstPtr(new Option6AddrLst(ISC_V6_4O6_SRC_ADDRESS,
+ IOAddress("2001:db8:1::1")))
+ );
+ option_vendor->addOption(
+ OptionUint16Ptr(new OptionUint16(Option::V6,
+ ISC_V6_4O6_SRC_PORT,
+ TEST_PORT)));
+
+ pkt->addOption(option_vendor);
+ testReceiveError(pkt);
+}
+
+// This test verifies that receiving packet over the IPC fails when the
+// interface which name is carried in the option is not present in the
+// system.
+TEST_F(Dhcp4o6IpcBaseTest, receiveWithInvalidInterface) {
+ Pkt6Ptr pkt(new Pkt6(DHCPV6_DHCPV4_QUERY, 0));
+ OptionVendorPtr option_vendor(new OptionVendor(Option::V6,
+ ENTERPRISE_ID_ISC));
+ option_vendor->addOption(
+ OptionStringPtr(new OptionString(Option::V6, ISC_V6_4O6_INTERFACE,
+ "ethX")));
+ option_vendor->addOption(
+ Option6AddrLstPtr(new Option6AddrLst(ISC_V6_4O6_SRC_ADDRESS,
+ IOAddress("2001:db8:1::1")))
+ );
+ option_vendor->addOption(
+ OptionUint16Ptr(new OptionUint16(Option::V6,
+ ISC_V6_4O6_SRC_PORT,
+ TEST_PORT)));
+
+ pkt->addOption(option_vendor);
+ testReceiveError(pkt);
+}
+
+
+// This test verifies that receiving packet over the IPC fails when the
+// source address option is not present.
+TEST_F(Dhcp4o6IpcBaseTest, receiveWithoutSourceAddressOption) {
+ Pkt6Ptr pkt(new Pkt6(DHCPV6_DHCPV4_QUERY, 0));
+ OptionVendorPtr option_vendor(new OptionVendor(Option::V6,
+ ENTERPRISE_ID_ISC));
+ option_vendor->addOption(
+ OptionStringPtr(new OptionString(Option::V6, ISC_V6_4O6_INTERFACE,
+ "eth0")));
+ option_vendor->addOption(
+ OptionUint16Ptr(new OptionUint16(Option::V6,
+ ISC_V6_4O6_SRC_PORT,
+ TEST_PORT)));
+
+ pkt->addOption(option_vendor);
+ testReceiveError(pkt);
+}
+
+// This test verifies that receiving packet over the IPC fails when the
+// source port option is not present.
+TEST_F(Dhcp4o6IpcBaseTest, receiveWithoutSourcePortOption) {
+ Pkt6Ptr pkt(new Pkt6(DHCPV6_DHCPV4_QUERY, 0));
+ OptionVendorPtr option_vendor(new OptionVendor(Option::V6,
+ ENTERPRISE_ID_ISC));
+ option_vendor->addOption(
+ OptionStringPtr(new OptionString(Option::V6, ISC_V6_4O6_INTERFACE,
+ "eth0")));
+ option_vendor->addOption(
+ Option6AddrLstPtr(new Option6AddrLst(ISC_V6_4O6_SRC_ADDRESS,
+ IOAddress("2001:db8:1::1")))
+ );
+
+ pkt->addOption(option_vendor);
+ testReceiveError(pkt);
+}
+
+// This test verifies that send method throws exception when the packet
+// is NULL.
+TEST_F(Dhcp4o6IpcBaseTest, sendNullMessage) {
+ TestIpc ipc(TEST_PORT, TestIpc::ENDPOINT_TYPE_V4);
+ ASSERT_NO_THROW(ipc.open());
+
+ // NULL message.
+ EXPECT_THROW(ipc.send(Pkt6Ptr()), Dhcp4o6IpcError);
+}
+
+// This test verifies that send method throws exception when the IPC
+// socket is not opened.
+TEST_F(Dhcp4o6IpcBaseTest, sendOverClosedSocket) {
+ TestIpc ipc(TEST_PORT, TestIpc::ENDPOINT_TYPE_V4);
+
+ // Create a message.
+ Pkt6Ptr pkt(createDHCPv4o6Message(DHCPV6_DHCPV4_QUERY));
+
+ // Sending over the closed socket should fail.
+ EXPECT_THROW(ipc.send(pkt), Dhcp4o6IpcError);
+}
+
+// This test verifies that send method throws exception when the IPC
+// socket has been unexpectedly closed.
+TEST_F(Dhcp4o6IpcBaseTest, sendOverUnexpectedlyClosedSocket) {
+ TestIpc ipc(TEST_PORT, TestIpc::ENDPOINT_TYPE_V4);
+ ASSERT_NO_THROW(ipc.open());
+
+ // Close the socket behind the scenes. The IPC doesn't know that the
+ // socket has been closed and it still holds the descriptor.
+ ::close(ipc.getSocketFd());
+
+ // Create a message.
+ Pkt6Ptr pkt(createDHCPv4o6Message(DHCPV6_DHCPV4_QUERY));
+
+ // Sending over the closed socket should fail.
+ EXPECT_THROW(ipc.send(pkt), Dhcp4o6IpcError);
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc b/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc
new file mode 100644
index 0000000..7260386
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc
@@ -0,0 +1,3176 @@
+// Copyright (C) 2012-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <cc/command_interpreter.h>
+#include <cc/data.h>
+#include <cc/simple_parser.h>
+#include <dhcp/option.h>
+#include <dhcp/option_custom.h>
+#include <dhcp/option_int.h>
+#include <dhcp/option_string.h>
+#include <dhcp/option4_addrlst.h>
+#include <dhcp/option6_addrlst.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/subnet.h>
+#include <dhcpsrv/cfg_mac_source.h>
+#include <dhcpsrv/parsers/dhcp_parsers.h>
+#include <dhcpsrv/parsers/option_data_parser.h>
+#include <dhcpsrv/parsers/shared_network_parser.h>
+#include <dhcpsrv/parsers/shared_networks_list_parser.h>
+#include <dhcpsrv/tests/test_libraries.h>
+#include <dhcpsrv/testutils/config_result_check.h>
+#include <exceptions/exceptions.h>
+#include <hooks/hooks_parser.h>
+#include <hooks/hooks_manager.h>
+#include <testutils/test_to_element.h>
+
+#include <gtest/gtest.h>
+#include <boost/foreach.hpp>
+#include <boost/pointer_cast.hpp>
+#include <boost/scoped_ptr.hpp>
+
+#include <map>
+#include <string>
+
+using namespace std;
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::hooks;
+using namespace isc::test;
+
+namespace {
+
+/// @brief DHCP Parser test fixture class
+class DhcpParserTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ DhcpParserTest() {
+ resetIfaceCfg();
+ }
+
+ /// @brief Destructor.
+ virtual ~DhcpParserTest() {
+ resetIfaceCfg();
+ }
+
+ /// @brief Resets selection of the interfaces from previous tests.
+ void resetIfaceCfg() {
+ CfgMgr::instance().clear();
+ }
+};
+
+/// Verifies the code that parses mac sources and adds them to CfgMgr
+TEST_F(DhcpParserTest, MacSources) {
+
+ // That's an equivalent of the following snippet:
+ // "mac-sources: [ \"duid\", \"ipv6\" ]";
+ ElementPtr values = Element::createList();
+ values->add(Element::create("duid"));
+ values->add(Element::create("ipv6-link-local"));
+
+ // Let's grab server configuration from CfgMgr
+ SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
+ ASSERT_TRUE(cfg);
+ CfgMACSource& sources = cfg->getMACSources();
+
+ // This should parse the configuration and check that it doesn't throw.
+ MACSourcesListConfigParser parser;
+ EXPECT_NO_THROW(parser.parse(sources, values));
+
+ // Finally, check the sources that were configured
+ CfgMACSources configured_sources = cfg->getMACSources().get();
+ ASSERT_EQ(2, configured_sources.size());
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_DUID, configured_sources[0]);
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_IPV6_LINK_LOCAL, configured_sources[1]);
+}
+
+/// @brief Check MACSourcesListConfigParser rejecting empty list
+///
+/// Verifies that the code rejects an empty mac-sources list.
+TEST_F(DhcpParserTest, MacSourcesEmpty) {
+
+ // That's an equivalent of the following snippet:
+ // "mac-sources: [ \"duid\", \"ipv6\" ]";
+ ElementPtr values = Element::createList();
+
+ // Let's grab server configuration from CfgMgr
+ SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
+ ASSERT_TRUE(cfg);
+ CfgMACSource& sources = cfg->getMACSources();
+
+ // This should throw, because if specified, at least one MAC source
+ // has to be specified.
+ MACSourcesListConfigParser parser;
+ EXPECT_THROW(parser.parse(sources, values), DhcpConfigError);
+}
+
+/// @brief Check MACSourcesListConfigParser rejecting empty list
+///
+/// Verifies that the code rejects fake mac source.
+TEST_F(DhcpParserTest, MacSourcesBogus) {
+
+ // That's an equivalent of the following snippet:
+ // "mac-sources: [ \"duid\", \"ipv6\" ]";
+ ElementPtr values = Element::createList();
+ values->add(Element::create("from-ebay"));
+ values->add(Element::create("just-guess-it"));
+
+ // Let's grab server configuration from CfgMgr
+ SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
+ ASSERT_TRUE(cfg);
+ CfgMACSource& sources = cfg->getMACSources();
+
+ // This should throw, because these are not valid sources.
+ MACSourcesListConfigParser parser;
+ EXPECT_THROW(parser.parse(sources, values), DhcpConfigError);
+}
+
+/// Verifies the code that properly catches duplicate entries
+/// in mac-sources definition.
+TEST_F(DhcpParserTest, MacSourcesDuplicate) {
+
+ // That's an equivalent of the following snippet:
+ // "mac-sources: [ \"duid\", \"ipv6\" ]";
+ ElementPtr values = Element::createList();
+ values->add(Element::create("ipv6-link-local"));
+ values->add(Element::create("duid"));
+ values->add(Element::create("duid"));
+ values->add(Element::create("duid"));
+
+ // Let's grab server configuration from CfgMgr
+ SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
+ ASSERT_TRUE(cfg);
+ CfgMACSource& sources = cfg->getMACSources();
+
+ // This should parse the configuration and check that it throws.
+ MACSourcesListConfigParser parser;
+ EXPECT_THROW(parser.parse(sources, values), DhcpConfigError);
+}
+
+
+/// @brief Test Fixture class which provides basic structure for testing
+/// configuration parsing. This is essentially the same structure provided
+/// by dhcp servers.
+class ParseConfigTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ ParseConfigTest()
+ :family_(AF_INET6) {
+ reset_context();
+ }
+
+ ~ParseConfigTest() {
+ reset_context();
+ CfgMgr::instance().clear();
+ }
+
+ /// @brief Parses a configuration.
+ ///
+ /// Parse the given configuration, populating the context storage with
+ /// the parsed elements.
+ ///
+ /// @param config_set is the set of elements to parse.
+ /// @param v6 boolean flag indicating if this is a DHCPv6 configuration.
+ /// @return returns an ConstElementPtr containing the numeric result
+ /// code and outcome comment.
+ isc::data::ConstElementPtr
+ parseElementSet(isc::data::ConstElementPtr config_set, bool v6) {
+ // Answer will hold the result.
+ ConstElementPtr answer;
+ if (!config_set) {
+ answer = isc::config::createAnswer(1,
+ string("Can't parse NULL config"));
+ return (answer);
+ }
+
+ ConfigPair config_pair;
+ try {
+ // Iterate over the config elements.
+ const std::map<std::string, ConstElementPtr>& values_map =
+ config_set->mapValue();
+ BOOST_FOREACH(config_pair, values_map) {
+
+ // These are the simple parsers. No need to go through
+ // the ParserPtr hooplas with them.
+ if ((config_pair.first == "option-data") ||
+ (config_pair.first == "option-def") ||
+ (config_pair.first == "dhcp-ddns")) {
+ continue;
+ }
+
+ // We also don't care about the default values that may be been
+ // inserted
+ if ((config_pair.first == "preferred-lifetime") ||
+ (config_pair.first == "valid-lifetime") ||
+ (config_pair.first == "renew-timer") ||
+ (config_pair.first == "rebind-timer")) {
+ continue;
+ }
+
+ // Save global hostname-char-*.
+ if ((config_pair.first == "hostname-char-set") ||
+ (config_pair.first == "hostname-char-replacement")) {
+ CfgMgr::instance().getStagingCfg()->addConfiguredGlobal(config_pair.first,
+ config_pair.second);
+ continue;
+ }
+
+ if (config_pair.first == "hooks-libraries") {
+ HooksLibrariesParser hook_parser;
+ HooksConfig& libraries =
+ CfgMgr::instance().getStagingCfg()->getHooksConfig();
+ hook_parser.parse(libraries, config_pair.second);
+ libraries.verifyLibraries(config_pair.second->getPosition());
+ libraries.loadLibraries();
+ continue;
+ }
+ }
+
+ // The option definition parser is the next one to be run.
+ std::map<std::string, ConstElementPtr>::const_iterator
+ def_config = values_map.find("option-def");
+ if (def_config != values_map.end()) {
+
+ CfgOptionDefPtr cfg_def = CfgMgr::instance().getStagingCfg()->getCfgOptionDef();
+ OptionDefListParser def_list_parser(family_);
+ def_list_parser.parse(cfg_def, def_config->second);
+ }
+
+ // The option values parser is the next one to be run.
+ std::map<std::string, ConstElementPtr>::const_iterator
+ option_config = values_map.find("option-data");
+ if (option_config != values_map.end()) {
+ CfgOptionPtr cfg_option = CfgMgr::instance().getStagingCfg()->getCfgOption();
+
+ OptionDataListParser option_list_parser(family_);
+ option_list_parser.parse(cfg_option, option_config->second);
+ }
+
+ // The dhcp-ddns parser is the next one to be run.
+ std::map<std::string, ConstElementPtr>::const_iterator
+ d2_client_config = values_map.find("dhcp-ddns");
+ if (d2_client_config != values_map.end()) {
+ // Used to be done by parser commit
+ D2ClientConfigParser parser;
+ D2ClientConfigPtr cfg = parser.parse(d2_client_config->second);
+ cfg->validateContents();
+ CfgMgr::instance().setD2ClientConfig(cfg);
+ }
+
+ std::map<std::string, ConstElementPtr>::const_iterator
+ subnets4_config = values_map.find("subnet4");
+ if (subnets4_config != values_map.end()) {
+ auto srv_config = CfgMgr::instance().getStagingCfg();
+ Subnets4ListConfigParser parser;
+ parser.parse(srv_config, subnets4_config->second);
+ }
+
+ std::map<std::string, ConstElementPtr>::const_iterator
+ subnets6_config = values_map.find("subnet6");
+ if (subnets6_config != values_map.end()) {
+ auto srv_config = CfgMgr::instance().getStagingCfg();
+ Subnets6ListConfigParser parser;
+ parser.parse(srv_config, subnets6_config->second);
+ }
+
+ std::map<std::string, ConstElementPtr>::const_iterator
+ networks_config = values_map.find("shared-networks");
+ if (networks_config != values_map.end()) {
+ if (v6) {
+ auto cfg_shared_networks = CfgMgr::instance().getStagingCfg()->getCfgSharedNetworks6();
+ SharedNetworks6ListParser parser;
+ parser.parse(cfg_shared_networks, networks_config->second);
+
+ } else {
+ auto cfg_shared_networks = CfgMgr::instance().getStagingCfg()->getCfgSharedNetworks4();
+ SharedNetworks4ListParser parser;
+ parser.parse(cfg_shared_networks, networks_config->second);
+ }
+ }
+
+ // Everything was fine. Configuration is successful.
+ answer = isc::config::createAnswer(0, "Configuration committed.");
+ } catch (const isc::Exception& ex) {
+ answer = isc::config::createAnswer(1,
+ string("Configuration parsing failed: ") + ex.what());
+
+ } catch (...) {
+ answer = isc::config::createAnswer(1,
+ string("Configuration parsing failed"));
+ }
+
+ return (answer);
+ }
+
+ /// @brief DHCP-specific method that sets global, and option specific defaults
+ ///
+ /// This method sets the defaults in the global scope, in option definitions,
+ /// and in option data.
+ ///
+ /// @param global pointer to the Element tree that holds configuration
+ /// @param global_defaults array with global default values
+ /// @param option_defaults array with option-data default values
+ /// @param option_def_defaults array with default values for option definitions
+ /// @return number of default values inserted.
+ size_t setAllDefaults(isc::data::ElementPtr global,
+ const SimpleDefaults& global_defaults,
+ const SimpleDefaults& option_defaults,
+ const SimpleDefaults& option_def_defaults) {
+ size_t cnt = 0;
+ // Set global defaults first.
+ cnt = SimpleParser::setDefaults(global, global_defaults);
+
+ // Now set option definition defaults for each specified option definition
+ ConstElementPtr option_defs = global->get("option-def");
+ if (option_defs) {
+ BOOST_FOREACH(ElementPtr single_def, option_defs->listValue()) {
+ cnt += SimpleParser::setDefaults(single_def, option_def_defaults);
+ }
+ }
+
+ ConstElementPtr options = global->get("option-data");
+ if (options) {
+ BOOST_FOREACH(ElementPtr single_option, options->listValue()) {
+ cnt += SimpleParser::setDefaults(single_option, option_defaults);
+ }
+ }
+
+ return (cnt);
+ }
+
+ /// This table defines default values for option definitions in DHCPv6
+ static const SimpleDefaults OPTION6_DEF_DEFAULTS;
+
+ /// This table defines default values for option definitions in DHCPv4
+ static const SimpleDefaults OPTION4_DEF_DEFAULTS;
+
+ /// This table defines default values for options in DHCPv6
+ static const SimpleDefaults OPTION6_DEFAULTS;
+
+ /// This table defines default values for options in DHCPv4
+ static const SimpleDefaults OPTION4_DEFAULTS;
+
+ /// This table defines default values for both DHCPv4 and DHCPv6
+ static const SimpleDefaults GLOBAL6_DEFAULTS;
+
+ /// @brief sets all default values for DHCPv4 and DHCPv6
+ ///
+ /// This function largely duplicates what SimpleParser4 and SimpleParser6 classes
+ /// provide. However, since there are tons of unit-tests in dhcpsrv that need
+ /// this functionality and there are good reasons to keep those classes in
+ /// src/bin/dhcp{4,6}, the most straightforward way is to simply copy the
+ /// minimum code here. Hence this method.
+ ///
+ /// @todo - TKM, I think this is fairly hideous and we should figure out a
+ /// a way to not have to replicate in this fashion. It may be minimum code
+ /// now, but it won't be fairly soon.
+ ///
+ /// @param config configuration structure to be filled with default values
+ /// @param v6 true = DHCPv6, false = DHCPv4
+ void setAllDefaults(ElementPtr config, bool v6) {
+ if (v6) {
+ setAllDefaults(config, GLOBAL6_DEFAULTS, OPTION6_DEFAULTS,
+ OPTION6_DEF_DEFAULTS);
+ } else {
+ setAllDefaults(config, GLOBAL6_DEFAULTS, OPTION4_DEFAULTS,
+ OPTION4_DEF_DEFAULTS);
+ }
+
+ /// D2 client configuration code is in this library
+ ConstElementPtr d2_client = config->get("dhcp-ddns");
+ if (d2_client) {
+ D2ClientConfigParser::setAllDefaults(d2_client);
+ }
+ }
+
+ /// @brief Convenience method for parsing a configuration
+ ///
+ /// Given a configuration string, convert it into Elements
+ /// and parse them.
+ /// @param config is the configuration string to parse
+ /// @param v6 boolean value indicating if this is DHCPv6 configuration.
+ /// @param set_defaults boolean value indicating if the defaults should
+ /// be derived before parsing the configuration.
+ ///
+ /// @return returns 0 if the configuration parsed successfully,
+ /// non-zero otherwise failure.
+ int parseConfiguration(const std::string& config, bool v6 = false,
+ bool set_defaults = true) {
+ int rcode_ = 1;
+ // Turn config into elements.
+ // Test json just to make sure its valid.
+ ElementPtr json = Element::fromJSON(config);
+ EXPECT_TRUE(json);
+ if (json) {
+ if (set_defaults) {
+ setAllDefaults(json, v6);
+ }
+
+ ConstElementPtr status = parseElementSet(json, v6);
+ ConstElementPtr comment = parseAnswer(rcode_, status);
+ error_text_ = comment->stringValue();
+ // If error was reported, the error string should contain
+ // position of the data element which caused failure.
+ if (rcode_ != 0) {
+ EXPECT_TRUE(errorContainsPosition(status, "<string>"));
+ }
+ }
+
+ return (rcode_);
+ }
+
+ /// @brief Find an option for a given space and code within the parser
+ /// context.
+ /// @param space is the space name of the desired option.
+ /// @param code is the numeric "type" of the desired option.
+ /// @return returns an OptionPtr which points to the found
+ /// option or is empty.
+ /// ASSERT_ tests don't work inside functions that return values
+ OptionPtr getOptionPtr(std::string space, uint32_t code)
+ {
+ OptionPtr option_ptr;
+ OptionContainerPtr options = CfgMgr::instance().getStagingCfg()->
+ getCfgOption()->getAll(space);
+ // Should always be able to get options list even if it is empty.
+ EXPECT_TRUE(options);
+ if (options) {
+ // Attempt to find desired option.
+ const OptionContainerTypeIndex& idx = options->get<1>();
+ const OptionContainerTypeRange& range = idx.equal_range(code);
+ int cnt = std::distance(range.first, range.second);
+ EXPECT_EQ(1, cnt);
+ if (cnt == 1) {
+ OptionDescriptor desc = *(idx.begin());
+ option_ptr = desc.option_;
+ EXPECT_TRUE(option_ptr);
+ }
+ }
+
+ return (option_ptr);
+ }
+
+ /// @brief Wipes the contents of the context to allowing another parsing
+ /// during a given test if needed.
+ /// @param family protocol family to use during the test, defaults
+ /// to AF_INET6
+ void reset_context(uint16_t family = AF_INET6){
+ // Note set context universe to V6 as it has to be something.
+ CfgMgr::instance().clear();
+ family_ = family;
+
+ // Ensure no hooks libraries are loaded.
+ EXPECT_TRUE(HooksManager::unloadLibraries());
+
+ // Set it to minimal, disabled config
+ D2ClientConfigPtr tmp(new D2ClientConfig());
+ CfgMgr::instance().setD2ClientConfig(tmp);
+ }
+
+ /// Allows the tests to interrogate the state of the libraries (if required).
+ const isc::hooks::HookLibsCollection& getLibraries() {
+ return (CfgMgr::instance().getStagingCfg()->getHooksConfig().get());
+ }
+
+ /// @brief specifies IP protocol family (AF_INET or AF_INET6)
+ uint16_t family_;
+
+ /// @brief Error string if the parsing failed
+ std::string error_text_;
+};
+
+/// This table defines default values for option definitions in DHCPv6
+const SimpleDefaults ParseConfigTest::OPTION6_DEF_DEFAULTS = {
+ { "record-types", Element::string, ""},
+ { "space", Element::string, "dhcp6"},
+ { "array", Element::boolean, "false"},
+ { "encapsulate", Element::string, "" }
+};
+
+/// This table defines default values for option definitions in DHCPv4
+const SimpleDefaults ParseConfigTest::OPTION4_DEF_DEFAULTS = {
+ { "record-types", Element::string, ""},
+ { "space", Element::string, "dhcp4"},
+ { "array", Element::boolean, "false"},
+ { "encapsulate", Element::string, "" }
+};
+
+/// This table defines default values for options in DHCPv6
+const SimpleDefaults ParseConfigTest::OPTION6_DEFAULTS = {
+ { "space", Element::string, "dhcp6"},
+ { "csv-format", Element::boolean, "true"},
+ { "always-send", Element::boolean,"false"}
+};
+
+/// This table defines default values for options in DHCPv4
+const SimpleDefaults ParseConfigTest::OPTION4_DEFAULTS = {
+ { "space", Element::string, "dhcp4"},
+ { "csv-format", Element::boolean, "true"},
+ { "always-send", Element::boolean, "false"}
+};
+
+/// This table defines default values for both DHCPv4 and DHCPv6
+const SimpleDefaults ParseConfigTest::GLOBAL6_DEFAULTS = {
+ { "renew-timer", Element::integer, "900" },
+ { "rebind-timer", Element::integer, "1800" },
+ { "preferred-lifetime", Element::integer, "3600" },
+ { "valid-lifetime", Element::integer, "7200" }
+};
+
+/// @brief Option configuration class
+///
+/// This class handles option-def and option-data which can be recovered
+/// using the toElement() method
+class CfgOptionsTest : public CfgToElement {
+public:
+ /// @brief Constructor
+ ///
+ /// @param cfg the server configuration where to get option-{def,data}
+ CfgOptionsTest(SrvConfigPtr cfg) :
+ cfg_option_def_(cfg->getCfgOptionDef()),
+ cfg_option_(cfg->getCfgOption()) { }
+
+ /// @brief Unparse a configuration object
+ ///
+ /// @return a pointer to unparsed configuration (a map with
+ /// not empty option-def and option-data lists)
+ ElementPtr toElement() const {
+ ElementPtr result = Element::createMap();
+ // Set option-def
+ ConstElementPtr option_def = cfg_option_def_->toElement();
+ if (!option_def->empty()) {
+ result->set("option-def", option_def);
+ }
+ // Set option-data
+ ConstElementPtr option_data = cfg_option_->toElement();
+ if (!option_data->empty()) {
+ result->set("option-data", option_data);
+ }
+ return (result);
+ }
+
+ /// @brief Run a toElement test (Element version)
+ ///
+ /// Use the runToElementTest template but add defaults to the config
+ ///
+ /// @param family the address family
+ /// @param config the expected result without defaults
+ void runCfgOptionsTest(uint16_t family, ConstElementPtr expected) {
+ ConstElementPtr option_def = expected->get("option-def");
+ if (option_def) {
+ SimpleParser::setListDefaults(option_def,
+ family == AF_INET ?
+ ParseConfigTest::OPTION4_DEF_DEFAULTS :
+ ParseConfigTest::OPTION6_DEF_DEFAULTS);
+ }
+ ConstElementPtr option_data = expected->get("option-data");
+ if (option_data) {
+ SimpleParser::setListDefaults(option_data,
+ family == AF_INET ?
+ ParseConfigTest::OPTION4_DEFAULTS :
+ ParseConfigTest::OPTION6_DEFAULTS);
+ }
+ runToElementTest<CfgOptionsTest>(expected, *this);
+ }
+
+ /// @brief Run a toElement test
+ ///
+ /// Use the runToElementTest template but add defaults to the config
+ ///
+ /// @param family the address family
+ /// @param expected the expected result without defaults
+ void runCfgOptionsTest(uint16_t family, std::string config) {
+ ConstElementPtr json;
+ ASSERT_NO_THROW(json = Element::fromJSON(config)) << config;
+ runCfgOptionsTest(family, json);
+ }
+
+private:
+ /// @brief Pointer to option definitions configuration.
+ CfgOptionDefPtr cfg_option_def_;
+
+ /// @brief Reference to options (data) configuration.
+ CfgOptionPtr cfg_option_;
+};
+
+/// @brief Check basic parsing of option definitions.
+///
+/// Note that this tests basic operation of the OptionDefinitionListParser and
+/// OptionDefinitionParser. It uses a simple configuration consisting of
+/// one definition and verifies that it is parsed and committed to storage
+/// correctly.
+TEST_F(ParseConfigTest, basicOptionDefTest) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"ipv4-address\","
+ " \"array\": false,"
+ " \"record-types\": \"\","
+ " \"space\": \"isc\","
+ " \"encapsulate\": \"\""
+ " } ]"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config);
+ ASSERT_EQ(0, rcode);
+
+
+ // Verify that the option definition can be retrieved.
+ OptionDefinitionPtr def =
+ CfgMgr::instance().getStagingCfg()->getCfgOptionDef()->get("isc", 100);
+ ASSERT_TRUE(def);
+
+ // Verify that the option definition is correct.
+ EXPECT_EQ("foo", def->getName());
+ EXPECT_EQ(100, def->getCode());
+ EXPECT_FALSE(def->getArrayType());
+ EXPECT_EQ(OPT_IPV4_ADDRESS_TYPE, def->getType());
+ EXPECT_TRUE(def->getEncapsulatedSpace().empty());
+
+ // Check if libdhcp++ runtime options have been updated.
+ OptionDefinitionPtr def_libdhcp = LibDHCP::getRuntimeOptionDef("isc", 100);
+ ASSERT_TRUE(def_libdhcp);
+
+ // The LibDHCP should return a separate instance of the option definition
+ // but the values should be equal.
+ EXPECT_TRUE(def_libdhcp != def);
+ EXPECT_TRUE(*def_libdhcp == *def);
+
+ // Check if it can be unparsed.
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, config);
+}
+
+/// @brief Check minimal parsing of option definitions.
+///
+/// Same than basic but without optional parameters set to their default.
+TEST_F(ParseConfigTest, minimalOptionDefTest) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"ipv4-address\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config);
+ ASSERT_EQ(0, rcode);
+
+
+ // Verify that the option definition can be retrieved.
+ OptionDefinitionPtr def =
+ CfgMgr::instance().getStagingCfg()->getCfgOptionDef()->get("isc", 100);
+ ASSERT_TRUE(def);
+
+ // Verify that the option definition is correct.
+ EXPECT_EQ("foo", def->getName());
+ EXPECT_EQ(100, def->getCode());
+ EXPECT_FALSE(def->getArrayType());
+ EXPECT_EQ(OPT_IPV4_ADDRESS_TYPE, def->getType());
+ EXPECT_TRUE(def->getEncapsulatedSpace().empty());
+
+ // Check if it can be unparsed.
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, config);
+}
+
+/// @brief Check parsing of option definitions using default dhcp6 space.
+///
+/// Same than minimal but using the fact the default universe is V6
+/// so the default space is dhcp6
+TEST_F(ParseConfigTest, defaultSpaceOptionDefTest) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 10000,"
+ " \"type\": \"ipv6-address\""
+ " } ]"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config, true);
+ ASSERT_EQ(0, rcode);
+
+
+ // Verify that the option definition can be retrieved.
+ OptionDefinitionPtr def =
+ CfgMgr::instance().getStagingCfg()->getCfgOptionDef()->get(DHCP6_OPTION_SPACE, 10000);
+ ASSERT_TRUE(def);
+
+ // Verify that the option definition is correct.
+ EXPECT_EQ("foo", def->getName());
+ EXPECT_EQ(10000, def->getCode());
+ EXPECT_FALSE(def->getArrayType());
+ EXPECT_EQ(OPT_IPV6_ADDRESS_TYPE, def->getType());
+ EXPECT_TRUE(def->getEncapsulatedSpace().empty());
+
+ // Check if it can be unparsed.
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, config);
+}
+
+/// @brief Check parsing of option definitions using invalid code fails.
+TEST_F(ParseConfigTest, badCodeOptionDefTest) {
+
+ {
+ SCOPED_TRACE("negative code");
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"negative\","
+ " \"code\": -1,"
+ " \"type\": \"ipv6-address\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, true);
+ ASSERT_NE(0, rcode);
+ }
+
+ {
+ SCOPED_TRACE("out of range code (v6)");
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"hundred-thousands\","
+ " \"code\": 100000,"
+ " \"type\": \"ipv6-address\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, true);
+ ASSERT_NE(0, rcode);
+ }
+
+ {
+ SCOPED_TRACE("out of range code (v4)");
+ family_ = AF_INET; // Switch to DHCPv4.
+
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"thousand\","
+ " \"code\": 1000,"
+ " \"type\": \"ip-address\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, false);
+ ASSERT_NE(0, rcode);
+ }
+
+ {
+ SCOPED_TRACE("conflict with PAD");
+ family_ = AF_INET; // Switch to DHCPv4.
+
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"zero\","
+ " \"code\": 0,"
+ " \"type\": \"ip-address\","
+ " \"space\": \"dhcp4\""
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, false);
+ ASSERT_NE(0, rcode);
+ }
+
+ {
+ SCOPED_TRACE("conflict with END");
+ family_ = AF_INET; // Switch to DHCPv4.
+
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"max\","
+ " \"code\": 255,"
+ " \"type\": \"ip-address\","
+ " \"space\": \"dhcp4\""
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, false);
+ ASSERT_NE(0, rcode);
+ }
+
+ {
+ SCOPED_TRACE("conflict with reserved");
+
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"zero\","
+ " \"code\": 0,"
+ " \"type\": \"ipv6-address\","
+ " \"space\": \"dhcp6\""
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, false);
+ ASSERT_NE(0, rcode);
+ }
+}
+
+/// @brief Check parsing of option definitions using invalid space fails.
+TEST_F(ParseConfigTest, badSpaceOptionDefTest) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"ipv6-address\","
+ " \"space\": \"-1\""
+ " } ]"
+ "}";
+
+ // Verify that the configuration string does not parse.
+ int rcode = parseConfiguration(config, true);
+ ASSERT_NE(0, rcode);
+}
+
+/// @brief Check basic parsing of options.
+///
+/// Note that this tests basic operation of the OptionDataListParser and
+/// OptionDataParser. It uses a simple configuration consisting of one
+/// one definition and matching option data. It verifies that the option
+/// is parsed and committed to storage correctly.
+TEST_F(ParseConfigTest, basicOptionDataTest) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"ipv4-address\","
+ " \"space\": \"isc\""
+ " } ], "
+ " \"option-data\": [ {"
+ " \"name\": \"foo\","
+ " \"space\": \"isc\","
+ " \"code\": 100,"
+ " \"data\": \"192.0.2.0\","
+ " \"csv-format\": true,"
+ " \"always-send\": false"
+ " } ]"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config);
+ ASSERT_EQ(0, rcode);
+
+ // Verify that the option can be retrieved.
+ OptionPtr opt_ptr = getOptionPtr("isc", 100);
+ ASSERT_TRUE(opt_ptr);
+
+ // Verify that the option data is correct.
+ std::string val = "type=00100, len=00004: 192.0.2.0 (ipv4-address)";
+
+ EXPECT_EQ(val, opt_ptr->toText());
+
+ // Check if it can be unparsed.
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, config);
+}
+
+/// @brief Check parsing of options with code 0.
+TEST_F(ParseConfigTest, optionDataTest0) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 0,"
+ " \"type\": \"ipv4-address\","
+ " \"space\": \"isc\""
+ " } ], "
+ " \"option-data\": [ {"
+ " \"name\": \"foo\","
+ " \"space\": \"isc\","
+ " \"code\": 0,"
+ " \"data\": \"192.0.2.0\","
+ " \"csv-format\": true,"
+ " \"always-send\": false"
+ " } ]"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config);
+ ASSERT_EQ(0, rcode);
+
+ // Verify that the option can be retrieved.
+ OptionPtr opt_ptr = getOptionPtr("isc", 0);
+ ASSERT_TRUE(opt_ptr);
+
+ // Verify that the option data is correct.
+ std::string val = "type=00000, len=00004: 192.0.2.0 (ipv4-address)";
+
+ EXPECT_EQ(val, opt_ptr->toText());
+
+ // Check if it can be unparsed.
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, config);
+}
+
+/// @brief Check parsing of options with code 255.
+TEST_F(ParseConfigTest, optionDataTest255) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 255,"
+ " \"type\": \"ipv4-address\","
+ " \"space\": \"isc\""
+ " } ], "
+ " \"option-data\": [ {"
+ " \"name\": \"foo\","
+ " \"space\": \"isc\","
+ " \"code\": 255,"
+ " \"data\": \"192.0.2.0\","
+ " \"csv-format\": true,"
+ " \"always-send\": false"
+ " } ]"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config);
+ ASSERT_EQ(0, rcode);
+
+ // Verify that the option can be retrieved.
+ OptionPtr opt_ptr = getOptionPtr("isc", 255);
+ ASSERT_TRUE(opt_ptr);
+
+ // Verify that the option data is correct.
+ std::string val = "type=00255, len=00004: 192.0.2.0 (ipv4-address)";
+
+ EXPECT_EQ(val, opt_ptr->toText());
+
+ // Check if it can be unparsed.
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, config);
+}
+
+/// @brief Check minimal parsing of options.
+///
+/// Same than basic but without optional parameters set to their default.
+TEST_F(ParseConfigTest, minimalOptionDataTest) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"ipv4-address\","
+ " \"space\": \"isc\""
+ " } ], "
+ " \"option-data\": [ {"
+ " \"name\": \"foo\","
+ " \"space\": \"isc\","
+ " \"data\": \"192.0.2.0\""
+ " } ]"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config);
+ ASSERT_EQ(0, rcode);
+
+ // Verify that the option can be retrieved.
+ OptionPtr opt_ptr = getOptionPtr("isc", 100);
+ ASSERT_TRUE(opt_ptr);
+
+ // Verify that the option data is correct.
+ std::string val = "type=00100, len=00004: 192.0.2.0 (ipv4-address)";
+
+ EXPECT_EQ(val, opt_ptr->toText());
+
+ ElementPtr expected = Element::fromJSON(config);
+ ElementPtr opt_data = expected->get("option-data")->getNonConst(0);
+ opt_data->set("code", Element::create(100));
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, expected);
+}
+
+/// @brief Check parsing of unknown options fails.
+TEST_F(ParseConfigTest, unknownOptionDataTest) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"name\": \"foo\","
+ " \"data\": \"01\","
+ " \"space\": \"bar\""
+ " } ]"
+ "}";
+
+ // Verify that the configuration string does not parse.
+ int rcode = parseConfiguration(config, true);
+ ASSERT_NE(0, rcode);
+}
+
+/// @brief Check parsing of option data using invalid code fails.
+TEST_F(ParseConfigTest, badCodeOptionDataTest) {
+
+ {
+ SCOPED_TRACE("negative code");
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"code\": -1,"
+ " \"data\": \"01\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, true);
+ ASSERT_NE(0, rcode);
+ }
+
+ {
+ SCOPED_TRACE("out of range code (v6)");
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"code\": 100000,"
+ " \"data\": \"01\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, true);
+ ASSERT_NE(0, rcode);
+ }
+
+ {
+ SCOPED_TRACE("out of range code (v4)");
+ family_ = AF_INET; // Switch to DHCPv4.
+
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"code\": 1000,"
+ " \"data\": \"01\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, false);
+ ASSERT_NE(0, rcode);
+ }
+
+ {
+ SCOPED_TRACE("conflict with PAD");
+ family_ = AF_INET; // Switch to DHCPv4.
+
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"code\": 0,"
+ " \"data\": \"01\","
+ " \"csv-format\": false,"
+ " \"space\": \"dhcp4\""
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, false);
+ ASSERT_NE(0, rcode);
+ }
+
+ {
+ SCOPED_TRACE("conflict with END");
+ family_ = AF_INET; // Switch to DHCPv4.
+
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"code\": 255,"
+ " \"data\": \"01\","
+ " \"csv-format\": false,"
+ " \"space\": \"dhcp4\""
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, false);
+ ASSERT_NE(0, rcode);
+ }
+
+ {
+ SCOPED_TRACE("conflict with reserved");
+ family_ = AF_INET6; // Switch to DHCPv6.
+
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"code\": 0,"
+ " \"data\": \"01\","
+ " \"csv-format\": false,"
+ " \"space\": \"dhcp6\""
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, false);
+ ASSERT_NE(0, rcode);
+ }
+}
+
+/// @brief Check parsing of options with invalid space fails.
+TEST_F(ParseConfigTest, badSpaceOptionDataTest) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"code\": 100,"
+ " \"data\": \"01\","
+ " \"space\": \"-1\""
+ " } ]"
+ "}";
+
+ // Verify that the configuration string does not parse.
+ int rcode = parseConfiguration(config, true);
+ ASSERT_NE(0, rcode);
+}
+
+/// @brief Check parsing of options with escape characters.
+///
+/// Note that this tests basic operation of the OptionDataListParser and
+/// OptionDataParser. It uses a simple configuration consisting of one
+/// one definition and matching option data. It verifies that the option
+/// is parsed and committed to storage correctly and that its content
+/// has the actual character (e.g. an actual backslash, not double backslash).
+TEST_F(ParseConfigTest, escapedOptionDataTest) {
+
+ family_ = AF_INET;
+
+ // We need to use double escapes here. The first backslash will
+ // be consumed by C++ preprocessor, so the actual string will
+ // have two backslash characters: \\SMSBoot\\x64\\wdsnbp.com.
+ //
+ std::string config =
+ "{\"option-data\": [ {"
+ " \"name\": \"boot-file-name\","
+ " \"data\": \"\\\\SMSBoot\\\\x64\\\\wdsnbp.com\""
+ " } ]"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config);
+ ASSERT_EQ(0, rcode);
+
+ // Verify that the option can be retrieved.
+ OptionPtr opt = getOptionPtr(DHCP4_OPTION_SPACE, DHO_BOOT_FILE_NAME);
+ ASSERT_TRUE(opt);
+
+ util::OutputBuffer buf(100);
+
+ uint8_t exp[] = { DHO_BOOT_FILE_NAME, 23, '\\', 'S', 'M', 'S', 'B', 'o', 'o',
+ 't', '\\', 'x', '6', '4', '\\', 'w', 'd', 's', 'n', 'b',
+ 'p', '.', 'c', 'o', 'm' };
+ ASSERT_EQ(25, sizeof(exp));
+
+ opt->pack(buf);
+ EXPECT_EQ(Option::OPTION4_HDR_LEN + 23, buf.getLength());
+
+ EXPECT_TRUE(0 == memcmp(buf.getData(), exp, 25));
+
+ ElementPtr expected = Element::fromJSON(config);
+ ElementPtr opt_data = expected->get("option-data")->getNonConst(0);
+ opt_data->set("code", Element::create(DHO_BOOT_FILE_NAME));
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, expected);
+}
+
+// This test checks behavior of the configuration parser for option data
+// for different values of csv-format parameter and when there is an option
+// definition present.
+TEST_F(ParseConfigTest, optionDataCSVFormatWithOptionDef) {
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"name\": \"swap-server\","
+ " \"space\": \"dhcp4\","
+ " \"code\": 16,"
+ " \"data\": \"192.0.2.0\""
+ " } ]"
+ "}";
+
+ // The default universe is V6. We need to change it to use dhcp4 option
+ // space.
+ family_ = AF_INET;
+ int rcode = 0;
+ ASSERT_NO_THROW(rcode = parseConfiguration(config));
+ ASSERT_EQ(0, rcode);
+
+ // Verify that the option data is correct.
+ OptionCustomPtr addr_opt = boost::dynamic_pointer_cast<
+ OptionCustom>(getOptionPtr(DHCP4_OPTION_SPACE, 16));
+ ASSERT_TRUE(addr_opt);
+ EXPECT_EQ("192.0.2.0", addr_opt->readAddress().toText());
+
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, config);
+
+ // Explicitly enable csv-format.
+ CfgMgr::instance().clear();
+ config =
+ "{ \"option-data\": [ {"
+ " \"name\": \"swap-server\","
+ " \"space\": \"dhcp4\","
+ " \"code\": 16,"
+ " \"csv-format\": true,"
+ " \"data\": \"192.0.2.0\""
+ " } ]"
+ "}";
+ ASSERT_NO_THROW(rcode = parseConfiguration(config));
+ ASSERT_EQ(0, rcode);
+
+ // Verify that the option data is correct.
+ addr_opt = boost::dynamic_pointer_cast<
+ OptionCustom>(getOptionPtr(DHCP4_OPTION_SPACE, 16));
+ ASSERT_TRUE(addr_opt);
+ EXPECT_EQ("192.0.2.0", addr_opt->readAddress().toText());
+
+ // To make runToElementTest to work the csv-format must be removed...
+
+ // Explicitly disable csv-format and use hex instead.
+ CfgMgr::instance().clear();
+ config =
+ "{ \"option-data\": [ {"
+ " \"name\": \"swap-server\","
+ " \"space\": \"dhcp4\","
+ " \"code\": 16,"
+ " \"csv-format\": false,"
+ " \"data\": \"C0000200\""
+ " } ]"
+ "}";
+ ASSERT_NO_THROW(rcode = parseConfiguration(config));
+ ASSERT_EQ(0, rcode);
+
+ // Verify that the option data is correct.
+ addr_opt = boost::dynamic_pointer_cast<
+ OptionCustom>(getOptionPtr(DHCP4_OPTION_SPACE, 16));
+ ASSERT_TRUE(addr_opt);
+ EXPECT_EQ("192.0.2.0", addr_opt->readAddress().toText());
+
+ CfgOptionsTest cfg2(CfgMgr::instance().getStagingCfg());
+ cfg2.runCfgOptionsTest(family_, config);
+}
+
+// This test verifies that definitions of standard encapsulated
+// options can be used.
+TEST_F(ParseConfigTest, encapsulatedOptionData) {
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"space\": \"s46-cont-mape-options\","
+ " \"name\": \"s46-rule\","
+ " \"data\": \"1, 0, 24, 192.0.2.0, 2001:db8:1::/64\""
+ " } ]"
+ "}";
+
+ // Make sure that we're using correct universe.
+ family_ = AF_INET6;
+ int rcode = 0;
+ ASSERT_NO_THROW(rcode = parseConfiguration(config));
+ ASSERT_EQ(0, rcode);
+
+ // Verify that the option data is correct.
+ OptionCustomPtr s46_rule = boost::dynamic_pointer_cast<OptionCustom>
+ (getOptionPtr(MAPE_V6_OPTION_SPACE, D6O_S46_RULE));
+ ASSERT_TRUE(s46_rule);
+
+ uint8_t flags;
+ uint8_t ea_len;
+ uint8_t prefix4_len;
+ IOAddress ipv4_prefix(IOAddress::IPV4_ZERO_ADDRESS());
+ PrefixTuple ipv6_prefix(PrefixLen(0), IOAddress::IPV6_ZERO_ADDRESS());;
+
+ ASSERT_NO_THROW({
+ flags = s46_rule->readInteger<uint8_t>(0);
+ ea_len = s46_rule->readInteger<uint8_t>(1);
+ prefix4_len = s46_rule->readInteger<uint8_t>(2);
+ ipv4_prefix = s46_rule->readAddress(3);
+ ipv6_prefix = s46_rule->readPrefix(4);
+ });
+
+ EXPECT_EQ(1, flags);
+ EXPECT_EQ(0, ea_len);
+ EXPECT_EQ(24, prefix4_len);
+ EXPECT_EQ("192.0.2.0", ipv4_prefix.toText());
+ EXPECT_EQ(64, ipv6_prefix.first.asUnsigned());
+ EXPECT_EQ("2001:db8:1::", ipv6_prefix.second.toText());
+
+ ElementPtr expected = Element::fromJSON(config);
+ ElementPtr opt_data = expected->get("option-data")->getNonConst(0);
+ opt_data->set("code", Element::create(D6O_S46_RULE));
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, expected);
+}
+
+// This test checks behavior of the configuration parser for option data
+// for different values of csv-format parameter and when there is no
+// option definition.
+TEST_F(ParseConfigTest, optionDataCSVFormatNoOptionDef) {
+ // This option doesn't have any definition. It is ok to use such
+ // an option but the data should be specified in hex, not as CSV.
+ // Note that the parser will by default use the CSV format for the
+ // data but only in case there is a suitable option definition.
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"name\": \"foo-name\","
+ " \"space\": \"dhcp6\","
+ " \"code\": 25000,"
+ " \"data\": \"1, 2, 5\""
+ " } ]"
+ "}";
+ int rcode = 0;
+ ASSERT_NO_THROW(rcode = parseConfiguration(config, true));
+ EXPECT_NE(0, rcode);
+
+ CfgMgr::instance().clear();
+ // The data specified here will work both for CSV format and hex format.
+ // What we want to test here is that when the csv-format is enforced, the
+ // parser will fail because of lack of an option definition.
+ config =
+ "{ \"option-data\": [ {"
+ " \"name\": \"foo-name\","
+ " \"space\": \"dhcp6\","
+ " \"code\": 25000,"
+ " \"csv-format\": true,"
+ " \"data\": \"0\""
+ " } ]"
+ "}";
+ ASSERT_NO_THROW(rcode = parseConfiguration(config, true));
+ EXPECT_NE(0, rcode);
+
+ CfgMgr::instance().clear();
+ // The same test case as above, but for the data specified in hex should
+ // be successful.
+ config =
+ "{ \"option-data\": [ {"
+ " \"name\": \"foo-name\","
+ " \"space\": \"dhcp6\","
+ " \"code\": 25000,"
+ " \"csv-format\": false,"
+ " \"data\": \"0\""
+ " } ]"
+ "}";
+ ASSERT_NO_THROW(rcode = parseConfiguration(config, true));
+ ASSERT_EQ(0, rcode);
+ OptionPtr opt = getOptionPtr(DHCP6_OPTION_SPACE, 25000);
+ ASSERT_TRUE(opt);
+ ASSERT_EQ(1, opt->getData().size());
+ EXPECT_EQ(0, opt->getData()[0]);
+
+ ElementPtr expected = Element::fromJSON(config);
+ ElementPtr opt_data = expected->get("option-data")->getNonConst(0);
+ opt_data->remove("name");
+ opt_data->set("data", Element::create(std::string("00")));
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, expected);
+
+ CfgMgr::instance().clear();
+ // When csv-format is not specified, the parser will check if the definition
+ // exists or not. Since there is no definition, the parser will accept the
+ // data in hex.
+ config =
+ "{ \"option-data\": [ {"
+ " \"name\": \"foo-name\","
+ " \"space\": \"dhcp6\","
+ " \"code\": 25000,"
+ " \"csv-format\": false,"
+ " \"data\": \"123456\""
+ " } ]"
+ "}";
+ ASSERT_NO_THROW(rcode = parseConfiguration(config, true));
+ EXPECT_EQ(0, rcode);
+ opt = getOptionPtr(DHCP6_OPTION_SPACE, 25000);
+ ASSERT_TRUE(opt);
+ ASSERT_EQ(3, opt->getData().size());
+ EXPECT_EQ(0x12, opt->getData()[0]);
+ EXPECT_EQ(0x34, opt->getData()[1]);
+ EXPECT_EQ(0x56, opt->getData()[2]);
+
+ expected = Element::fromJSON(config);
+ opt_data = expected->get("option-data")->getNonConst(0);
+ opt_data->remove("name");
+ CfgOptionsTest cfg2(CfgMgr::instance().getStagingCfg());
+ cfg2.runCfgOptionsTest(family_, expected);
+}
+
+// This test verifies that the option name is not mandatory, if the option
+// code has been specified.
+TEST_F(ParseConfigTest, optionDataNoName) {
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"space\": \"dhcp6\","
+ " \"code\": 23,"
+ " \"data\": \"2001:db8:1::1\""
+ " } ]"
+ "}";
+ int rcode = 0;
+ ASSERT_NO_THROW(rcode = parseConfiguration(config, true));
+ EXPECT_EQ(0, rcode);
+ Option6AddrLstPtr opt = boost::dynamic_pointer_cast<
+ Option6AddrLst>(getOptionPtr(DHCP6_OPTION_SPACE, 23));
+ ASSERT_TRUE(opt);
+ ASSERT_EQ(1, opt->getAddresses().size());
+ EXPECT_EQ( "2001:db8:1::1", opt->getAddresses()[0].toText());
+
+ ElementPtr expected = Element::fromJSON(config);
+ ElementPtr opt_data = expected->get("option-data")->getNonConst(0);
+ opt_data->set("name", Element::create(std::string("dns-servers")));
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, expected);
+}
+
+// This test verifies that the option code is not mandatory, if the option
+// name has been specified.
+TEST_F(ParseConfigTest, optionDataNoCode) {
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"space\": \"dhcp6\","
+ " \"name\": \"dns-servers\","
+ " \"data\": \"2001:db8:1::1\""
+ " } ]"
+ "}";
+ int rcode = 0;
+ ASSERT_NO_THROW(rcode = parseConfiguration(config, true));
+ EXPECT_EQ(0, rcode);
+ Option6AddrLstPtr opt = boost::dynamic_pointer_cast<
+ Option6AddrLst>(getOptionPtr(DHCP6_OPTION_SPACE, 23));
+ ASSERT_TRUE(opt);
+ ASSERT_EQ(1, opt->getAddresses().size());
+ EXPECT_EQ( "2001:db8:1::1", opt->getAddresses()[0].toText());
+
+ ElementPtr expected = Element::fromJSON(config);
+ ElementPtr opt_data = expected->get("option-data")->getNonConst(0);
+ opt_data->set("code", Element::create(D6O_NAME_SERVERS));
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, expected);
+}
+
+// This test verifies that the option data configuration with a minimal
+// set of parameters works as expected.
+TEST_F(ParseConfigTest, optionDataMinimal) {
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"name\": \"dns-servers\","
+ " \"data\": \"2001:db8:1::10\""
+ " } ]"
+ "}";
+ int rcode = 0;
+ ASSERT_NO_THROW(rcode = parseConfiguration(config, true));
+ EXPECT_EQ(0, rcode);
+ Option6AddrLstPtr opt = boost::dynamic_pointer_cast<
+ Option6AddrLst>(getOptionPtr(DHCP6_OPTION_SPACE, 23));
+ ASSERT_TRUE(opt);
+ ASSERT_EQ(1, opt->getAddresses().size());
+ EXPECT_EQ( "2001:db8:1::10", opt->getAddresses()[0].toText());
+
+ ElementPtr expected = Element::fromJSON(config);
+ ElementPtr opt_data = expected->get("option-data")->getNonConst(0);
+ opt_data->set("code", Element::create(D6O_NAME_SERVERS));
+ opt_data->set("space", Element::create(std::string(DHCP6_OPTION_SPACE)));
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, expected);
+
+ CfgMgr::instance().clear();
+ // This time using an option code.
+ config =
+ "{ \"option-data\": [ {"
+ " \"code\": 23,"
+ " \"data\": \"2001:db8:1::20\""
+ " } ]"
+ "}";
+ rcode = 0;
+ ASSERT_NO_THROW(rcode = parseConfiguration(config, true));
+ EXPECT_EQ(0, rcode);
+ opt = boost::dynamic_pointer_cast<Option6AddrLst>(getOptionPtr(DHCP6_OPTION_SPACE,
+ 23));
+ ASSERT_TRUE(opt);
+ ASSERT_EQ(1, opt->getAddresses().size());
+ EXPECT_EQ( "2001:db8:1::20", opt->getAddresses()[0].toText());
+
+ expected = Element::fromJSON(config);
+ opt_data = expected->get("option-data")->getNonConst(0);
+ opt_data->set("name", Element::create(std::string("dns-servers")));
+ opt_data->set("space", Element::create(std::string(DHCP6_OPTION_SPACE)));
+ CfgOptionsTest cfg2(CfgMgr::instance().getStagingCfg());
+ cfg2.runCfgOptionsTest(family_, expected);
+}
+
+// This test verifies that the option data configuration with a minimal
+// set of parameters works as expected when option definition is
+// created in the configuration file.
+TEST_F(ParseConfigTest, optionDataMinimalWithOptionDef) {
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo-name\","
+ " \"code\": 2345,"
+ " \"type\": \"ipv6-address\","
+ " \"array\": true,"
+ " \"space\": \"dhcp6\""
+ " } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"foo-name\","
+ " \"data\": \"2001:db8:1::10, 2001:db8:1::123\""
+ " } ]"
+ "}";
+
+ int rcode = 0;
+ ASSERT_NO_THROW(rcode = parseConfiguration(config, true));
+ EXPECT_EQ(0, rcode);
+ Option6AddrLstPtr opt = boost::dynamic_pointer_cast<
+ Option6AddrLst>(getOptionPtr(DHCP6_OPTION_SPACE, 2345));
+ ASSERT_TRUE(opt);
+ ASSERT_EQ(2, opt->getAddresses().size());
+ EXPECT_EQ("2001:db8:1::10", opt->getAddresses()[0].toText());
+ EXPECT_EQ("2001:db8:1::123", opt->getAddresses()[1].toText());
+
+ ElementPtr expected = Element::fromJSON(config);
+ ElementPtr opt_data = expected->get("option-data")->getNonConst(0);
+ opt_data->set("code", Element::create(2345));
+ opt_data->set("space", Element::create(std::string(DHCP6_OPTION_SPACE)));
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, expected);
+
+ CfgMgr::instance().clear();
+ // Do the same test but now use an option code.
+ config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo-name\","
+ " \"code\": 2345,"
+ " \"type\": \"ipv6-address\","
+ " \"array\": true,"
+ " \"space\": \"dhcp6\""
+ " } ],"
+ " \"option-data\": [ {"
+ " \"code\": 2345,"
+ " \"data\": \"2001:db8:1::10, 2001:db8:1::123\""
+ " } ]"
+ "}";
+
+ rcode = 0;
+ ASSERT_NO_THROW(rcode = parseConfiguration(config, true));
+ EXPECT_EQ(0, rcode);
+ opt = boost::dynamic_pointer_cast<Option6AddrLst>(getOptionPtr(DHCP6_OPTION_SPACE,
+ 2345));
+ ASSERT_TRUE(opt);
+ ASSERT_EQ(2, opt->getAddresses().size());
+ EXPECT_EQ("2001:db8:1::10", opt->getAddresses()[0].toText());
+ EXPECT_EQ("2001:db8:1::123", opt->getAddresses()[1].toText());
+
+ expected = Element::fromJSON(config);
+ opt_data = expected->get("option-data")->getNonConst(0);
+ opt_data->set("name", Element::create(std::string("foo-name")));
+ opt_data->set("space", Element::create(std::string(DHCP6_OPTION_SPACE)));
+ CfgOptionsTest cfg2(CfgMgr::instance().getStagingCfg());
+ cfg2.runCfgOptionsTest(family_, expected);
+}
+
+// This test verifies an empty option data configuration is supported.
+TEST_F(ParseConfigTest, emptyOptionData) {
+ // Configuration string.
+ const std::string config =
+ "{ \"option-data\": [ {"
+ " \"name\": \"dhcp4o6-server-addr\""
+ " } ]"
+ "}";
+
+ int rcode = 0;
+ ASSERT_NO_THROW(rcode = parseConfiguration(config, true));
+ EXPECT_EQ(0, rcode);
+ const Option6AddrLstPtr opt = boost::dynamic_pointer_cast<
+ Option6AddrLst>(getOptionPtr(DHCP6_OPTION_SPACE, D6O_DHCPV4_O_DHCPV6_SERVER));
+ ASSERT_TRUE(opt);
+ ASSERT_EQ(0, opt->getAddresses().size());
+
+ ElementPtr expected = Element::fromJSON(config);
+ ElementPtr opt_data = expected->get("option-data")->getNonConst(0);
+ opt_data->set("code", Element::create(D6O_DHCPV4_O_DHCPV6_SERVER));
+ opt_data->set("space", Element::create(std::string(DHCP6_OPTION_SPACE)));
+ opt_data->set("csv-format", Element::create(false));
+ opt_data->set("data", Element::create(std::string("")));
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, expected);
+}
+
+// This test verifies an option data without suboptions is supported
+TEST_F(ParseConfigTest, optionDataNoSubOption) {
+ // Configuration string. A global definition for option 43 is needed.
+ const std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"vendor-encapsulated-options\","
+ " \"code\": 43,"
+ " \"type\": \"empty\","
+ " \"space\": \"dhcp4\","
+ " \"encapsulate\": \"vendor-encapsulated-options\""
+ " } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"vendor-encapsulated-options\""
+ " } ]"
+ "}";
+
+ // The default universe is V6. We need to change it to use dhcp4 option
+ // space.
+ family_ = AF_INET;
+ int rcode = 0;
+ ASSERT_NO_THROW(rcode = parseConfiguration(config));
+ EXPECT_EQ(0, rcode);
+ const OptionPtr opt = getOptionPtr(DHCP4_OPTION_SPACE, DHO_VENDOR_ENCAPSULATED_OPTIONS);
+ ASSERT_TRUE(opt);
+ ASSERT_EQ(0, opt->getOptions().size());
+
+ ElementPtr expected = Element::fromJSON(config);
+ ElementPtr opt_data = expected->get("option-data")->getNonConst(0);
+ opt_data->set("code", Element::create(DHO_VENDOR_ENCAPSULATED_OPTIONS));
+ opt_data->set("space", Element::create(std::string(DHCP4_OPTION_SPACE)));
+ opt_data->set("csv-format", Element::create(false));
+ opt_data->set("data", Element::create(std::string("")));
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, expected);
+}
+
+// This tests option-data in CSV format and embedded commas.
+TEST_F(ParseConfigTest, commaCSVFormatOptionData) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-data\": [ {"
+ " \"csv-format\": true,"
+ " \"code\": 41,"
+ " \"data\": \"EST5EDT4\\\\,M3.2.0/02:00\\\\,M11.1.0/02:00\","
+ " \"space\": \"dhcp6\""
+ " } ]"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config, true);
+ ASSERT_EQ(0, rcode);
+
+ // Verify that the option can be retrieved.
+ OptionPtr opt = getOptionPtr(DHCP6_OPTION_SPACE, 41);
+ ASSERT_TRUE(opt);
+
+ // Get the option as an option string.
+ OptionStringPtr opt_str = boost::dynamic_pointer_cast<OptionString>(opt);
+ ASSERT_TRUE(opt_str);
+
+
+ // Verify that the option data is correct.
+ string val = "EST5EDT4,M3.2.0/02:00,M11.1.0/02:00";
+ EXPECT_EQ(val, opt_str->getValue());
+
+ ElementPtr expected = Element::fromJSON(config);
+ ElementPtr opt_data = expected->get("option-data")->getNonConst(0);
+ opt_data->remove("csv-format");
+ opt_data->set("name", Element::create(std::string("new-posix-timezone")));
+ CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg());
+ cfg.runCfgOptionsTest(family_, expected);
+}
+
+// Verifies that hex literals can support a variety of formats.
+TEST_F(ParseConfigTest, hexOptionData) {
+
+ // All of the following variants should parse correctly
+ // into the same two IPv4 addresses: 12.0.3.1 and 192.0.3.2
+ std::vector<std::string> valid_hexes = {
+ "0C000301C0000302", // even number
+ "C000301C0000302", // odd number
+ "0C 00 03 01 C0 00 03 02", // spaces
+ "0C:00:03:01:C0:00:03:02", // colons
+ "0x0C000301C0000302", // 0x
+ "C 0 3 1 C0 0 3 02", // one or two digit octets
+ "0x0c000301C0000302" // upper or lower case digits
+ };
+
+ for (auto hex_str : valid_hexes) {
+ ostringstream os;
+ os <<
+ "{ \n"
+ " \"option-data\": [ { \n"
+ " \"name\": \"domain-name-servers\", \n"
+ " \"code\": 6, \n"
+ " \"space\": \"dhcp4\", \n"
+ " \"csv-format\": false, \n"
+ " \"data\": \"" << hex_str << "\" \n"
+ " } ] \n"
+ "} \n";
+
+ reset_context(AF_INET);
+ int rcode = 0;
+ ASSERT_NO_THROW(rcode = parseConfiguration(os.str(), true));
+ EXPECT_EQ(0, rcode);
+
+ Option4AddrLstPtr opt = boost::dynamic_pointer_cast<Option4AddrLst>
+ (getOptionPtr(DHCP4_OPTION_SPACE, 6));
+ ASSERT_TRUE(opt);
+ ASSERT_EQ(2, opt->getAddresses().size());
+ EXPECT_EQ("12.0.3.1", opt->getAddresses()[0].toText());
+ EXPECT_EQ("192.0.3.2", opt->getAddresses()[1].toText());
+ }
+}
+
+// Verifies that binary option data can be configured with either
+// "'strings'" or hex literals.
+TEST_F(ParseConfigTest, stringOrHexBinaryData) {
+ // Structure the defines a given test scenario
+ struct Scenario {
+ std::string description_; // describes the scenario for logging
+ std::string str_data_; // configured data value of the option
+ std::vector<uint8_t> exp_binary_; // expected parsed binary data
+ std::string exp_error_; // expected error test for invalid input
+ };
+
+ // Convenience value to use for initting valid scenarios
+ std::string no_error("");
+
+ // Valid and invalid scenarios we will test.
+ // Note we are not concerned with the varitions of valid or invalid
+ // hex literals those are tested elsewhere.
+ std::vector<Scenario> scenarios = {
+ {
+ "valid hex digits",
+ "0C:00:03:01:C0:00:03:02",
+ {0x0C,0x00,0x03,0x01,0xC0,0x00,0x03,0x02},
+ no_error
+ },
+ {
+ "valid string",
+ "'abcdefghijk'",
+ {0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B},
+ no_error
+ },
+ {
+ "valid empty",
+ "",
+ {},
+ no_error
+ },
+ {
+ "invalid empty",
+ "''",
+ {},
+ "Configuration parsing failed: option data is not a valid string"
+ " of hexadecimal digits: '' (<string>:7:13)"
+ },
+ {
+ "missing end quote",
+ "'abcdefghijk",
+ {},
+ "Configuration parsing failed: option data is not a valid string"
+ " of hexadecimal digits: 'abcdefghijk (<string>:7:13)"
+ },
+ {
+ "missing open quote",
+ "abcdefghijk'",
+ {},
+ "Configuration parsing failed: option data is not a valid string"
+ " of hexadecimal digits: abcdefghijk' (<string>:7:13)"
+ },
+ {
+ "no quotes",
+ "abcdefghijk",
+ {},
+ "Configuration parsing failed: option data is not a valid string"
+ " of hexadecimal digits: abcdefghijk (<string>:7:13)"
+ }
+ };
+
+ // Iterate over our test scenarios
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+ {
+ // Build the configuration text.
+ ostringstream os;
+ os <<
+ "{ \n"
+ " \"option-data\": [ { \n"
+ " \"name\": \"user-class\", \n"
+ " \"code\": 77, \n"
+ " \"space\": \"dhcp4\", \n"
+ " \"csv-format\": false, \n"
+ " \"data\": \"" << scenario.str_data_ << "\" \n"
+ " } ] \n"
+ "} \n";
+
+ // Attempt to parse it.
+ reset_context(AF_INET);
+ int rcode = 0;
+ ASSERT_NO_THROW(rcode = parseConfiguration(os.str(), true));
+
+ if (!scenario.exp_error_.empty()) {
+ // We expected to fail, did we?
+ ASSERT_NE(0, rcode);
+ // Did we fail for the reason we think we should?
+ EXPECT_EQ(error_text_, scenario.exp_error_);
+ } else {
+ // We expected to succeed, did we?
+ ASSERT_EQ(0, rcode);
+ OptionPtr opt = getOptionPtr(DHCP4_OPTION_SPACE, 77);
+ ASSERT_TRUE(opt);
+ // Verify the parsed data is correct.
+ EXPECT_EQ(opt->getData(), scenario.exp_binary_);
+ }
+ }
+ }
+}
+
+
+/// The next set of tests check basic operation of the HooksLibrariesParser.
+//
+// Convenience function to set a configuration of zero or more hooks
+// libraries:
+//
+// lib1 - No parameters
+// lib2 - Empty parameters statement
+// lib3 - Valid parameters
+std::string
+setHooksLibrariesConfig(const char* lib1 = NULL, const char* lib2 = NULL,
+ const char* lib3 = NULL) {
+ const string lbrace("{");
+ const string rbrace("}");
+ const string quote("\"");
+ const string comma_space(", ");
+ const string library("\"library\": ");
+
+ string config = string("{ \"hooks-libraries\": [");
+ if (lib1 != NULL) {
+ // Library 1 has no parameters
+ config += lbrace;
+ config += library + quote + std::string(lib1) + quote;
+ config += rbrace;
+
+ if (lib2 != NULL) {
+ // Library 2 has an empty parameters statement
+ config += comma_space + lbrace;
+ config += library + quote + std::string(lib2) + quote + comma_space;
+ config += string("\"parameters\": {}");
+ config += rbrace;
+
+ if (lib3 != NULL) {
+ // Library 3 has valid parameters
+ config += comma_space + lbrace;
+ config += library + quote + std::string(lib3) + quote + comma_space;
+ config += string("\"parameters\": {");
+ config += string(" \"svalue\": \"string value\", ");
+ config += string(" \"ivalue\": 42, "); // Integer value
+ config += string(" \"bvalue\": true"); // Boolean value
+ config += string("}");
+ config += rbrace;
+ }
+ }
+ }
+ config += std::string("] }");
+
+ return (config);
+}
+
+// hooks-libraries element that does not contain anything.
+TEST_F(ParseConfigTest, noHooksLibraries) {
+ // Check that no libraries are currently loaded
+ vector<string> hooks_libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(hooks_libraries.empty());
+
+ // Create an empty hooks-libraries configuration element.
+ const string config = setHooksLibrariesConfig();
+
+ // Verify that the configuration string parses.
+ const int rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // Verify that the configuration object unparses.
+ ConstElementPtr expected;
+ ASSERT_NO_THROW(expected =
+ Element::fromJSON(config)->get("hooks-libraries"));
+ ASSERT_TRUE(expected);
+ const HooksConfig& cfg =
+ CfgMgr::instance().getStagingCfg()->getHooksConfig();
+ runToElementTest<HooksConfig>(expected, cfg);
+
+ // Check that the parser recorded nothing.
+ isc::hooks::HookLibsCollection libraries = getLibraries();
+ EXPECT_TRUE(libraries.empty());
+
+ // Check that there are still no libraries loaded.
+ hooks_libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(hooks_libraries.empty());
+}
+
+// hooks-libraries element that contains a single library.
+TEST_F(ParseConfigTest, oneHooksLibrary) {
+ // Check that no libraries are currently loaded
+ vector<string> hooks_libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(hooks_libraries.empty());
+
+ // Configuration with hooks-libraries set to a single library.
+ const string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1);
+
+ // Verify that the configuration string parses.
+ const int rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // Verify that the configuration object unparses.
+ ConstElementPtr expected;
+ ASSERT_NO_THROW(expected =
+ Element::fromJSON(config)->get("hooks-libraries"));
+ ASSERT_TRUE(expected);
+ const HooksConfig& cfg =
+ CfgMgr::instance().getStagingCfg()->getHooksConfig();
+ runToElementTest<HooksConfig>(expected, cfg);
+
+ // Check that the parser recorded a single library.
+ isc::hooks::HookLibsCollection libraries = getLibraries();
+ ASSERT_EQ(1, libraries.size());
+ EXPECT_EQ(CALLOUT_LIBRARY_1, libraries[0].first);
+
+ // Check that the change was propagated to the hooks manager.
+ hooks_libraries = HooksManager::getLibraryNames();
+ ASSERT_EQ(1, hooks_libraries.size());
+ EXPECT_EQ(CALLOUT_LIBRARY_1, hooks_libraries[0]);
+}
+
+// hooks-libraries element that contains two libraries
+TEST_F(ParseConfigTest, twoHooksLibraries) {
+ // Check that no libraries are currently loaded
+ vector<string> hooks_libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(hooks_libraries.empty());
+
+ // Configuration with hooks-libraries set to two libraries.
+ const string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1,
+ CALLOUT_LIBRARY_2);
+
+ // Verify that the configuration string parses.
+ const int rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // Verify that the configuration object unparses.
+ ConstElementPtr expected;
+ ASSERT_NO_THROW(expected =
+ Element::fromJSON(config)->get("hooks-libraries"));
+ ASSERT_TRUE(expected);
+ const HooksConfig& cfg =
+ CfgMgr::instance().getStagingCfg()->getHooksConfig();
+ runToElementTest<HooksConfig>(expected, cfg);
+
+ // Check that the parser recorded two libraries in the expected order.
+ isc::hooks::HookLibsCollection libraries = getLibraries();
+ ASSERT_EQ(2, libraries.size());
+ EXPECT_EQ(CALLOUT_LIBRARY_1, libraries[0].first);
+ EXPECT_EQ(CALLOUT_LIBRARY_2, libraries[1].first);
+
+ // Verify that the change was propagated to the hooks manager.
+ hooks_libraries = HooksManager::getLibraryNames();
+ ASSERT_EQ(2, hooks_libraries.size());
+ EXPECT_EQ(CALLOUT_LIBRARY_1, hooks_libraries[0]);
+ EXPECT_EQ(CALLOUT_LIBRARY_2, hooks_libraries[1]);
+}
+
+// Configure with two libraries, then reconfigure with the same libraries.
+TEST_F(ParseConfigTest, reconfigureSameHooksLibraries) {
+ // Check that no libraries are currently loaded
+ vector<string> hooks_libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(hooks_libraries.empty());
+
+ // Configuration with hooks-libraries set to two libraries.
+ const std::string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1,
+ CALLOUT_LIBRARY_2);
+
+ // Verify that the configuration string parses. The twoHooksLibraries
+ // test shows that the list will be as expected.
+ int rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // Verify that the configuration object unparses.
+ ConstElementPtr expected;
+ ASSERT_NO_THROW(expected =
+ Element::fromJSON(config)->get("hooks-libraries"));
+ ASSERT_TRUE(expected);
+ const HooksConfig& cfg =
+ CfgMgr::instance().getStagingCfg()->getHooksConfig();
+ runToElementTest<HooksConfig>(expected, cfg);
+
+ // The previous test shows that the parser correctly recorded the two
+ // libraries and that they loaded correctly.
+
+ // Parse the string again.
+ rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // The list has not changed between the two parse operations. However,
+ // the parameters (or the files they could point to) could have
+ // changed, so the libraries are reloaded anyway.
+ const HooksConfig& cfg2 =
+ CfgMgr::instance().getStagingCfg()->getHooksConfig();
+ runToElementTest<HooksConfig>(expected, cfg2);
+ isc::hooks::HookLibsCollection libraries = getLibraries();
+ ASSERT_EQ(2, libraries.size());
+ EXPECT_EQ(CALLOUT_LIBRARY_1, libraries[0].first);
+ EXPECT_EQ(CALLOUT_LIBRARY_2, libraries[1].first);
+
+ // ... and check that the same two libraries are still loaded in the
+ // HooksManager.
+ hooks_libraries = HooksManager::getLibraryNames();
+ ASSERT_EQ(2, hooks_libraries.size());
+ EXPECT_EQ(CALLOUT_LIBRARY_1, hooks_libraries[0]);
+ EXPECT_EQ(CALLOUT_LIBRARY_2, hooks_libraries[1]);
+}
+
+// Configure the hooks with two libraries, then reconfigure with the same
+// libraries, but in reverse order.
+TEST_F(ParseConfigTest, reconfigureReverseHooksLibraries) {
+ // Check that no libraries are currently loaded
+ vector<string> hooks_libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(hooks_libraries.empty());
+
+ // Configuration with hooks-libraries set to two libraries.
+ std::string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1,
+ CALLOUT_LIBRARY_2);
+
+ // Verify that the configuration string parses. The twoHooksLibraries
+ // test shows that the list will be as expected.
+ int rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // A previous test shows that the parser correctly recorded the two
+ // libraries and that they loaded correctly.
+
+ // Parse the reversed set of libraries.
+ config = setHooksLibrariesConfig(CALLOUT_LIBRARY_2, CALLOUT_LIBRARY_1);
+ rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // The list has changed, and this is what we should see.
+ isc::hooks::HookLibsCollection libraries = getLibraries();
+ ASSERT_EQ(2, libraries.size());
+ EXPECT_EQ(CALLOUT_LIBRARY_2, libraries[0].first);
+ EXPECT_EQ(CALLOUT_LIBRARY_1, libraries[1].first);
+
+ // ... and check that this was propagated to the HooksManager.
+ hooks_libraries = HooksManager::getLibraryNames();
+ ASSERT_EQ(2, hooks_libraries.size());
+ EXPECT_EQ(CALLOUT_LIBRARY_2, hooks_libraries[0]);
+ EXPECT_EQ(CALLOUT_LIBRARY_1, hooks_libraries[1]);
+}
+
+// Configure the hooks with two libraries, then reconfigure with
+// no libraries.
+TEST_F(ParseConfigTest, reconfigureZeroHooksLibraries) {
+ // Check that no libraries are currently loaded
+ vector<string> hooks_libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(hooks_libraries.empty());
+
+ // Configuration with hooks-libraries set to two libraries.
+ std::string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1,
+ CALLOUT_LIBRARY_2);
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // A previous test shows that the parser correctly recorded the two
+ // libraries and that they loaded correctly.
+
+ // Parse the string again, this time without any libraries.
+ config = setHooksLibrariesConfig();
+ rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // Verify that the configuration object unparses.
+ ConstElementPtr expected;
+ ASSERT_NO_THROW(expected =
+ Element::fromJSON(config)->get("hooks-libraries"));
+ ASSERT_TRUE(expected);
+ const HooksConfig& cfg =
+ CfgMgr::instance().getStagingCfg()->getHooksConfig();
+ runToElementTest<HooksConfig>(expected, cfg);
+
+ // The list has changed, and this is what we should see.
+ isc::hooks::HookLibsCollection libraries = getLibraries();
+ EXPECT_TRUE(libraries.empty());
+
+ // Check that no libraries are currently loaded
+ hooks_libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(hooks_libraries.empty());
+}
+
+// Check with a set of libraries, some of which are invalid.
+TEST_F(ParseConfigTest, invalidHooksLibraries) {
+ // Check that no libraries are currently loaded
+ vector<string> hooks_libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(hooks_libraries.empty());
+
+ // Configuration string. This contains an invalid library which should
+ // trigger an error in the "build" stage.
+ const std::string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1,
+ NOT_PRESENT_LIBRARY,
+ CALLOUT_LIBRARY_2);
+
+ // Verify that the configuration fails to parse. (Syntactically it's OK,
+ // but the library is invalid).
+ const int rcode = parseConfiguration(config);
+ ASSERT_FALSE(rcode == 0) << error_text_;
+
+ // Check that the message contains the library in error.
+ EXPECT_FALSE(error_text_.find(NOT_PRESENT_LIBRARY) == string::npos) <<
+ "Error text returned from parse failure is " << error_text_;
+
+ // Check that the parser recorded the names but, as they were in error,
+ // does not flag them as changed.
+ isc::hooks::HookLibsCollection libraries = getLibraries();
+ ASSERT_EQ(3, libraries.size());
+ EXPECT_EQ(CALLOUT_LIBRARY_1, libraries[0].first);
+ EXPECT_EQ(NOT_PRESENT_LIBRARY, libraries[1].first);
+ EXPECT_EQ(CALLOUT_LIBRARY_2, libraries[2].first);
+
+ // ...and check it did not alter the libraries in the hooks manager.
+ hooks_libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(hooks_libraries.empty());
+}
+
+// Check that trying to reconfigure with an invalid set of libraries fails.
+TEST_F(ParseConfigTest, reconfigureInvalidHooksLibraries) {
+ // Check that no libraries are currently loaded
+ vector<string> hooks_libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(hooks_libraries.empty());
+
+ // Configure with a single library.
+ std::string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1);
+ int rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // A previous test shows that the parser correctly recorded the two
+ // libraries and that they loaded correctly.
+
+ // Configuration string. This contains an invalid library which should
+ // trigger an error in the "build" stage.
+ config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1, NOT_PRESENT_LIBRARY,
+ CALLOUT_LIBRARY_2);
+
+ // Verify that the configuration fails to parse. (Syntactically it's OK,
+ // but the library is invalid).
+ rcode = parseConfiguration(config);
+ EXPECT_FALSE(rcode == 0) << error_text_;
+
+ // Check that the message contains the library in error.
+ EXPECT_FALSE(error_text_.find(NOT_PRESENT_LIBRARY) == string::npos) <<
+ "Error text returned from parse failure is " << error_text_;
+
+ // Check that the parser recorded the names but, as the library set was
+ // incorrect, did not mark the configuration as changed.
+ isc::hooks::HookLibsCollection libraries = getLibraries();
+ ASSERT_EQ(3, libraries.size());
+ EXPECT_EQ(CALLOUT_LIBRARY_1, libraries[0].first);
+ EXPECT_EQ(NOT_PRESENT_LIBRARY, libraries[1].first);
+ EXPECT_EQ(CALLOUT_LIBRARY_2, libraries[2].first);
+
+ // ... but check that the hooks manager was not updated with the incorrect
+ // names.
+ hooks_libraries.clear();
+ hooks_libraries = HooksManager::getLibraryNames();
+ ASSERT_EQ(1, hooks_libraries.size());
+ EXPECT_EQ(CALLOUT_LIBRARY_1, hooks_libraries[0]);
+}
+
+// Check that if hooks-libraries contains invalid syntax, it is detected.
+TEST_F(ParseConfigTest, invalidSyntaxHooksLibraries) {
+
+ // Element holds a mixture of (valid) maps and non-maps.
+ string config1 = "{ \"hooks-libraries\": [ "
+ "{ \"library\": \"/opt/lib/lib1\" }, "
+ "\"/opt/lib/lib2\" "
+ "] }";
+ string error1 = "one or more entries in the hooks-libraries list is not"
+ " a map";
+
+ int rcode = parseConfiguration(config1);
+ ASSERT_NE(0, rcode);
+ EXPECT_TRUE(error_text_.find(error1) != string::npos) <<
+ "Error text returned from parse failure is " << error_text_;
+
+ // Element holds valid maps, except one where the library element is not
+ // a string.
+ string config2 = "{ \"hooks-libraries\": [ "
+ "{ \"library\": \"/opt/lib/lib1\" }, "
+ "{ \"library\": 123 } "
+ "] }";
+ string error2 = "value of 'library' element is not a string giving"
+ " the path to a hooks library";
+
+ rcode = parseConfiguration(config2);
+ ASSERT_NE(0, rcode);
+ EXPECT_TRUE(error_text_.find(error2) != string::npos) <<
+ "Error text returned from parse failure is " << error_text_;
+
+ // Element holds valid maps, except one where the library element is the
+ // empty string.
+ string config3 = "{ \"hooks-libraries\": [ "
+ "{ \"library\": \"/opt/lib/lib1\" }, "
+ "{ \"library\": \"\" } "
+ "] }";
+ string error3 = "value of 'library' element must not be blank";
+
+ rcode = parseConfiguration(config3);
+ ASSERT_NE(0, rcode);
+ EXPECT_TRUE(error_text_.find(error3) != string::npos) <<
+ "Error text returned from parse failure is " << error_text_;
+
+ // Element holds valid maps, except one where the library element is all
+ // spaces.
+ string config4 = "{ \"hooks-libraries\": [ "
+ "{ \"library\": \"/opt/lib/lib1\" }, "
+ "{ \"library\": \" \" } "
+ "] }";
+ string error4 = "value of 'library' element must not be blank";
+
+ rcode = parseConfiguration(config4);
+ ASSERT_NE(0, rcode);
+ EXPECT_TRUE(error_text_.find(error3) != string::npos) <<
+ "Error text returned from parse failure is " << error_text_;
+
+ // Element holds valid maps, except one that does not contain a
+ // 'library' element.
+ string config5 = "{ \"hooks-libraries\": [ "
+ "{ \"library\": \"/opt/lib/lib1\" }, "
+ "{ \"parameters\": { \"alpha\": 123 } }, "
+ "{ \"library\": \"/opt/lib/lib2\" } "
+ "] }";
+ string error5 = "one or more hooks-libraries elements are missing the"
+ " name of the library";
+
+ rcode = parseConfiguration(config5);
+ ASSERT_NE(0, rcode);
+ EXPECT_TRUE(error_text_.find(error5) != string::npos) <<
+ "Error text returned from parse failure is " << error_text_;
+}
+
+// Check that some parameters may have configuration parameters configured.
+TEST_F(ParseConfigTest, HooksLibrariesParameters) {
+ // Check that no libraries are currently loaded
+ vector<string> hooks_libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(hooks_libraries.empty());
+
+ // Configuration string. This contains an invalid library which should
+ // trigger an error in the "build" stage.
+ const std::string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1,
+ CALLOUT_LIBRARY_2,
+ CALLOUT_PARAMS_LIBRARY);
+
+ // Verify that the configuration fails to parse. (Syntactically it's OK,
+ // but the library is invalid).
+ const int rcode = parseConfiguration(config);
+ ASSERT_EQ(0, rcode);
+
+ // Verify that the configuration object unparses.
+ ConstElementPtr expected;
+ ASSERT_NO_THROW(expected =
+ Element::fromJSON(config)->get("hooks-libraries"));
+ ASSERT_TRUE(expected);
+ const HooksConfig& cfg =
+ CfgMgr::instance().getStagingCfg()->getHooksConfig();
+ runToElementTest<HooksConfig>(expected, cfg);
+
+ // Check that the parser recorded the names.
+ isc::hooks::HookLibsCollection libraries = getLibraries();
+ ASSERT_EQ(3, libraries.size());
+ EXPECT_EQ(CALLOUT_LIBRARY_1, libraries[0].first);
+ EXPECT_EQ(CALLOUT_LIBRARY_2, libraries[1].first);
+ EXPECT_EQ(CALLOUT_PARAMS_LIBRARY, libraries[2].first);
+
+ // Also, check that the third library has its parameters specified.
+ // They were set by setHooksLibrariesConfig. The first has no
+ // parameters, the second one has an empty map and the third
+ // one has actual parameters.
+ EXPECT_FALSE(libraries[0].second);
+ EXPECT_TRUE(libraries[1].second);
+ ASSERT_TRUE(libraries[2].second);
+
+ // Ok, get the parameter for the third library.
+ ConstElementPtr params = libraries[2].second;
+
+ // It must be a map.
+ ASSERT_EQ(Element::map, params->getType());
+
+ // This map should have 3 parameters:
+ // - svalue (and will expect its value to be "string value")
+ // - ivalue (and will expect its value to be 42)
+ // - bvalue (and will expect its value to be true)
+ ConstElementPtr svalue = params->get("svalue");
+ ConstElementPtr ivalue = params->get("ivalue");
+ ConstElementPtr bvalue = params->get("bvalue");
+
+ // There should be no extra parameters.
+ EXPECT_FALSE(params->get("nonexistent"));
+
+ ASSERT_TRUE(svalue);
+ ASSERT_TRUE(ivalue);
+ ASSERT_TRUE(bvalue);
+
+ ASSERT_EQ(Element::string, svalue->getType());
+ ASSERT_EQ(Element::integer, ivalue->getType());
+ ASSERT_EQ(Element::boolean, bvalue->getType());
+
+ EXPECT_EQ("string value", svalue->stringValue());
+ EXPECT_EQ(42, ivalue->intValue());
+ EXPECT_EQ(true, bvalue->boolValue());
+}
+
+/// @brief Checks that a valid, enabled D2 client configuration works correctly.
+TEST_F(ParseConfigTest, validD2Config) {
+
+ // Configuration string containing valid values.
+ std::string config_str =
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.0.2.0\", "
+ " \"server-port\" : 3432, "
+ " \"sender-ip\" : \"192.0.2.1\", "
+ " \"sender-port\" : 3433, "
+ " \"max-queue-size\" : 2048, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"user-context\": { \"foo\": \"bar\" } "
+ " }"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config_str);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // Verify that DHCP-DDNS is enabled and we can fetch the configuration.
+ EXPECT_TRUE(CfgMgr::instance().ddnsEnabled());
+ D2ClientConfigPtr d2_client_config;
+ ASSERT_NO_THROW(d2_client_config = CfgMgr::instance().getD2ClientConfig());
+ ASSERT_TRUE(d2_client_config);
+
+ // Verify that the configuration values are as expected.
+ EXPECT_TRUE(d2_client_config->getEnableUpdates());
+ EXPECT_EQ("192.0.2.0", d2_client_config->getServerIp().toText());
+ EXPECT_EQ(3432, d2_client_config->getServerPort());
+ EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_client_config->getNcrProtocol());
+ EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_client_config->getNcrFormat());
+ ASSERT_TRUE(d2_client_config->getContext());
+ EXPECT_EQ("{ \"foo\": \"bar\" }", d2_client_config->getContext()->str());
+
+ // Verify that the configuration object unparses.
+ ConstElementPtr expected;
+ ASSERT_NO_THROW(expected = Element::fromJSON(config_str)->get("dhcp-ddns"));
+ ASSERT_TRUE(expected);
+ runToElementTest<D2ClientConfig>(expected, *d2_client_config);
+
+ // Another valid Configuration string.
+ // This one is disabled, has IPV6 server ip, control flags false,
+ // empty prefix/suffix
+ std::string config_str2 =
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : false, "
+ " \"server-ip\" : \"2001:db8::\", "
+ " \"server-port\" : 43567, "
+ " \"sender-ip\" : \"2001:db8::1\", "
+ " \"sender-port\" : 3433, "
+ " \"max-queue-size\" : 2048, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"user-context\": { \"foo\": \"bar\" } "
+ " }"
+ "}";
+
+ // Verify that the configuration string parses.
+ rcode = parseConfiguration(config_str2);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // Verify that DHCP-DDNS is disabled and we can fetch the configuration.
+ EXPECT_FALSE(CfgMgr::instance().ddnsEnabled());
+ ASSERT_NO_THROW(d2_client_config = CfgMgr::instance().getD2ClientConfig());
+ ASSERT_TRUE(d2_client_config);
+
+ // Verify that the configuration values are as expected.
+ EXPECT_FALSE(d2_client_config->getEnableUpdates());
+ EXPECT_EQ("2001:db8::", d2_client_config->getServerIp().toText());
+ EXPECT_EQ(43567, d2_client_config->getServerPort());
+ EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_client_config->getNcrProtocol());
+ EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_client_config->getNcrFormat());
+ ASSERT_TRUE(d2_client_config->getContext());
+ EXPECT_EQ("{ \"foo\": \"bar\" }", d2_client_config->getContext()->str());
+
+ ASSERT_NO_THROW(expected = Element::fromJSON(config_str2)->get("dhcp-ddns"));
+ ASSERT_TRUE(expected);
+ runToElementTest<D2ClientConfig>(expected, *d2_client_config);
+}
+
+/// @brief Checks that D2 client can be configured with enable flag of
+/// false only.
+TEST_F(ParseConfigTest, validDisabledD2Config) {
+
+ // Configuration string. This defines a disabled D2 client config.
+ std::string config_str =
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : false"
+ " }"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config_str);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // Verify that DHCP-DDNS is disabled.
+ EXPECT_FALSE(CfgMgr::instance().ddnsEnabled());
+
+ // Make sure fetched config agrees.
+ D2ClientConfigPtr d2_client_config;
+ ASSERT_NO_THROW(d2_client_config = CfgMgr::instance().getD2ClientConfig());
+ EXPECT_TRUE(d2_client_config);
+ EXPECT_FALSE(d2_client_config->getEnableUpdates());
+}
+
+/// @brief Checks that given a partial configuration, parser supplies
+/// default values
+TEST_F(ParseConfigTest, parserDefaultsD2Config) {
+
+ // Configuration string. This defines an enabled D2 client config
+ // with the mandatory parameter in such a case, all other parameters
+ // are optional and their default values will be used.
+ std::string config_str =
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true "
+ " }"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config_str);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // Verify that DHCP-DDNS is enabled.
+ EXPECT_TRUE(CfgMgr::instance().ddnsEnabled());
+
+ // Make sure fetched config is correct.
+ D2ClientConfigPtr d2_client_config;
+ ASSERT_NO_THROW(d2_client_config = CfgMgr::instance().getD2ClientConfig());
+ EXPECT_TRUE(d2_client_config);
+ EXPECT_TRUE(d2_client_config->getEnableUpdates());
+ EXPECT_EQ(D2ClientConfig::DFT_SERVER_IP,
+ d2_client_config->getServerIp().toText());
+ EXPECT_EQ(D2ClientConfig::DFT_SERVER_PORT,
+ d2_client_config->getServerPort());
+ EXPECT_EQ(dhcp_ddns::stringToNcrProtocol(D2ClientConfig::DFT_NCR_PROTOCOL),
+ d2_client_config->getNcrProtocol());
+ EXPECT_EQ(dhcp_ddns::stringToNcrFormat(D2ClientConfig::DFT_NCR_FORMAT),
+ d2_client_config->getNcrFormat());
+}
+
+
+/// @brief Check various invalid D2 client configurations.
+TEST_F(ParseConfigTest, invalidD2Config) {
+ std::string invalid_configs[] = {
+ // Invalid server ip value
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"x192.0.2.0\", "
+ " \"server-port\" : 53001, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\" "
+ " }"
+ "}",
+ // Unknown protocol
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.0.2.0\", "
+ " \"server-port\" : 53001, "
+ " \"ncr-protocol\" : \"Bogus\", "
+ " \"ncr-format\" : \"JSON\" "
+ " }"
+ "}",
+ // Unsupported protocol
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.0.2.0\", "
+ " \"server-port\" : 53001, "
+ " \"ncr-protocol\" : \"TCP\", "
+ " \"ncr-format\" : \"JSON\" "
+ " }"
+ "}",
+ // Unknown format
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.0.2.0\", "
+ " \"server-port\" : 53001, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"Bogus\" "
+ " }"
+ "}",
+ // Invalid Port
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.0.2.0\", "
+ " \"server-port\" : \"bogus\", "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\" "
+ " }"
+ "}",
+ // Mismatched server and sender IPs
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.0.2.0\", "
+ " \"server-port\" : 3432, "
+ " \"sender-ip\" : \"3001::5\", "
+ " \"sender-port\" : 3433, "
+ " \"max-queue-size\" : 2048, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\" "
+ " }"
+ "}",
+ // Identical server and sender IP/port
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"3001::5\", "
+ " \"server-port\" : 3433, "
+ " \"sender-ip\" : \"3001::5\", "
+ " \"sender-port\" : 3433, "
+ " \"max-queue-size\" : 2048, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\" "
+ " }"
+ "}",
+ // stop
+ ""
+ };
+
+ // Fetch the original config.
+ D2ClientConfigPtr original_config;
+ ASSERT_NO_THROW(original_config = CfgMgr::instance().getD2ClientConfig());
+
+ // Iterate through the invalid configuration strings, attempting to
+ // parse each one. They should fail to parse, but fail gracefully.
+ D2ClientConfigPtr current_config;
+ int i = 0;
+ while (!invalid_configs[i].empty()) {
+ // Verify that the configuration string parses without throwing.
+ int rcode = parseConfiguration(invalid_configs[i]);
+
+ // Verify that parse result indicates a parsing error.
+ ASSERT_TRUE(rcode != 0) << "Invalid config #: " << i
+ << " should not have passed!";
+
+ // Verify that the "official" config still matches the original config.
+ ASSERT_NO_THROW(current_config =
+ CfgMgr::instance().getD2ClientConfig());
+ EXPECT_EQ(*original_config, *current_config);
+ ++i;
+ }
+}
+
+/// @brief Checks that a valid relay info structure for IPv4 can be handled
+TEST_F(ParseConfigTest, validRelayInfo4) {
+
+ // Relay information structure. Very simple for now.
+ std::string config_str =
+ " {"
+ " \"ip-address\" : \"192.0.2.1\""
+ " }";
+ ElementPtr json = Element::fromJSON(config_str);
+
+ // Create an "empty" RelayInfo to hold the parsed result.
+ Network::RelayInfoPtr result(new Network::RelayInfo());
+
+ RelayInfoParser parser(Option::V4);
+
+ EXPECT_NO_THROW(parser.parse(result, json));
+ EXPECT_TRUE(result->containsAddress(IOAddress("192.0.2.1")));
+}
+
+/// @brief Checks that a bogus relay info structure for IPv4 is rejected.
+TEST_F(ParseConfigTest, bogusRelayInfo4) {
+
+ // Invalid config (wrong family type of the ip-address field)
+ std::string config_str_bogus1 =
+ " {"
+ " \"ip-address\" : \"2001:db8::1\""
+ " }";
+ ElementPtr json_bogus1 = Element::fromJSON(config_str_bogus1);
+
+ // Invalid config (that thing is not an IPv4 address)
+ std::string config_str_bogus2 =
+ " {"
+ " \"ip-address\" : \"256.345.123.456\""
+ " }";
+ ElementPtr json_bogus2 = Element::fromJSON(config_str_bogus2);
+
+ // Invalid config (ip-address is mandatory)
+ std::string config_str_bogus3 =
+ " {"
+ " }";
+ ElementPtr json_bogus3 = Element::fromJSON(config_str_bogus3);
+
+ // Create an "empty" RelayInfo to hold the parsed result.
+ Network::RelayInfoPtr result(new Network::RelayInfo());
+
+ RelayInfoParser parser(Option::V4);
+
+ // wrong family type
+ EXPECT_THROW(parser.parse(result, json_bogus1), DhcpConfigError);
+
+ // Too large byte values in pseudo-IPv4 addr
+ EXPECT_THROW(parser.parse(result, json_bogus2), DhcpConfigError);
+
+ // Mandatory ip-address is missing. What a pity.
+ EXPECT_THROW(parser.parse(result, json_bogus2), DhcpConfigError);
+}
+
+/// @brief Checks that a valid relay info structure for IPv6 can be handled
+TEST_F(ParseConfigTest, validRelayInfo6) {
+
+ // Relay information structure. Very simple for now.
+ std::string config_str =
+ " {"
+ " \"ip-address\" : \"2001:db8::1\""
+ " }";
+ ElementPtr json = Element::fromJSON(config_str);
+
+ // Create an "empty" RelayInfo to hold the parsed result.
+ Network::RelayInfoPtr result(new Network::RelayInfo());
+
+ RelayInfoParser parser(Option::V6);
+
+ EXPECT_NO_THROW(parser.parse(result, json));
+ EXPECT_TRUE(result->containsAddress(IOAddress("2001:db8::1")));
+}
+
+/// @brief Checks that a valid relay info structure for IPv6 can be handled
+TEST_F(ParseConfigTest, bogusRelayInfo6) {
+
+ // Invalid config (wrong family type of the ip-address field
+ std::string config_str_bogus1 =
+ " {"
+ " \"ip-address\" : \"192.0.2.1\""
+ " }";
+ ElementPtr json_bogus1 = Element::fromJSON(config_str_bogus1);
+
+ // That IPv6 address doesn't look right
+ std::string config_str_bogus2 =
+ " {"
+ " \"ip-address\" : \"2001:db8:::4\""
+ " }";
+ ElementPtr json_bogus2 = Element::fromJSON(config_str_bogus2);
+
+ // Missing mandatory ip-address field.
+ std::string config_str_bogus3 =
+ " {"
+ " }";
+ ElementPtr json_bogus3 = Element::fromJSON(config_str_bogus3);
+
+ // Create an "empty" RelayInfo to hold the parsed result.
+ Network::RelayInfoPtr result(new Network::RelayInfo());
+
+ RelayInfoParser parser(Option::V6);
+
+ // Negative scenario (wrong family type)
+ EXPECT_THROW(parser.parse(result, json_bogus1), DhcpConfigError);
+
+ // Looks like IPv6 address, but has too many colons
+ EXPECT_THROW(parser.parse(result, json_bogus2), DhcpConfigError);
+
+ // Mandatory ip-address is missing. What a pity.
+ EXPECT_THROW(parser.parse(result, json_bogus3), DhcpConfigError);
+}
+
+// This test verifies that it is possible to parse an IPv4 subnet for which
+// only mandatory parameters are specified without setting the defaults.
+TEST_F(ParseConfigTest, defaultSubnet4) {
+ std::string config =
+ "{"
+ " \"subnet4\": [ {"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"id\": 123"
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, false, false);
+ ASSERT_EQ(0, rcode);
+
+ auto subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->getBySubnetId(123);
+ ASSERT_TRUE(subnet);
+
+ EXPECT_TRUE(subnet->hasFetchGlobalsFn());
+
+ EXPECT_TRUE(subnet->getIface().unspecified());
+ EXPECT_TRUE(subnet->getIface().empty());
+
+ EXPECT_TRUE(subnet->getClientClass().unspecified());
+ EXPECT_TRUE(subnet->getClientClass().empty());
+
+ EXPECT_TRUE(subnet->getValid().unspecified());
+ EXPECT_EQ(0, subnet->getValid().get());
+
+ EXPECT_TRUE(subnet->getT1().unspecified());
+ EXPECT_EQ(0, subnet->getT1().get());
+
+ EXPECT_TRUE(subnet->getT2().unspecified());
+ EXPECT_EQ(0, subnet->getT2().get());
+
+ EXPECT_TRUE(subnet->getReservationsGlobal().unspecified());
+ EXPECT_FALSE(subnet->getReservationsGlobal().get());
+
+ EXPECT_TRUE(subnet->getReservationsInSubnet().unspecified());
+ EXPECT_TRUE(subnet->getReservationsInSubnet().get());
+
+ EXPECT_TRUE(subnet->getReservationsOutOfPool().unspecified());
+ EXPECT_FALSE(subnet->getReservationsOutOfPool().get());
+
+ EXPECT_TRUE(subnet->getCalculateTeeTimes().unspecified());
+ EXPECT_FALSE(subnet->getCalculateTeeTimes().get());
+
+ EXPECT_TRUE(subnet->getT1Percent().unspecified());
+ EXPECT_EQ(0.0, subnet->getT1Percent().get());
+
+ EXPECT_TRUE(subnet->getT2Percent().unspecified());
+ EXPECT_EQ(0.0, subnet->getT2Percent().get());
+
+ EXPECT_TRUE(subnet->getMatchClientId().unspecified());
+ EXPECT_TRUE(subnet->getMatchClientId().get());
+
+ EXPECT_TRUE(subnet->getAuthoritative().unspecified());
+ EXPECT_FALSE(subnet->getAuthoritative().get());
+
+ EXPECT_TRUE(subnet->getSiaddr().unspecified());
+ EXPECT_TRUE(subnet->getSiaddr().get().isV4Zero());
+
+ EXPECT_TRUE(subnet->getSname().unspecified());
+ EXPECT_TRUE(subnet->getSname().empty());
+
+ EXPECT_TRUE(subnet->getFilename().unspecified());
+ EXPECT_TRUE(subnet->getFilename().empty());
+
+ EXPECT_FALSE(subnet->get4o6().enabled());
+
+ EXPECT_TRUE(subnet->get4o6().getIface4o6().unspecified());
+ EXPECT_TRUE(subnet->get4o6().getIface4o6().empty());
+
+ EXPECT_TRUE(subnet->get4o6().getSubnet4o6().unspecified());
+ EXPECT_TRUE(subnet->get4o6().getSubnet4o6().get().first.isV6Zero());
+ EXPECT_EQ(128, subnet->get4o6().getSubnet4o6().get().second);
+
+ EXPECT_TRUE(subnet->getDdnsSendUpdates().unspecified());
+ EXPECT_FALSE(subnet->getDdnsSendUpdates().get());
+
+ EXPECT_TRUE(subnet->getDdnsOverrideNoUpdate().unspecified());
+ EXPECT_FALSE(subnet->getDdnsOverrideNoUpdate().get());
+
+ EXPECT_TRUE(subnet->getDdnsOverrideClientUpdate().unspecified());
+ EXPECT_FALSE(subnet->getDdnsOverrideClientUpdate().get());
+
+ EXPECT_TRUE(subnet->getDdnsReplaceClientNameMode().unspecified());
+ EXPECT_EQ(D2ClientConfig::RCM_NEVER, subnet->getDdnsReplaceClientNameMode().get());
+
+ EXPECT_TRUE(subnet->getDdnsGeneratedPrefix().unspecified());
+ EXPECT_TRUE(subnet->getDdnsGeneratedPrefix().empty());
+
+ EXPECT_TRUE(subnet->getDdnsQualifyingSuffix().unspecified());
+ EXPECT_TRUE(subnet->getDdnsQualifyingSuffix().empty());
+
+ EXPECT_TRUE(subnet->getHostnameCharSet().unspecified());
+ EXPECT_TRUE(subnet->getHostnameCharSet().empty());
+
+ EXPECT_TRUE(subnet->getHostnameCharReplacement().unspecified());
+ EXPECT_TRUE(subnet->getHostnameCharReplacement().empty());
+
+ EXPECT_TRUE(subnet->getStoreExtendedInfo().unspecified());
+ EXPECT_FALSE(subnet->getStoreExtendedInfo().get());
+
+ EXPECT_TRUE(subnet->getDdnsUpdateOnRenew().unspecified());
+ EXPECT_FALSE(subnet->getDdnsUpdateOnRenew().get());
+
+ EXPECT_TRUE(subnet->getDdnsUseConflictResolution().unspecified());
+ EXPECT_FALSE(subnet->getDdnsUseConflictResolution().get());
+}
+
+// This test verifies that it is possible to parse an IPv6 subnet for which
+// only mandatory parameters are specified without setting the defaults.
+TEST_F(ParseConfigTest, defaultSubnet6) {
+ std::string config =
+ "{"
+ " \"subnet6\": [ {"
+ " \"subnet\": \"2001:db8:1::/64\","
+ " \"id\": 123"
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, true, false);
+ ASSERT_EQ(0, rcode);
+
+ auto subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->getBySubnetId(123);
+ ASSERT_TRUE(subnet);
+
+ EXPECT_TRUE(subnet->hasFetchGlobalsFn());
+
+ EXPECT_TRUE(subnet->getIface().unspecified());
+ EXPECT_TRUE(subnet->getIface().empty());
+
+ EXPECT_TRUE(subnet->getClientClass().unspecified());
+ EXPECT_TRUE(subnet->getClientClass().empty());
+
+ EXPECT_TRUE(subnet->getValid().unspecified());
+ EXPECT_EQ(0, subnet->getValid().get());
+
+ EXPECT_TRUE(subnet->getT1().unspecified());
+ EXPECT_EQ(0, subnet->getT1().get());
+
+ EXPECT_TRUE(subnet->getT2().unspecified());
+ EXPECT_EQ(0, subnet->getT2().get());
+
+ EXPECT_TRUE(subnet->getReservationsGlobal().unspecified());
+ EXPECT_FALSE(subnet->getReservationsGlobal().get());
+
+ EXPECT_TRUE(subnet->getReservationsInSubnet().unspecified());
+ EXPECT_TRUE(subnet->getReservationsInSubnet().get());
+
+ EXPECT_TRUE(subnet->getReservationsOutOfPool().unspecified());
+ EXPECT_FALSE(subnet->getReservationsOutOfPool().get());
+
+ EXPECT_TRUE(subnet->getCalculateTeeTimes().unspecified());
+ EXPECT_FALSE(subnet->getCalculateTeeTimes().get());
+
+ EXPECT_TRUE(subnet->getT1Percent().unspecified());
+ EXPECT_EQ(0.0, subnet->getT1Percent().get());
+
+ EXPECT_TRUE(subnet->getT2Percent().unspecified());
+ EXPECT_EQ(0.0, subnet->getT2Percent().get());
+
+ EXPECT_TRUE(subnet->getPreferred().unspecified());
+ EXPECT_EQ(0, subnet->getPreferred().get());
+
+ EXPECT_TRUE(subnet->getRapidCommit().unspecified());
+ EXPECT_FALSE(subnet->getRapidCommit().get());
+
+ EXPECT_TRUE(subnet->getDdnsSendUpdates().unspecified());
+ EXPECT_FALSE(subnet->getDdnsSendUpdates().get());
+
+ EXPECT_TRUE(subnet->getDdnsOverrideNoUpdate().unspecified());
+ EXPECT_FALSE(subnet->getDdnsOverrideNoUpdate().get());
+
+ EXPECT_TRUE(subnet->getDdnsOverrideClientUpdate().unspecified());
+ EXPECT_FALSE(subnet->getDdnsOverrideClientUpdate().get());
+
+ EXPECT_TRUE(subnet->getDdnsReplaceClientNameMode().unspecified());
+ EXPECT_EQ(D2ClientConfig::RCM_NEVER, subnet->getDdnsReplaceClientNameMode().get());
+
+ EXPECT_TRUE(subnet->getDdnsGeneratedPrefix().unspecified());
+ EXPECT_EQ("", subnet->getDdnsGeneratedPrefix().get());
+
+ EXPECT_TRUE(subnet->getDdnsQualifyingSuffix().unspecified());
+ EXPECT_TRUE(subnet->getDdnsQualifyingSuffix().empty());
+
+ EXPECT_TRUE(subnet->getHostnameCharSet().unspecified());
+ EXPECT_TRUE(subnet->getHostnameCharSet().empty());
+
+ EXPECT_TRUE(subnet->getHostnameCharReplacement().unspecified());
+ EXPECT_TRUE(subnet->getHostnameCharReplacement().empty());
+
+ EXPECT_TRUE(subnet->getStoreExtendedInfo().unspecified());
+ EXPECT_FALSE(subnet->getStoreExtendedInfo().get());
+
+ EXPECT_TRUE(subnet->getDdnsUpdateOnRenew().unspecified());
+ EXPECT_FALSE(subnet->getDdnsUpdateOnRenew().get());
+
+ EXPECT_TRUE(subnet->getDdnsUseConflictResolution().unspecified());
+ EXPECT_FALSE(subnet->getDdnsUseConflictResolution().get());
+}
+
+// This test verifies that it is possible to parse an IPv4 shared network
+// for which only mandatory parameter is specified without setting the
+// defaults.
+TEST_F(ParseConfigTest, defaultSharedNetwork4) {
+ std::string config =
+ "{"
+ " \"shared-networks\": [ {"
+ " \"name\": \"frog\""
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, false, false);
+ ASSERT_EQ(0, rcode);
+
+ auto network =
+ CfgMgr::instance().getStagingCfg()->getCfgSharedNetworks4()->getByName("frog");
+ ASSERT_TRUE(network);
+
+ EXPECT_TRUE(network->hasFetchGlobalsFn());
+ EXPECT_TRUE(network->getIface().unspecified());
+ EXPECT_TRUE(network->getIface().empty());
+
+ EXPECT_TRUE(network->getClientClass().unspecified());
+ EXPECT_TRUE(network->getClientClass().empty());
+
+ EXPECT_TRUE(network->getValid().unspecified());
+ EXPECT_EQ(0, network->getValid().get());
+
+ EXPECT_TRUE(network->getT1().unspecified());
+ EXPECT_EQ(0, network->getT1().get());
+
+ EXPECT_TRUE(network->getT2().unspecified());
+ EXPECT_EQ(0, network->getT2().get());
+
+ EXPECT_TRUE(network->getReservationsGlobal().unspecified());
+ EXPECT_FALSE(network->getReservationsGlobal().get());
+
+ EXPECT_TRUE(network->getReservationsInSubnet().unspecified());
+ EXPECT_TRUE(network->getReservationsInSubnet().get());
+
+ EXPECT_TRUE(network->getReservationsOutOfPool().unspecified());
+ EXPECT_FALSE(network->getReservationsOutOfPool().get());
+
+ EXPECT_TRUE(network->getCalculateTeeTimes().unspecified());
+ EXPECT_FALSE(network->getCalculateTeeTimes().get());
+
+ EXPECT_TRUE(network->getT1Percent().unspecified());
+ EXPECT_EQ(0.0, network->getT1Percent().get());
+
+ EXPECT_TRUE(network->getT2Percent().unspecified());
+ EXPECT_EQ(0.0, network->getT2Percent().get());
+
+ EXPECT_TRUE(network->getMatchClientId().unspecified());
+ EXPECT_TRUE(network->getMatchClientId().get());
+
+ EXPECT_TRUE(network->getAuthoritative().unspecified());
+ EXPECT_FALSE(network->getAuthoritative().get());
+
+ EXPECT_TRUE(network->getDdnsSendUpdates().unspecified());
+ EXPECT_FALSE(network->getDdnsSendUpdates().get());
+
+ EXPECT_TRUE(network->getDdnsOverrideNoUpdate().unspecified());
+ EXPECT_FALSE(network->getDdnsOverrideNoUpdate().get());
+
+ EXPECT_TRUE(network->getDdnsOverrideClientUpdate().unspecified());
+ EXPECT_FALSE(network->getDdnsOverrideClientUpdate().get());
+
+ EXPECT_TRUE(network->getDdnsReplaceClientNameMode().unspecified());
+ EXPECT_EQ(D2ClientConfig::RCM_NEVER, network->getDdnsReplaceClientNameMode().get());
+
+ EXPECT_TRUE(network->getDdnsGeneratedPrefix().unspecified());
+ EXPECT_TRUE(network->getDdnsGeneratedPrefix().empty());
+
+ EXPECT_TRUE(network->getDdnsQualifyingSuffix().unspecified());
+ EXPECT_TRUE(network->getDdnsQualifyingSuffix().empty());
+
+ EXPECT_TRUE(network->getStoreExtendedInfo().unspecified());
+ EXPECT_FALSE(network->getStoreExtendedInfo().get());
+
+ EXPECT_TRUE(network->getDdnsUpdateOnRenew().unspecified());
+ EXPECT_FALSE(network->getDdnsUpdateOnRenew().get());
+
+ EXPECT_TRUE(network->getDdnsUseConflictResolution().unspecified());
+ EXPECT_FALSE(network->getDdnsUseConflictResolution().get());
+}
+
+// This test verifies that it is possible to parse an IPv6 shared network
+// for which only mandatory parameter is specified without setting the
+// defaults.
+TEST_F(ParseConfigTest, defaultSharedNetwork6) {
+ std::string config =
+ "{"
+ " \"shared-networks\": [ {"
+ " \"name\": \"frog\""
+ " } ]"
+ "}";
+
+ int rcode = parseConfiguration(config, true, false);
+ ASSERT_EQ(0, rcode);
+
+ auto network =
+ CfgMgr::instance().getStagingCfg()->getCfgSharedNetworks6()->getByName("frog");
+ ASSERT_TRUE(network);
+
+ EXPECT_TRUE(network->hasFetchGlobalsFn());
+
+ EXPECT_TRUE(network->getIface().unspecified());
+ EXPECT_TRUE(network->getIface().empty());
+
+ EXPECT_TRUE(network->getClientClass().unspecified());
+ EXPECT_TRUE(network->getClientClass().empty());
+
+ EXPECT_TRUE(network->getValid().unspecified());
+ EXPECT_EQ(0, network->getValid().get());
+
+ EXPECT_TRUE(network->getT1().unspecified());
+ EXPECT_EQ(0, network->getT1().get());
+
+ EXPECT_TRUE(network->getT2().unspecified());
+ EXPECT_EQ(0, network->getT2().get());
+
+ EXPECT_TRUE(network->getReservationsGlobal().unspecified());
+ EXPECT_FALSE(network->getReservationsGlobal().get());
+
+ EXPECT_TRUE(network->getReservationsInSubnet().unspecified());
+ EXPECT_TRUE(network->getReservationsInSubnet().get());
+
+ EXPECT_TRUE(network->getReservationsOutOfPool().unspecified());
+ EXPECT_FALSE(network->getReservationsOutOfPool().get());
+
+ EXPECT_TRUE(network->getCalculateTeeTimes().unspecified());
+ EXPECT_FALSE(network->getCalculateTeeTimes().get());
+
+ EXPECT_TRUE(network->getT1Percent().unspecified());
+ EXPECT_EQ(0.0, network->getT1Percent().get());
+
+ EXPECT_TRUE(network->getT2Percent().unspecified());
+ EXPECT_EQ(0.0, network->getT2Percent().get());
+
+ EXPECT_TRUE(network->getPreferred().unspecified());
+ EXPECT_EQ(0, network->getPreferred().get());
+
+ EXPECT_TRUE(network->getRapidCommit().unspecified());
+ EXPECT_FALSE(network->getRapidCommit().get());
+
+ EXPECT_TRUE(network->getDdnsSendUpdates().unspecified());
+ EXPECT_FALSE(network->getDdnsSendUpdates().get());
+
+ EXPECT_TRUE(network->getDdnsOverrideNoUpdate().unspecified());
+ EXPECT_FALSE(network->getDdnsOverrideNoUpdate().get());
+
+ EXPECT_TRUE(network->getDdnsOverrideClientUpdate().unspecified());
+ EXPECT_FALSE(network->getDdnsOverrideClientUpdate().get());
+
+ EXPECT_TRUE(network->getDdnsReplaceClientNameMode().unspecified());
+ EXPECT_EQ(D2ClientConfig::RCM_NEVER, network->getDdnsReplaceClientNameMode().get());
+
+ EXPECT_TRUE(network->getDdnsGeneratedPrefix().unspecified());
+ EXPECT_TRUE(network->getDdnsGeneratedPrefix().empty());
+
+ EXPECT_TRUE(network->getDdnsQualifyingSuffix().unspecified());
+ EXPECT_TRUE(network->getDdnsQualifyingSuffix().empty());
+
+ EXPECT_TRUE(network->getStoreExtendedInfo().unspecified());
+ EXPECT_FALSE(network->getStoreExtendedInfo().get());
+
+ EXPECT_TRUE(network->getDdnsUpdateOnRenew().unspecified());
+ EXPECT_FALSE(network->getDdnsUpdateOnRenew().get());
+
+ EXPECT_TRUE(network->getDdnsUseConflictResolution().unspecified());
+ EXPECT_FALSE(network->getDdnsUseConflictResolution().get());
+}
+
+// This test verifies a negative value for the subnet ID is rejected (v4).
+TEST_F(ParseConfigTest, negativeSubnetId4) {
+ std::string config =
+ "{"
+ " \"subnet4\": [ {"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"id\": -1"
+ " } ]"
+ "}";
+
+ ElementPtr json = Element::fromJSON(config);
+ EXPECT_TRUE(json);
+ ConstElementPtr status = parseElementSet(json, false);
+ int rcode = 0;
+ ConstElementPtr comment = parseAnswer(rcode, status);
+ ASSERT_TRUE(comment);
+ ASSERT_EQ(comment->getType(), Element::string);
+ EXPECT_EQ(1, rcode);
+ std::string expected = "Configuration parsing failed: ";
+ expected += "subnet configuration failed: ";
+ expected += "The 'id' value (-1) is not within expected range: ";
+ expected += "(0 - 4294967294)";
+ EXPECT_EQ(expected, comment->stringValue());
+}
+
+// This test verifies a negative value for the subnet ID is rejected (v6).
+TEST_F(ParseConfigTest, negativeSubnetId6) {
+ std::string config =
+ "{"
+ " \"subnet6\": [ {"
+ " \"subnet\": \"2001:db8:1::/64\","
+ " \"id\": -1"
+ " } ]"
+ "}";
+
+ ElementPtr json = Element::fromJSON(config);
+ EXPECT_TRUE(json);
+ ConstElementPtr status = parseElementSet(json, true);
+ int rcode = 0;
+ ConstElementPtr comment = parseAnswer(rcode, status);
+ ASSERT_TRUE(comment);
+ ASSERT_EQ(comment->getType(), Element::string);
+ EXPECT_EQ(1, rcode);
+ std::string expected = "Configuration parsing failed: ";
+ expected += "subnet configuration failed: ";
+ expected += "The 'id' value (-1) is not within expected range: ";
+ expected += "(0 - 4294967294)";
+ EXPECT_EQ(expected, comment->stringValue());
+}
+
+// This test verifies a too high value for the subnet ID is rejected (v4).
+TEST_F(ParseConfigTest, reservedSubnetId4) {
+ std::string config =
+ "{"
+ " \"subnet4\": [ {"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"id\": 4294967295"
+ " } ]"
+ "}";
+
+ ElementPtr json = Element::fromJSON(config);
+ EXPECT_TRUE(json);
+ ConstElementPtr status = parseElementSet(json, false);
+ int rcode = 0;
+ ConstElementPtr comment = parseAnswer(rcode, status);
+ ASSERT_TRUE(comment);
+ ASSERT_EQ(comment->getType(), Element::string);
+ EXPECT_EQ(1, rcode);
+ std::string expected = "Configuration parsing failed: ";
+ expected += "subnet configuration failed: ";
+ expected += "The 'id' value (4294967295) is not within expected range: ";
+ expected += "(0 - 4294967294)";
+ EXPECT_EQ(expected, comment->stringValue());
+}
+
+// This test verifies a too high value for the subnet ID is rejected (v6).
+TEST_F(ParseConfigTest, reservedSubnetId6) {
+ std::string config =
+ "{"
+ " \"subnet6\": [ {"
+ " \"subnet\": \"2001:db8:1::/64\","
+ " \"id\": 4294967295"
+ " } ]"
+ "}";
+
+ ElementPtr json = Element::fromJSON(config);
+ EXPECT_TRUE(json);
+ ConstElementPtr status = parseElementSet(json, true);
+ int rcode = 0;
+ ConstElementPtr comment = parseAnswer(rcode, status);
+ ASSERT_TRUE(comment);
+ ASSERT_EQ(comment->getType(), Element::string);
+ EXPECT_EQ(1, rcode);
+ std::string expected = "Configuration parsing failed: ";
+ expected += "subnet configuration failed: ";
+ expected += "The 'id' value (4294967295) is not within expected range: ";
+ expected += "(0 - 4294967294)";
+ EXPECT_EQ(expected, comment->stringValue());
+}
+
+// There's no test for ControlSocketParser, as it is tested in the DHCPv4 code
+// (see CtrlDhcpv4SrvTest.commandSocketBasic in
+// src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc).
+
+} // Anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/dhcp_queue_control_parser_unittest.cc b/src/lib/dhcpsrv/tests/dhcp_queue_control_parser_unittest.cc
new file mode 100644
index 0000000..e10dccd
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/dhcp_queue_control_parser_unittest.cc
@@ -0,0 +1,213 @@
+// Copyright (C) 2018-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <cc/data.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/parsers/dhcp_queue_control_parser.h>
+#include <testutils/multi_threading_utils.h>
+#include <testutils/test_to_element.h>
+#include <util/multi_threading_mgr.h>
+#include <gtest/gtest.h>
+
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::test;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Test fixture class for @c DHCPQueueControlParser
+class DHCPQueueControlParserTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ DHCPQueueControlParserTest() = default;
+
+ /// @brief Destructor
+ virtual ~DHCPQueueControlParserTest() = default;
+
+protected:
+ /// @brief Setup for each test.
+ ///
+ /// Clears the configuration in the @c CfgMgr.
+ virtual void SetUp();
+
+ /// @brief Cleans up after each test.
+ ///
+ /// Clears the configuration in the @c CfgMgr.
+ virtual void TearDown();
+};
+
+void
+DHCPQueueControlParserTest::SetUp() {
+ CfgMgr::instance().clear();
+}
+
+void
+DHCPQueueControlParserTest::TearDown() {
+ CfgMgr::instance().clear();
+}
+
+// Verifies that DHCPQueueControlParser handles
+// expected valid dhcp-queue-control content
+TEST_F(DHCPQueueControlParserTest, validContent) {
+ struct Scenario {
+ std::string description_;
+ std::string json_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "queue disabled",
+ "{ \n"
+ " \"enable-queue\": false \n"
+ "} \n"
+ },
+ {
+ "queue disabled, arbitrary content allowed",
+ "{ \n"
+ " \"enable-queue\": false, \n"
+ " \"foo\": \"bogus\", \n"
+ " \"random-int\" : 1234 \n"
+ "} \n"
+ },
+ {
+ "queue enabled, with queue-type",
+ "{ \n"
+ " \"enable-queue\": true, \n"
+ " \"queue-type\": \"some-type\" \n"
+ "} \n"
+ },
+ {
+ "queue enabled with queue-type and arbitrary content",
+ "{ \n"
+ " \"enable-queue\": true, \n"
+ " \"queue-type\": \"some-type\", \n"
+ " \"foo\": \"bogus\", \n"
+ " \"random-int\" : 1234 \n"
+ "} \n"
+ }
+ };
+
+ // Iterate over the valid scenarios and verify they succeed.
+ ConstElementPtr config_elems;
+ ConstElementPtr queue_control;
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+ {
+ // Construct the config JSON
+ ASSERT_NO_THROW(config_elems = Element::fromJSON(scenario.json_))
+ << "invalid JSON, test is broken";
+
+ // Parsing config into a queue control should succeed.
+ DHCPQueueControlParser parser;
+ try {
+ queue_control = parser.parse(config_elems);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "parser threw an exception: " << ex.what();
+ }
+
+ // Verify the resultant queue control.
+ ASSERT_TRUE(queue_control);
+
+ // The parser should have created a duplicate of the
+ // configuration elements.
+ ASSERT_TRUE(queue_control.get() != config_elems.get());
+ EXPECT_TRUE(queue_control->equals(*config_elems));
+ }
+ }
+}
+
+// Verifies that DHCPQueueControlParser correctly catches
+// invalid dhcp-queue-control content
+TEST_F(DHCPQueueControlParserTest, invalidContent) {
+ struct Scenario {
+ std::string description_;
+ std::string json_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "enable-queue missing",
+ "{ \n"
+ " \"enable-type\": \"some-type\" \n"
+ "} \n"
+ },
+ {
+ "enable-queue not boolean",
+ "{ \n"
+ " \"enable-queue\": \"always\" \n"
+ "} \n"
+ },
+ {
+ "queue enabled, type missing",
+ "{ \n"
+ " \"enable-queue\": true \n"
+ "} \n"
+ },
+ {
+ "queue enabled, type not a string",
+ "{ \n"
+ " \"enable-queue\": true, \n"
+ " \"queue-type\": 7777 \n"
+ "} \n"
+ }
+ };
+
+ // Iterate over the valid scenarios and verify they succeed.
+ ConstElementPtr config_elems;
+ ConstElementPtr queue_control;
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+ {
+ // Construct the config JSON
+ ASSERT_NO_THROW(config_elems = Element::fromJSON(scenario.json_))
+ << "invalid JSON, test is broken";
+
+ // Parsing config into a queue control should succeed.
+ DHCPQueueControlParser parser;
+ EXPECT_THROW(parser.parse(config_elems), DhcpConfigError);
+ }
+ }
+}
+
+// Verifies that DHCPQueueControlParser disables the queue when multi-threading
+// is enabled
+TEST_F(DHCPQueueControlParserTest, multiThreading) {
+ // Enable config with some queue type.
+ std::string config =
+ "{ \n"
+ " \"enable-queue\": true, \n"
+ " \"queue-type\": \"some-type\" \n"
+ "} \n";
+
+ // Construct the config JSON.
+ ConstElementPtr config_elems;
+ ASSERT_NO_THROW(config_elems = Element::fromJSON(config))
+ << "invalid JSON, test is broken";
+
+ // Parse config.
+ DHCPQueueControlParser parser;
+ ConstElementPtr queue_control;
+ ASSERT_FALSE(MultiThreadingMgr::instance().getMode());
+ ASSERT_NO_THROW(queue_control = parser.parse(config_elems))
+ << "parse fails, test is broken";
+ // Verify that queue is enabled.
+ ASSERT_TRUE(queue_control);
+ ASSERT_TRUE(queue_control->get("enable-queue"));
+ EXPECT_EQ("true", queue_control->get("enable-queue")->str());
+
+ // Retry with multi-threading.
+ MultiThreadingTest mt(true);
+ ASSERT_TRUE(MultiThreadingMgr::instance().getMode());
+ ASSERT_NO_THROW(queue_control = parser.parse(config_elems));
+ ASSERT_TRUE(queue_control);
+ ASSERT_TRUE(queue_control->get("enable-queue"));
+ EXPECT_EQ("false", queue_control->get("enable-queue")->str());
+}
+
+}; // anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/duid_config_parser_unittest.cc b/src/lib/dhcpsrv/tests/duid_config_parser_unittest.cc
new file mode 100644
index 0000000..be7e96b
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/duid_config_parser_unittest.cc
@@ -0,0 +1,224 @@
+// Copyright (C) 2015-2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <cc/data.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/cfg_duid.h>
+#include <dhcpsrv/parsers/duid_config_parser.h>
+#include <dhcpsrv/parsers/dhcp_parsers.h>
+#include <dhcpsrv/testutils/config_result_check.h>
+#include <testutils/test_to_element.h>
+#include <util/encode/hex.h>
+#include <gtest/gtest.h>
+#include <limits>
+#include <sstream>
+#include <string>
+
+using namespace isc;
+using namespace isc::data;
+using namespace isc::dhcp;
+
+namespace {
+
+/// @brief Test fixture class for @c DUIDConfigParser
+class DUIDConfigParserTest : public ::testing::Test {
+public:
+
+ /// @brief constructor
+ ///
+ /// Initializes cfg_duid_ to a new empty object
+ DUIDConfigParserTest()
+ :cfg_duid_(new CfgDUID()){
+ }
+
+ /// @brief Creates simple configuration with DUID type only.
+ ///
+ /// @param duid_type DUID type in the textual format.
+ std::string createConfigWithType(const std::string& duid_type) const;
+
+ /// @brief Creates simple configuration with DUID type and one
+ /// numeric parameter.
+ ///
+ /// @param name Parameter name.
+ /// @param value Parameter value.
+ std::string createConfigWithInteger(const std::string& name,
+ const int64_t value) const;
+
+ /// @brief Parse configuration.
+ ///
+ /// @param config String representing DUID configuration.
+ void build(const std::string& config) const;
+
+ /// @brief Test that only a DUID type can be specified.
+ ///
+ /// @param duid_type DUID type in numeric format.
+ /// @param duid_type_text DUID type in textual format.
+ void testTypeOnly(const DUID::DUIDType& duid_type,
+ const std::string& duid_type_text) const;
+
+ /// @brief Test that invalid configuration is rejected.
+ ///
+ /// @param config Holds JSON configuration to be used.
+ void testInvalidConfig(const std::string& config) const;
+
+ /// @brief Test out of range numeric values.
+ ///
+ /// @param param_name Parameter name.
+ /// @tparam Type of the numeric parameter.
+ template<typename NumericType>
+ void testOutOfRange(const std::string& param_name) {
+ // Obtain maximum value for the specified numeric type.
+ const uint64_t max_value = std::numeric_limits<NumericType>::max();
+
+ // Negative values are not allowed.
+ EXPECT_THROW(build(createConfigWithInteger(param_name, -1)),
+ DhcpConfigError);
+ // Zero is allowed.
+ EXPECT_NO_THROW(build(createConfigWithInteger(param_name, 0)));
+ // Maximum value.
+ EXPECT_NO_THROW(build(createConfigWithInteger(param_name, max_value)));
+ // Value greater than maximum should result in exception.
+ EXPECT_THROW(build(createConfigWithInteger(param_name, max_value + 1)),
+ DhcpConfigError);
+ }
+
+ /// @brief Converts vector to string of hexadecimal digits.
+ ///
+ /// @param vec Input vector.
+ /// @return String of hexadecimal digits converted from vector.
+ std::string toString(const std::vector<uint8_t>& vec) const;
+
+ /// Config DUID pointer
+ CfgDUIDPtr cfg_duid_;
+};
+
+std::string
+DUIDConfigParserTest::createConfigWithType(const std::string& duid_type) const {
+ std::ostringstream s;
+ s << "{ \"type\": \"" << duid_type << "\" }";
+ return (s.str());
+}
+
+std::string
+DUIDConfigParserTest::createConfigWithInteger(const std::string& name,
+ const int64_t value) const {
+ std::ostringstream s;
+ s << "{ \"type\": \"LLT\", \"" << name << "\": " << value << " }";
+ return (s.str());
+}
+
+void
+DUIDConfigParserTest::build(const std::string& config) const {
+ ElementPtr config_element = Element::fromJSON(config);
+ DUIDConfigParser parser;
+ parser.parse(cfg_duid_, config_element);
+}
+
+void
+DUIDConfigParserTest::testTypeOnly(const DUID::DUIDType& duid_type,
+ const std::string& duid_type_text) const {
+ // Use DUID configuration with only a "type".
+ ASSERT_NO_THROW(build(createConfigWithType(duid_type_text)));
+
+ // Make sure that the type is correct and that other parameters are set
+ // to their defaults.
+ ASSERT_TRUE(cfg_duid_);
+ EXPECT_EQ(duid_type, cfg_duid_->getType());
+ EXPECT_TRUE(cfg_duid_->getIdentifier().empty());
+ EXPECT_EQ(0, cfg_duid_->getHType());
+ EXPECT_EQ(0, cfg_duid_->getTime());
+ EXPECT_EQ(0, cfg_duid_->getEnterpriseId());
+}
+
+void
+DUIDConfigParserTest::testInvalidConfig(const std::string& config) const {
+ EXPECT_THROW(build(config), DhcpConfigError);
+}
+
+std::string
+DUIDConfigParserTest::toString(const std::vector<uint8_t>& vec) const {
+ try {
+ return (util::encode::encodeHex(vec));
+ } catch (...) {
+ ADD_FAILURE() << "toString: unable to encode vector to"
+ " hexadecimal string";
+ }
+ return ("");
+}
+
+// This test verifies that it is allowed to specify a DUID-LLT type.
+TEST_F(DUIDConfigParserTest, typeOnlyLLT) {
+ testTypeOnly(DUID::DUID_LLT, "LLT");
+}
+
+// This test verifies that it is allowed to specify a DUID-EN type.
+TEST_F(DUIDConfigParserTest, typeOnlyEN) {
+ testTypeOnly(DUID::DUID_EN, "EN");
+}
+
+// This test verifies that it is allowed to specify a DUID-LL type.
+TEST_F(DUIDConfigParserTest, typeOnlyLL) {
+ testTypeOnly(DUID::DUID_LL, "LL");
+}
+
+// This test verifies that using unsupported DUID type will result in
+// configuration error.
+TEST_F(DUIDConfigParserTest, typeInvalid) {
+ testInvalidConfig(createConfigWithType("WRONG"));
+}
+
+// This test verifies that DUID type is required.
+TEST_F(DUIDConfigParserTest, noType) {
+ // First check that the configuration with DUID type specified is
+ // accepted.
+ ASSERT_NO_THROW(build("{ \"type\": \"LLT\", \"time\": 1 }"));
+ // Now remove the type and expect an error.
+ testInvalidConfig("{ \"time\": 1 }");
+}
+
+// This test verifies that all parameters can be set.
+TEST_F(DUIDConfigParserTest, allParameters) {
+ // Set all parameters.
+ std::string config = "{"
+ " \"type\": \"EN\","
+ " \"identifier\": \"ABCDEF\","
+ " \"time\": 100,"
+ " \"htype\": 8,"
+ " \"enterprise-id\": 2024,"
+ " \"persist\": false"
+ "}";
+ ASSERT_NO_THROW(build(config));
+
+ // Verify that parameters have been set correctly.
+ ASSERT_TRUE(cfg_duid_);
+ EXPECT_EQ(DUID::DUID_EN, cfg_duid_->getType());
+ EXPECT_EQ("ABCDEF", toString(cfg_duid_->getIdentifier()));
+ EXPECT_EQ(8, cfg_duid_->getHType());
+ EXPECT_EQ(100, cfg_duid_->getTime());
+ EXPECT_EQ(2024, cfg_duid_->getEnterpriseId());
+ EXPECT_FALSE(cfg_duid_->persist());
+
+ // Check the config can be got back.
+ isc::test::runToElementTest<CfgDUID>(config, *cfg_duid_);
+}
+
+// Test out of range values for time.
+TEST_F(DUIDConfigParserTest, timeOutOfRange) {
+ testOutOfRange<uint32_t>("time");
+}
+
+// Test out of range values for hardware type.
+TEST_F(DUIDConfigParserTest, htypeOutOfRange) {
+ testOutOfRange<uint16_t>("htype");
+}
+
+// Test out of range values for enterprise id.
+TEST_F(DUIDConfigParserTest, enterpriseIdOutOfRange) {
+ testOutOfRange<uint32_t>("enterprise-id");
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/expiration_config_parser_unittest.cc b/src/lib/dhcpsrv/tests/expiration_config_parser_unittest.cc
new file mode 100644
index 0000000..fd76507
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/expiration_config_parser_unittest.cc
@@ -0,0 +1,252 @@
+// Copyright (C) 2015,2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <cc/data.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/cfg_expiration.h>
+#include <dhcpsrv/parsers/expiration_config_parser.h>
+#include <dhcpsrv/parsers/dhcp_parsers.h>
+#include <gtest/gtest.h>
+#include <sstream>
+#include <stdint.h>
+#include <string>
+
+using namespace isc::data;
+using namespace isc::dhcp;
+
+namespace {
+
+/// @brief Test fixture class for @c ExpirationConfigParser.
+class ExpirationConfigParserTest : public ::testing::Test {
+protected:
+
+ /// @brief Setup for each test.
+ ///
+ /// Clears the configuration in the @c CfgMgr.
+ virtual void SetUp();
+
+ /// @brief Cleans up after each test.
+ ///
+ /// Clears the configuration in the @c CfgMgr.
+ virtual void TearDown();
+
+ /// @brief Include a specified parameter in the configuration.
+ ///
+ /// If the specified parameter already exists, its value is replaced.
+ ///
+ /// @param param_name Parameter name.
+ /// @param value Parameter value.
+ void addParam(const std::string& param_name, const int64_t value);
+
+ /// @brief Creates configuration and parses it with the parser under test.
+ ///
+ /// This method creates the JSON configuration form the parameters
+ /// specified using the @c ExpirationConfigParserTest::addParam method.
+ /// It then uses the parser to parse this configuration. Any exceptions
+ /// emitted by the parser are propagated to the caller (they aren't
+ /// caught by this method).
+ ///
+ /// @return Pointer to the parsed configuration.
+ CfgExpirationPtr renderConfig() const;
+
+ /// @brief Tests that the out of range parameter value is not accepted.
+ ///
+ /// This test checks that the negative value and the value which is
+ /// greater than the maximum for the given parameter is not accepted.
+ ///
+ /// @param param Parameter name.
+ /// @param max_value Maximum value allowed for the parameter.
+ void testOutOfRange(const std::string& param, const uint64_t max_value);
+
+private:
+
+ /// @brief Holds configuration parameters specified for a test.
+ std::map<std::string, int64_t> config_params_;
+
+};
+
+void
+ExpirationConfigParserTest::SetUp() {
+ CfgMgr::instance().clear();
+}
+
+void
+ExpirationConfigParserTest::TearDown() {
+ CfgMgr::instance().clear();
+}
+
+void
+ExpirationConfigParserTest::addParam(const std::string& param_name,
+ const int64_t value) {
+ config_params_[param_name] = value;
+}
+
+CfgExpirationPtr
+ExpirationConfigParserTest::renderConfig() const {
+ std::ostringstream s;
+ // Create JSON configuration from the parameters in the map.
+ s << "{";
+ for (std::map<std::string, int64_t>::const_iterator param =
+ config_params_.begin(); param != config_params_.end();
+ ++param) {
+ // Include comma sign if we're at the subsequent parameter.
+ if (std::distance(config_params_.begin(), param) > 0) {
+ s << ",";
+ }
+ s << "\"" << param->first << "\": " << param->second;
+ }
+ s << "}";
+
+ ElementPtr config_element = Element::fromJSON(s.str());
+
+ // Parse the configuration. This may emit exceptions.
+ ExpirationConfigParser parser;
+ parser.parse(config_element);
+
+ // No exception so return configuration.
+ return (CfgMgr::instance().getStagingCfg()->getCfgExpiration());
+}
+
+void
+ExpirationConfigParserTest::testOutOfRange(const std::string& param,
+ const uint64_t max_value) {
+ // Remove any existing parameters which would influence the
+ // behavior of the test.
+ config_params_.clear();
+
+ // Negative value is not allowed.
+ addParam(param, -3);
+ EXPECT_THROW(renderConfig(), DhcpConfigError)
+ << "test for negative value of '" << param << "' failed";
+
+ // Value greater than maximum is not allowed.
+ addParam(param, max_value + 1);
+ EXPECT_THROW(renderConfig(), DhcpConfigError)
+ << "test for out of range value of '" << param << "' failed";
+
+ // Value in range should be accepted.
+ addParam(param, max_value);
+ EXPECT_NO_THROW(renderConfig())
+ << "test for in range value of '" << param << "' failed";
+
+ // Value of 0 should be accepted.
+ addParam(param, 0);
+ EXPECT_NO_THROW(renderConfig())
+ << "test for zero value of '" << param << "' failed";
+}
+
+
+// This test verifies that all parameters for the expiration may be configured.
+TEST_F(ExpirationConfigParserTest, allParameters) {
+ // Create configuration which overrides default values of all parameters.
+ addParam("reclaim-timer-wait-time", 20);
+ addParam("flush-reclaimed-timer-wait-time", 35);
+ addParam("hold-reclaimed-time", 1800);
+ addParam("max-reclaim-leases", 50);
+ addParam("max-reclaim-time", 100);
+ addParam("unwarned-reclaim-cycles", 10);
+
+ CfgExpirationPtr cfg;
+ ASSERT_NO_THROW(cfg = renderConfig());
+ EXPECT_EQ(20, cfg->getReclaimTimerWaitTime());
+ EXPECT_EQ(35, cfg->getFlushReclaimedTimerWaitTime());
+ EXPECT_EQ(1800, cfg->getHoldReclaimedTime());
+ EXPECT_EQ(50, cfg->getMaxReclaimLeases());
+ EXPECT_EQ(100, cfg->getMaxReclaimTime());
+ EXPECT_EQ(10, cfg->getUnwarnedReclaimCycles());
+}
+
+// This test verifies that default values are used if no parameter is
+// specified.
+TEST_F(ExpirationConfigParserTest, noParameters) {
+ CfgExpirationPtr cfg;
+ ASSERT_NO_THROW(cfg = renderConfig());
+ EXPECT_EQ(CfgExpiration::DEFAULT_RECLAIM_TIMER_WAIT_TIME,
+ cfg->getReclaimTimerWaitTime());
+ EXPECT_EQ(CfgExpiration::DEFAULT_FLUSH_RECLAIMED_TIMER_WAIT_TIME,
+ cfg->getFlushReclaimedTimerWaitTime());
+ EXPECT_EQ(CfgExpiration::DEFAULT_HOLD_RECLAIMED_TIME,
+ cfg->getHoldReclaimedTime());
+ EXPECT_EQ(CfgExpiration::DEFAULT_MAX_RECLAIM_LEASES,
+ cfg->getMaxReclaimLeases());
+ EXPECT_EQ(CfgExpiration::DEFAULT_MAX_RECLAIM_TIME,
+ cfg->getMaxReclaimTime());
+ EXPECT_EQ(CfgExpiration::DEFAULT_UNWARNED_RECLAIM_CYCLES,
+ cfg->getUnwarnedReclaimCycles());
+}
+
+// This test verifies that a subset of parameters may be specified and
+// that default values are used for those that aren't specified.
+TEST_F(ExpirationConfigParserTest, someParameters) {
+ addParam("reclaim-timer-wait-time", 15);
+ addParam("hold-reclaimed-time", 2000);
+ addParam("max-reclaim-time", 200);
+
+ CfgExpirationPtr cfg;
+ ASSERT_NO_THROW(cfg = renderConfig());
+ EXPECT_EQ(15, cfg->getReclaimTimerWaitTime());
+ EXPECT_EQ(CfgExpiration::DEFAULT_FLUSH_RECLAIMED_TIMER_WAIT_TIME,
+ cfg->getFlushReclaimedTimerWaitTime());
+ EXPECT_EQ(2000, cfg->getHoldReclaimedTime());
+ EXPECT_EQ(CfgExpiration::DEFAULT_MAX_RECLAIM_LEASES,
+ cfg->getMaxReclaimLeases());
+ EXPECT_EQ(200, cfg->getMaxReclaimTime());
+ EXPECT_EQ(CfgExpiration::DEFAULT_UNWARNED_RECLAIM_CYCLES,
+ cfg->getUnwarnedReclaimCycles());
+}
+
+// This test verifies that another subset of parameters may be specified
+// and that default values are used for those that aren't specified.
+TEST_F(ExpirationConfigParserTest, otherParameters) {
+ addParam("flush-reclaimed-timer-wait-time", 50);
+ addParam("max-reclaim-leases", 60);
+ addParam("unwarned-reclaim-cycles", 20);
+
+ CfgExpirationPtr cfg;
+ ASSERT_NO_THROW(cfg = renderConfig());
+
+ EXPECT_EQ(CfgExpiration::DEFAULT_RECLAIM_TIMER_WAIT_TIME,
+ cfg->getReclaimTimerWaitTime());
+ EXPECT_EQ(50, cfg->getFlushReclaimedTimerWaitTime());
+ EXPECT_EQ(CfgExpiration::DEFAULT_HOLD_RECLAIMED_TIME,
+ cfg->getHoldReclaimedTime());
+ EXPECT_EQ(60, cfg->getMaxReclaimLeases());
+ EXPECT_EQ(CfgExpiration::DEFAULT_MAX_RECLAIM_TIME,
+ cfg->getMaxReclaimTime());
+ EXPECT_EQ(20, cfg->getUnwarnedReclaimCycles());
+}
+
+// This test verifies that negative parameter values are not allowed.
+TEST_F(ExpirationConfigParserTest, outOfRangeValues) {
+ testOutOfRange("reclaim-timer-wait-time",
+ CfgExpiration::LIMIT_RECLAIM_TIMER_WAIT_TIME);
+ testOutOfRange("flush-reclaimed-timer-wait-time",
+ CfgExpiration::LIMIT_FLUSH_RECLAIMED_TIMER_WAIT_TIME);
+ testOutOfRange("hold-reclaimed-time",
+ CfgExpiration::LIMIT_HOLD_RECLAIMED_TIME);
+ testOutOfRange("max-reclaim-leases",
+ CfgExpiration::LIMIT_MAX_RECLAIM_LEASES);
+ testOutOfRange("max-reclaim-time",
+ CfgExpiration::LIMIT_MAX_RECLAIM_TIME);
+ testOutOfRange("unwarned-reclaim-cycles",
+ CfgExpiration::LIMIT_UNWARNED_RECLAIM_CYCLES);
+}
+
+// This test verifies that it is not allowed to specify a value as
+// a text.
+TEST_F(ExpirationConfigParserTest, notNumberValue) {
+ // The value should not be in quotes.
+ std::string config = "{ \"reclaim-timer-wait-time\": \"10\" }";
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse the configuration. It should throw exception.
+ ExpirationConfigParser parser;
+ EXPECT_THROW(parser.parse(config_element), DhcpConfigError);
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/free_lease_queue_unittest.cc b/src/lib/dhcpsrv/tests/free_lease_queue_unittest.cc
new file mode 100644
index 0000000..21a4b50
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/free_lease_queue_unittest.cc
@@ -0,0 +1,613 @@
+// Copyright (C) 2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcpsrv/free_lease_queue.h>
+
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+
+namespace {
+
+// This test verifies that it is not allowed to add a range that overlaps with
+// any existing range.
+TEST(FreeLeaseQueueTest, addRangeOverlapping) {
+ FreeLeaseQueue lq;
+ // Add the initial range. This should succeed.
+ ASSERT_NO_THROW(lq.addRange(IOAddress("192.0.2.10"), IOAddress("192.0.3.100")));
+
+ // Let's assume the following naming convention:
+ // - r1s - start of the first range added.
+ // - r1e - end of the first range added.
+ // - r2s - start of the second range (will be added later in this test).
+ // - r2e - end of the second range (will be added later in this test).
+ // - ns - start of the new range colliding with existing ones.
+ // - ne - end of the new range colliding with existing ones.
+ // - #### overlap
+ // - ns/ne/r1s - overlap on the edges of the ranges (single address shared).
+
+ // r1s ns####r2s ne
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.50"), IOAddress("192.0.3.199")),
+ BadValue);
+
+ // ns r1s###ne r1s
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.5"), IOAddress("192.0.2.100")),
+ BadValue);
+
+ // r1s ns####ne r2s
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.50"), IOAddress("192.0.3.50")),
+ BadValue);
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.5"), IOAddress("192.0.3.200")),
+ BadValue);
+
+ // ns ne/r1s r1e
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.5"), IOAddress("192.0.2.10")),
+ BadValue);
+
+ // r1s r1e/ns ne
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.3.100"), IOAddress("192.0.3.105")),
+ BadValue);
+
+ // ns/ne/r1s r1e
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.10"), IOAddress("192.0.2.10")),
+ BadValue);
+
+ // r1s r1e/ns/ne
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.3.100"), IOAddress("192.0.3.100")),
+ BadValue);
+
+ // Add another range, marked as r2s, r2e.
+ ASSERT_NO_THROW(lq.addRange(IOAddress("192.0.4.10"), IOAddress("192.0.5.100")));
+
+ // r1s ns####r1e ne r2s r2e
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.50"), IOAddress("192.0.3.199")),
+ BadValue);
+
+ // r1s r1e r2s ns####r2e ne
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.4.50"), IOAddress("192.0.5.199")),
+ BadValue);
+
+ // r1s ns####r2s####r2e ne
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.50"), IOAddress("192.0.5.199")),
+ BadValue);
+
+ // ns r1s####ne r1e r2s r2e
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.5"), IOAddress("192.0.2.100")),
+ BadValue);
+
+ // r1s r1e ns r2s####ne r2e
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.4.5"), IOAddress("192.0.4.100")),
+ BadValue);
+
+ // ns r1s####r1e r2s####ne r2e
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.5"), IOAddress("192.0.4.100")),
+ BadValue);
+
+ // r1s ns####ne r1e r2s r2e
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.50"), IOAddress("192.0.3.50")),
+ BadValue);
+
+ // r1s r1e r2s ns####ne r2e
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.4.50"), IOAddress("192.0.5.50")),
+ BadValue);
+
+ // r1s ns####r1e r2s####ne r2e
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.50"), IOAddress("192.0.5.50")),
+ BadValue);
+
+ // ns r1s r1e ne r2s r2e
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.5"), IOAddress("192.0.3.200")),
+ BadValue);
+
+ // r1s r1e ns r2s####r2e ne
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.4.5"), IOAddress("192.0.5.200")),
+ BadValue);
+
+ // ns r1s####r1e r2s####r2e ne
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.5"), IOAddress("192.0.5.200")),
+ BadValue);
+
+ // ns ne/r1s r1e r2s r2e
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.2.5"), IOAddress("192.0.2.10")),
+ BadValue);
+ // r1s r1e/ns ne r2s r2e
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.3.100"), IOAddress("192.0.3.105")),
+ BadValue);
+ // r1s r1e ns ne/r2s r2e
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.4.5"), IOAddress("192.0.4.10")),
+ BadValue);
+ // r1s r1e r2s r2e/ns ne
+ EXPECT_THROW(lq.addRange(IOAddress("192.0.5.100"), IOAddress("192.0.5.105")),
+ BadValue);
+}
+
+// This test verifies that it is not allowed to add a prefix range that overlaps with
+// any existing range.
+TEST(FreeLeaseQueueTest, addPrefixRangeOverlapping) {
+ FreeLeaseQueue lq;
+ // Add the initial range. This should succeed.
+ ASSERT_NO_THROW(lq.addRange(IOAddress("2001:db8:1::"), 64, 96));
+
+ EXPECT_THROW(lq.addRange(IOAddress("2001:db8:1:0:0:5:ffff:0"), 96, 120),
+ BadValue);
+ EXPECT_THROW(lq.addRange(IOAddress("2001:db8:1::0"), 80, 88),
+ BadValue);
+}
+
+// This test verifies that a range can be removed from the container.
+TEST(FreeLeaseQueueTest, removeRange) {
+ FreeLeaseQueue lq;
+
+ // Add two ranges.
+ AddressRange range1(IOAddress("192.0.2.99"), IOAddress("192.0.2.100"));
+ AddressRange range2(IOAddress("192.0.3.99"), IOAddress("192.0.3.100"));
+ ASSERT_NO_THROW(lq.addRange(range1));
+ ASSERT_NO_THROW(lq.addRange(range2));
+
+ bool removed = false;
+
+ // Expect true to be returned when the range is removed.
+ ASSERT_NO_THROW(removed = lq.removeRange(range1));
+ EXPECT_TRUE(removed);
+
+ // An attempt to append an address to the removed range should not succeed.
+ EXPECT_FALSE(lq.append(IOAddress("192.0.2.99")));
+
+ // Removing it the second time should return false to indicate that the range
+ // was no longer there.
+ ASSERT_NO_THROW(removed = lq.removeRange(range1));
+ EXPECT_FALSE(removed);
+
+ // Same for the second range.
+ ASSERT_NO_THROW(removed = lq.removeRange(range2));
+ EXPECT_TRUE(removed);
+
+ ASSERT_NO_THROW(removed = lq.removeRange(range2));
+ EXPECT_FALSE(removed);
+}
+
+// This test verifies that a prefix range can be removed from the container.
+TEST(FreeLeaseQueueTest, removePrefixRange) {
+ FreeLeaseQueue lq;
+
+ // Add two ranges.
+ PrefixRange range1(IOAddress("3000::"), 64, 96);
+ PrefixRange range2(IOAddress("3001::"), 64, 96);
+ ASSERT_NO_THROW(lq.addRange(range1));
+ ASSERT_NO_THROW(lq.addRange(range2));
+
+ bool removed = false;
+
+ // Expect true to be returned when the range is removed.
+ ASSERT_NO_THROW(removed = lq.removeRange(range1));
+ EXPECT_TRUE(removed);
+
+ // An attempt to append a prefix to the removed range should not succeed.
+ EXPECT_FALSE(lq.append(IOAddress("3000::5:0:0"), 96));
+
+ // Removing it the second time should return false to indicate that the range
+ // was no longer there.
+ ASSERT_NO_THROW(removed = lq.removeRange(range1));
+ EXPECT_FALSE(removed);
+
+ // Same for the second range.
+ ASSERT_NO_THROW(removed = lq.removeRange(range2));
+ EXPECT_TRUE(removed);
+
+ ASSERT_NO_THROW(removed = lq.removeRange(range2));
+ EXPECT_FALSE(removed);
+}
+
+// This test verifies that an attempt to use an address from outside the
+// given range throws and that an attempt to use non-existing in-range
+// address returns false.
+TEST(FreeLeaseQueueTest, useInvalidAddress) {
+ AddressRange range(IOAddress("192.0.2.99"), IOAddress("192.0.2.100"));
+
+ FreeLeaseQueue lq;
+ ASSERT_NO_THROW(lq.addRange(range));
+
+ // Using out of range address.
+ EXPECT_THROW(lq.use(range, IOAddress("192.0.2.1")), BadValue);
+
+ // Using in-range address but not existing in the container.
+ bool used = true;
+ ASSERT_NO_THROW(used = lq.use(range, IOAddress("192.0.2.99")));
+ EXPECT_FALSE(used);
+}
+
+// This test verifies that an attempt to use a prefix from outside the
+// given range throws and that an attempt to use non-existing in-range
+// address returns false.
+TEST(FreeLeaseQueueTest, useInvalidPrefix) {
+ PrefixRange range(IOAddress("2001:db8:1::"), 64, 96);
+
+ FreeLeaseQueue lq;
+ ASSERT_NO_THROW(lq.addRange(range));
+
+ // Using out of range prefix.
+ EXPECT_THROW(lq.use(range, IOAddress("2001:db8:2::")), BadValue);
+
+ // Using in-range prefix but not existing in the container.
+ bool used = false;
+ ASSERT_NO_THROW(used = lq.use(range, IOAddress("2001:db8:1::5:0:0")));
+ EXPECT_FALSE(used);
+}
+
+// Check that duplicates are eliminated when appending free address to
+// the range queue.
+TEST(FreeLeaseQueueTest, appendDuplicates) {
+ FreeLeaseQueue lq;
+
+ AddressRange range(IOAddress("192.0.2.1"), IOAddress("192.0.2.255"));
+ ASSERT_NO_THROW(lq.addRange(range));
+
+ ASSERT_NO_THROW(lq.append(range, IOAddress("192.0.2.10")));
+ // Append the duplicate of the first address.
+ ASSERT_NO_THROW(lq.append(range, IOAddress("192.0.2.10")));
+ ASSERT_NO_THROW(lq.append(range, IOAddress("192.0.2.5")));
+
+ IOAddress next(0);
+ // The first address should be returned first.
+ ASSERT_NO_THROW(next = lq.next(range));
+ EXPECT_EQ("192.0.2.10", next.toText());
+
+ // The duplicate should not be returned. Instead, the second address should
+ // be returned.
+ ASSERT_NO_THROW(next = lq.next(range));
+ EXPECT_EQ("192.0.2.5", next.toText());
+}
+
+// This test verifies that it is possible to pick next address from the given
+// range.
+TEST(FreeLeaseQueueTest, next) {
+ FreeLeaseQueue lq;
+
+ // Let's create two distinct address ranges.
+ AddressRange range1(IOAddress("192.0.2.1"), IOAddress("192.0.2.255"));
+ AddressRange range2(IOAddress("192.0.3.1"), IOAddress("192.0.3.255"));
+ ASSERT_NO_THROW(lq.addRange(range1));
+ ASSERT_NO_THROW(lq.addRange(range2));
+
+ // Append some IP addresses to those address ranges.
+ ASSERT_NO_THROW(lq.append(AddressRange(range1), IOAddress("192.0.2.10")));
+ ASSERT_NO_THROW(lq.append(AddressRange(range1), IOAddress("192.0.2.5")));
+ ASSERT_NO_THROW(lq.append(AddressRange(range2), IOAddress("192.0.3.23")));
+ ASSERT_NO_THROW(lq.append(AddressRange(range2), IOAddress("192.0.3.46")));
+
+ // Get the first address from the first range.
+ IOAddress next(0);
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("192.0.2.10", next.toText());
+
+ // Get the next one.
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("192.0.2.5", next.toText());
+
+ // After iterating over all addresses we should get the first one again.
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("192.0.2.10", next.toText());
+
+ // Repeat that test for the second address range.
+ ASSERT_NO_THROW(next = lq.next(range2));
+ EXPECT_EQ("192.0.3.23", next.toText());
+
+ ASSERT_NO_THROW(next = lq.next(range2));
+ EXPECT_EQ("192.0.3.46", next.toText());
+
+ ASSERT_NO_THROW(next = lq.next(range2));
+ EXPECT_EQ("192.0.3.23", next.toText());
+
+ // Use (remove) the address from the first range.
+ bool used = false;
+ ASSERT_NO_THROW(used = lq.use(range1, IOAddress("192.0.2.5")));
+ EXPECT_TRUE(used);
+
+ // We should now be getting the sole address from that range.
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("192.0.2.10", next.toText());
+
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("192.0.2.10", next.toText());
+
+ // Check the same for the second range.
+ ASSERT_NO_THROW(lq.use(range2, IOAddress("192.0.3.46")));
+
+ ASSERT_NO_THROW(next = lq.next(range2));
+ EXPECT_EQ("192.0.3.23", next.toText());
+}
+
+// This test verifies that it is possible to pick next prefix from the given
+// range.
+TEST(FreeLeaseQueueTest, nextPrefix) {
+ FreeLeaseQueue lq;
+
+ PrefixRange range1(IOAddress("2001:db8:1::"), 64, 96);
+ ASSERT_NO_THROW(lq.addRange(range1));
+
+ ASSERT_NO_THROW(lq.append(range1, IOAddress("2001:db8:1::4:0")));
+ ASSERT_NO_THROW(lq.append(range1, IOAddress("2001:db8:1::7:0")));
+ ASSERT_NO_THROW(lq.append(range1, IOAddress("2001:db8:1::3:0")));
+ ASSERT_NO_THROW(lq.append(range1, IOAddress("2001:db8:1::")));
+
+ IOAddress next = IOAddress::IPV6_ZERO_ADDRESS();
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("2001:db8:1::4:0", next.toText());
+
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("2001:db8:1::7:0", next.toText());
+
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("2001:db8:1::3:0", next.toText());
+
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("2001:db8:1::", next.toText());
+
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("2001:db8:1::4:0", next.toText());
+
+ // Use (remove) the prefix from the range.
+ bool used = false;
+ ASSERT_NO_THROW(used = lq.use(range1, IOAddress("2001:db8:1::7:0")));
+ EXPECT_TRUE(used);
+
+ // After we have removed the second prefix, the third prefix should be
+ // returned.
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("2001:db8:1::3:0", next.toText());
+}
+
+// This test verifies that it is possible to pop next address from the given
+// range.
+TEST(FreeLeaseQueueTest, pop) {
+ FreeLeaseQueue lq;
+
+ // Let's create two distinct address ranges.
+ AddressRange range1(IOAddress("192.0.2.1"), IOAddress("192.0.2.255"));
+ AddressRange range2(IOAddress("192.0.3.1"), IOAddress("192.0.3.255"));
+ ASSERT_NO_THROW(lq.addRange(range1));
+ ASSERT_NO_THROW(lq.addRange(range2));
+
+ // Append some IP addresses to those address ranges.
+ ASSERT_NO_THROW(lq.append(AddressRange(range1), IOAddress("192.0.2.10")));
+ ASSERT_NO_THROW(lq.append(AddressRange(range1), IOAddress("192.0.2.5")));
+ ASSERT_NO_THROW(lq.append(AddressRange(range2), IOAddress("192.0.3.23")));
+ ASSERT_NO_THROW(lq.append(AddressRange(range2), IOAddress("192.0.3.46")));
+
+ // Pop first from first range.
+ IOAddress next(0);
+ ASSERT_NO_THROW(next = lq.pop(range1));
+ EXPECT_EQ("192.0.2.10", next.toText());
+
+ // Pop second from the first range.
+ ASSERT_NO_THROW(next = lq.pop(range1));
+ EXPECT_EQ("192.0.2.5", next.toText());
+
+ // After iterating over all addresses we should get empty queue.
+ ASSERT_NO_THROW(next = lq.pop(range1));
+ EXPECT_TRUE(next.isV4Zero());
+
+ // Repeat that test for the second address range.
+ ASSERT_NO_THROW(next = lq.pop(range2));
+ EXPECT_EQ("192.0.3.23", next.toText());
+
+ ASSERT_NO_THROW(next = lq.pop(range2));
+ EXPECT_EQ("192.0.3.46", next.toText());
+
+ ASSERT_NO_THROW(next = lq.pop(range2));
+ EXPECT_TRUE(next.isV4Zero());
+}
+
+// This test verifies that it is possible to pop next prefix from the given
+// range.
+TEST(FreeLeaseQueueTest, popPrefix) {
+ FreeLeaseQueue lq;
+
+ // Add the range.
+ PrefixRange range1(IOAddress("2001:db8:1::"), 64, 96);
+ ASSERT_NO_THROW(lq.addRange(range1));
+
+ // Append several prefixes to that range.
+ ASSERT_NO_THROW(lq.append(range1, IOAddress("2001:db8:1::4:0")));
+ ASSERT_NO_THROW(lq.append(range1, IOAddress("2001:db8:1::7:0")));
+ ASSERT_NO_THROW(lq.append(range1, IOAddress("2001:db8:1::3:0")));
+ ASSERT_NO_THROW(lq.append(range1, IOAddress("2001:db8:1::")));
+
+ // Make sure we get retrieve them in the order in which they have
+ // been added.
+ IOAddress next = IOAddress::IPV6_ZERO_ADDRESS();
+ ASSERT_NO_THROW(next = lq.pop(range1));
+ EXPECT_EQ("2001:db8:1::4:0", next.toText());
+
+ ASSERT_NO_THROW(next = lq.pop(range1));
+ EXPECT_EQ("2001:db8:1::7:0", next.toText());
+
+ ASSERT_NO_THROW(next = lq.pop(range1));
+ EXPECT_EQ("2001:db8:1::3:0", next.toText());
+
+ ASSERT_NO_THROW(next = lq.pop(range1));
+ EXPECT_EQ("2001:db8:1::", next.toText());
+
+ // After we went over all of them they should all be gone from the
+ // container and the IPv6 zero address should be returned.
+ ASSERT_NO_THROW(next = lq.pop(range1));
+ EXPECT_TRUE(next.isV6Zero());
+}
+
+
+// Check that out of bounds address can't be appended to the range.
+TEST(FreeLeaseQueueTest, nextRangeMismatch) {
+ AddressRange range(IOAddress("192.0.2.1"), IOAddress("192.0.2.255"));
+
+ FreeLeaseQueue lq;
+ EXPECT_THROW(lq.append(AddressRange(range), IOAddress("192.0.3.1")),
+ isc::BadValue);
+}
+
+// Check that it is possible to return an address to the range and that the
+// appropriate range is detected.
+TEST(FreeLeaseQueueTest, detectRange) {
+ FreeLeaseQueue lq;
+
+ // Create three ranges.
+ AddressRange range1(IOAddress("192.0.2.1"), IOAddress("192.0.2.255"));
+ AddressRange range2(IOAddress("192.0.3.1"), IOAddress("192.0.3.255"));
+ AddressRange range3(IOAddress("10.0.0.1"), IOAddress("10.8.1.45"));
+ ASSERT_NO_THROW(lq.addRange(range1));
+ ASSERT_NO_THROW(lq.addRange(range2));
+ ASSERT_NO_THROW(lq.addRange(range3));
+
+ // Append some addresses matching the first range.
+ ASSERT_NO_THROW(lq.append(IOAddress("192.0.2.7")));
+ ASSERT_NO_THROW(lq.append(IOAddress("192.0.2.1")));
+ ASSERT_NO_THROW(lq.append(IOAddress("192.0.2.255")));
+
+ // Make sure that these addresses have been appended to that range.
+ IOAddress next(0);
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("192.0.2.7", next.toText());
+
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("192.0.2.1", next.toText());
+
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("192.0.2.255", next.toText());
+
+ // Append some addresses matching the remaining two ranges.
+ ASSERT_NO_THROW(lq.append(IOAddress("10.5.4.3")));
+ ASSERT_NO_THROW(lq.append(IOAddress("192.0.3.98")));
+
+ ASSERT_NO_THROW(next = lq.next(range3));
+ EXPECT_EQ("10.5.4.3", next.toText());
+
+ ASSERT_NO_THROW(next = lq.next(range2));
+ EXPECT_EQ("192.0.3.98", next.toText());
+
+ // Appending out of bounds address should not succeed.
+ EXPECT_FALSE(lq.append(IOAddress("10.0.0.0")));
+}
+
+// Check that it is possible to return a delegated prefix to the range and
+// that the appropriate range is detected.
+TEST(FreeLeaseQueueTest, detectPrefixRange) {
+ FreeLeaseQueue lq;
+
+ // Create three ranges.
+ PrefixRange range1(IOAddress("2001:db8:1::"), 64, 96);
+ PrefixRange range2(IOAddress("2001:db8:2::"), 112, 120);
+ PrefixRange range3(IOAddress("2001:db8:3::"), 96, 104);
+ ASSERT_NO_THROW(lq.addRange(range1));
+ ASSERT_NO_THROW(lq.addRange(range2));
+ ASSERT_NO_THROW(lq.addRange(range3));
+
+ // Append some prefixes matching the first range.
+ ASSERT_NO_THROW(lq.append(IOAddress("2001:db8:1::7:0"), 96));
+ ASSERT_NO_THROW(lq.append(IOAddress("2001:db8:1::100:0"), 96));
+ ASSERT_NO_THROW(lq.append(IOAddress("2001:db8:1::4:0"), 96));
+
+ // Make sure that these prefixes have been appended to that range.
+ IOAddress next(0);
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("2001:db8:1::7:0", next.toText());
+
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("2001:db8:1::100:0", next.toText());
+
+ ASSERT_NO_THROW(next = lq.next(range1));
+ EXPECT_EQ("2001:db8:1::4:0", next.toText());
+
+ // Append some prefixes matching the remaining two ranges.
+ ASSERT_NO_THROW(lq.append(IOAddress("2001:db8:2::10"), 120));
+ ASSERT_NO_THROW(lq.append(IOAddress("2001:db8:3::50"), 104));
+
+ ASSERT_NO_THROW(next = lq.next(range3));
+ EXPECT_EQ("2001:db8:3::50", next.toText());
+
+ ASSERT_NO_THROW(next = lq.next(range2));
+ EXPECT_EQ("2001:db8:2::10", next.toText());
+
+ // Appending out of bounds prefix should not succeed.
+ EXPECT_FALSE(lq.append(IOAddress("2001:db8:4::10"), 96));
+ EXPECT_FALSE(lq.append(IOAddress("2001:db8:2::30"), 97));
+}
+
+// This test verifies that false is returned if the specified address to be
+// appended does not belong to any of the existing ranges.
+TEST(FreeLeaseQueueTest, detectRangeFailed) {
+ AddressRange range(IOAddress("192.0.2.1"), IOAddress("192.0.2.255"));
+
+ FreeLeaseQueue lq;
+ ASSERT_NO_THROW(lq.addRange(range));
+
+ EXPECT_FALSE(lq.append(IOAddress("192.0.3.7")));
+}
+
+// This test verifies that it is possible to append IP addresses to the
+// selected range via random access index.
+TEST(FreeLeaseQueueTest, appendThroughRangeIndex) {
+ FreeLeaseQueue lq;
+
+ AddressRange range1(IOAddress("192.0.2.1"), IOAddress("192.0.2.255"));
+ AddressRange range2(IOAddress("192.0.3.1"), IOAddress("192.0.3.255"));
+ AddressRange range3(IOAddress("10.0.0.1"), IOAddress("10.8.1.45"));
+ ASSERT_NO_THROW(lq.addRange(range1));
+ ASSERT_NO_THROW(lq.addRange(range2));
+ ASSERT_NO_THROW(lq.addRange(range3));
+
+ uint64_t index1 = 0;
+ ASSERT_NO_THROW(index1 = lq.getRangeIndex(range1));
+ uint64_t index2 = 0;
+ ASSERT_NO_THROW(index2 = lq.getRangeIndex(range2));
+ uint64_t index3 = 0;
+ ASSERT_NO_THROW(index3 = lq.getRangeIndex(range3));
+
+ EXPECT_NE(index1, index2);
+ EXPECT_NE(index2, index3);
+ EXPECT_NE(index1, index3);
+
+ ASSERT_NO_THROW(lq.append(index1, IOAddress("192.0.2.50")));
+ ASSERT_NO_THROW(lq.append(index2, IOAddress("192.0.3.50")));
+ ASSERT_NO_THROW(lq.append(index3, IOAddress("10.1.1.50")));
+
+ ASSERT_THROW(lq.append(index2, IOAddress("10.1.1.51")), BadValue);
+ ASSERT_THROW(lq.append(index3, IOAddress("192.0.3.34")), BadValue);
+}
+
+// This test verifies that it is possible to append delegated prefixes to the
+// selected range via random access index.
+TEST(FreeLeaseQueueTest, appendPrefixThroughRangeIndex) {
+ FreeLeaseQueue lq;
+
+ PrefixRange range1(IOAddress("2001:db8:1::"), 64, 96);
+ PrefixRange range2(IOAddress("2001:db8:2::"), 64, 96);
+ PrefixRange range3(IOAddress("2001:db8:3::"), 64, 96);
+ ASSERT_NO_THROW(lq.addRange(range1));
+ ASSERT_NO_THROW(lq.addRange(range2));
+ ASSERT_NO_THROW(lq.addRange(range3));
+
+ uint64_t index1 = 0;
+ ASSERT_NO_THROW(index1 = lq.getRangeIndex(range1));
+ uint64_t index2 = 0;
+ ASSERT_NO_THROW(index2 = lq.getRangeIndex(range2));
+ uint64_t index3 = 0;
+ ASSERT_NO_THROW(index3 = lq.getRangeIndex(range3));
+
+ EXPECT_NE(index1, index2);
+ EXPECT_NE(index2, index3);
+ EXPECT_NE(index1, index3);
+
+ ASSERT_NO_THROW(lq.append(index1, IOAddress("2001:db8:1::5:0:0")));
+ ASSERT_NO_THROW(lq.append(index2, IOAddress("2001:db8:2::7:0:0")));
+ ASSERT_NO_THROW(lq.append(index3, IOAddress("2001:db8:3::2:0:0")));
+
+ ASSERT_THROW(lq.append(index2, IOAddress("2001:db8:3::3:0:0")), BadValue);
+ ASSERT_THROW(lq.append(index3, IOAddress("2001:db8:2::8:0:0")), BadValue);
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.cc b/src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.cc
new file mode 100644
index 0000000..e278eb7
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.cc
@@ -0,0 +1,4145 @@
+// Copyright (C) 2014-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <database/database_connection.h>
+#include <database/db_exceptions.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/dhcpsrv_exceptions.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/tests/generic_lease_mgr_unittest.h>
+#include <dhcpsrv/testutils/test_utils.h>
+#include <exceptions/exceptions.h>
+#include <stats/stats_mgr.h>
+#include <testutils/gtest_utils.h>
+
+#include <boost/foreach.hpp>
+#include <boost/scoped_ptr.hpp>
+
+#include <gtest/gtest.h>
+
+#include <limits>
+#include <sstream>
+
+using namespace std;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::db;
+namespace ph = std::placeholders;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+// IPv4 and IPv6 addresses used in the tests
+const char* ADDRESS4[] = {
+ "192.0.2.0", "192.0.2.1", "192.0.2.2", "192.0.2.3",
+ "192.0.2.4", "192.0.2.5", "192.0.2.6", "192.0.2.7",
+ NULL
+};
+const char* ADDRESS6[] = {
+ "2001:db8::0", "2001:db8::1", "2001:db8::2", "2001:db8::3",
+ "2001:db8::4", "2001:db8::5", "2001:db8::6", "2001:db8::7",
+ NULL
+};
+
+// Lease types that correspond to ADDRESS6 leases
+static const Lease::Type LEASETYPE6[] = {
+ Lease::TYPE_NA, Lease::TYPE_TA, Lease::TYPE_PD, Lease::TYPE_NA,
+ Lease::TYPE_TA, Lease::TYPE_PD, Lease::TYPE_NA, Lease::TYPE_TA
+};
+
+GenericLeaseMgrTest::GenericLeaseMgrTest()
+ : lmptr_(NULL) {
+ // Initialize address strings and IOAddresses
+ for (int i = 0; ADDRESS4[i] != NULL; ++i) {
+ string addr(ADDRESS4[i]);
+ straddress4_.push_back(addr);
+ IOAddress ioaddr(addr);
+ ioaddress4_.push_back(ioaddr);
+ }
+
+ for (int i = 0; ADDRESS6[i] != NULL; ++i) {
+ string addr(ADDRESS6[i]);
+ straddress6_.push_back(addr);
+ IOAddress ioaddr(addr);
+ ioaddress6_.push_back(ioaddr);
+
+ /// Let's create different lease types. We use LEASETYPE6 values as
+ /// a template
+ leasetype6_.push_back(LEASETYPE6[i]);
+ }
+}
+
+GenericLeaseMgrTest::~GenericLeaseMgrTest() {
+ // Does nothing. The derived classes are expected to clean up, i.e.
+ // remove the lmptr_ pointer.
+}
+
+Lease4Ptr
+GenericLeaseMgrTest::initializeLease4(std::string address) {
+ Lease4Ptr lease(new Lease4());
+
+ // Set the address of the lease
+ lease->addr_ = IOAddress(address);
+
+ // Set other parameters. For historical reasons, address 0 is not used.
+ if (address == straddress4_[0]) {
+ lease->hwaddr_.reset(new HWAddr(vector<uint8_t>(6, 0x08), HTYPE_ETHER));
+ lease->client_id_ = ClientIdPtr(new ClientId(vector<uint8_t>(8, 0x42)));
+ lease->valid_lft_ = 8677;
+ lease->cltt_ = 168256;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 23;
+ lease->fqdn_rev_ = true;
+ lease->fqdn_fwd_ = false;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else if (address == straddress4_[1]) {
+ lease->hwaddr_.reset(new HWAddr(vector<uint8_t>(6, 0x19), HTYPE_ETHER));
+ lease->client_id_ = ClientIdPtr(
+ new ClientId(vector<uint8_t>(8, 0x53)));
+ lease->valid_lft_ = 3677;
+ lease->cltt_ = 123456;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 73;
+ lease->fqdn_rev_ = true;
+ lease->fqdn_fwd_ = true;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else if (address == straddress4_[2]) {
+ lease->hwaddr_.reset(new HWAddr(vector<uint8_t>(6, 0x2a), HTYPE_ETHER));
+ lease->client_id_ = ClientIdPtr(new ClientId(vector<uint8_t>(8, 0x64)));
+ lease->valid_lft_ = 5412;
+ lease->cltt_ = 234567;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 73; // Same as lease 1
+ lease->fqdn_rev_ = false;
+ lease->fqdn_fwd_ = false;
+ lease->hostname_ = "";
+
+ } else if (address == straddress4_[3]) {
+ // Hardware address same as lease 1.
+ lease->hwaddr_.reset(new HWAddr(vector<uint8_t>(6, 0x19), HTYPE_ETHER));
+ lease->client_id_ = ClientIdPtr(
+ new ClientId(vector<uint8_t>(8, 0x75)));
+
+ // The times used in the next tests are deliberately restricted - we
+ // should be able to cope with valid lifetimes up to 0xffffffff.
+ // However, this will lead to overflows.
+ // @TODO: test overflow conditions when code has been fixed.
+ lease->valid_lft_ = 7000;
+ lease->cltt_ = 234567;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 37;
+ lease->fqdn_rev_ = true;
+ lease->fqdn_fwd_ = true;
+ lease->hostname_ = "otherhost.example.com.";
+
+ } else if (address == straddress4_[4]) {
+ lease->hwaddr_.reset(new HWAddr(vector<uint8_t>(6, 0x4c), HTYPE_ETHER));
+ // Same ClientId as straddr4_[1]
+ lease->client_id_ = ClientIdPtr(
+ new ClientId(vector<uint8_t>(8, 0x53))); // Same as lease 1
+ lease->valid_lft_ = 7736;
+ lease->cltt_ = 222456;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 85;
+ lease->fqdn_rev_ = true;
+ lease->fqdn_fwd_ = true;
+ lease->hostname_ = "otherhost.example.com.";
+
+ } else if (address == straddress4_[5]) {
+ // Same as lease 1
+ lease->hwaddr_.reset(new HWAddr(vector<uint8_t>(6, 0x19), HTYPE_ETHER));
+ // Same ClientId and IAID as straddress4_1
+ lease->client_id_ = ClientIdPtr(
+ new ClientId(vector<uint8_t>(8, 0x53))); // Same as lease 1
+ lease->valid_lft_ = 7832;
+ lease->cltt_ = 227476;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 175;
+ lease->fqdn_rev_ = false;
+ lease->fqdn_fwd_ = false;
+ lease->hostname_ = "otherhost.example.com.";
+ lease->setContext(Element::fromJSON("{ \"foo\": true }"));
+
+ } else if (address == straddress4_[6]) {
+ lease->hwaddr_.reset(new HWAddr(vector<uint8_t>(6, 0x6e), HTYPE_ETHER));
+ // Same ClientId as straddress4_1
+ lease->client_id_ = ClientIdPtr(
+ new ClientId(vector<uint8_t>(8, 0x53))); // Same as lease 1
+ lease->valid_lft_ = 1832;
+ lease->cltt_ = 627476;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 112;
+ lease->fqdn_rev_ = false;
+ lease->fqdn_fwd_ = true;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else if (address == straddress4_[7]) {
+ lease->hwaddr_.reset(new HWAddr(vector<uint8_t>(), HTYPE_ETHER)); // Empty
+ lease->client_id_ = ClientIdPtr(
+ new ClientId(vector<uint8_t>(8, 0x77)));
+ lease->valid_lft_ = 7975;
+ lease->cltt_ = 213876;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 19;
+ lease->fqdn_rev_ = true;
+ lease->fqdn_fwd_ = true;
+ lease->hostname_ = "myhost.example.com.";
+ lease->setContext(Element::fromJSON("{ \"bar\": false }"));
+
+ } else {
+ // Unknown address, return an empty pointer.
+ lease.reset();
+
+ }
+
+ return (lease);
+}
+
+Lease6Ptr
+GenericLeaseMgrTest::initializeLease6(std::string address) {
+ Lease6Ptr lease(new Lease6());
+
+ // Set the address of the lease
+ lease->addr_ = IOAddress(address);
+
+ // Set other parameters. For historical reasons, address 0 is not used.
+ if (address == straddress6_[0]) {
+ lease->type_ = leasetype6_[0];
+ lease->prefixlen_ = 4;
+ lease->iaid_ = 142;
+ lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x77)));
+ lease->preferred_lft_ = 900;
+ lease->valid_lft_ = 8677;
+ lease->cltt_ = 168256;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 23;
+ lease->fqdn_fwd_ = true;
+ lease->fqdn_rev_ = true;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else if (address == straddress6_[1]) {
+ lease->type_ = leasetype6_[1];
+ lease->prefixlen_ = 0;
+ lease->iaid_ = 42;
+ lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x42)));
+ lease->preferred_lft_ = 3600;
+ lease->valid_lft_ = 3677;
+ lease->cltt_ = 123456;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 73;
+ lease->fqdn_fwd_ = false;
+ lease->fqdn_rev_ = true;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else if (address == straddress6_[2]) {
+ lease->type_ = leasetype6_[2];
+ lease->prefixlen_ = 7;
+ lease->iaid_ = 89;
+ lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x3a)));
+ lease->preferred_lft_ = 1800;
+ lease->valid_lft_ = 5412;
+ lease->cltt_ = 234567;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 73; // Same as lease 1
+ lease->fqdn_fwd_ = false;
+ lease->fqdn_rev_ = false;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else if (address == straddress6_[3]) {
+ lease->type_ = leasetype6_[3];
+ lease->prefixlen_ = 28;
+ lease->iaid_ = 0xfffffffe;
+ vector<uint8_t> duid;
+ for (uint8_t i = 31; i < 126; ++i) {
+ duid.push_back(i);
+ }
+ lease->duid_ = DuidPtr(new DUID(duid));
+
+ // The times used in the next tests are deliberately restricted - we
+ // should be able to cope with valid lifetimes up to 0xffffffff.
+ // However, this will lead to overflows.
+ // @TODO: test overflow conditions when code has been fixed.
+ lease->preferred_lft_ = 7200;
+ lease->valid_lft_ = 7000;
+ lease->cltt_ = 234567;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 37;
+ lease->fqdn_fwd_ = true;
+ lease->fqdn_rev_ = false;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else if (address == straddress6_[4]) {
+ // Same DUID and IAID as straddress6_1
+ lease->type_ = leasetype6_[4];
+ lease->prefixlen_ = 15;
+ lease->iaid_ = 42;
+ lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x42)));
+ lease->preferred_lft_ = 4800;
+ lease->valid_lft_ = 7736;
+ lease->cltt_ = 222456;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 671;
+ lease->fqdn_fwd_ = true;
+ lease->fqdn_rev_ = true;
+ lease->hostname_ = "otherhost.example.com.";
+
+ } else if (address == straddress6_[5]) {
+ // Same DUID and IAID as straddress6_1
+ lease->type_ = leasetype6_[5];
+ lease->prefixlen_ = 24;
+ lease->iaid_ = 42; // Same as lease 4
+ lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x42)));
+ // Same as lease 4
+ lease->preferred_lft_ = 5400;
+ lease->valid_lft_ = 7832;
+ lease->cltt_ = 227476;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 175;
+ lease->fqdn_fwd_ = false;
+ lease->fqdn_rev_ = true;
+ lease->hostname_ = "hostname.example.com.";
+ lease->setContext(Element::fromJSON("{ \"foo\": true }"));
+
+ } else if (address == straddress6_[6]) {
+ // Same DUID as straddress6_1
+ lease->type_ = leasetype6_[6];
+ lease->prefixlen_ = 24;
+ lease->iaid_ = 93;
+ lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x42)));
+ // Same as lease 4
+ lease->preferred_lft_ = 5400;
+ lease->valid_lft_ = 1832;
+ lease->cltt_ = 627476;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 112;
+ lease->fqdn_fwd_ = false;
+ lease->fqdn_rev_ = true;
+ lease->hostname_ = "hostname.example.com.";
+
+ } else if (address == straddress6_[7]) {
+ // Same IAID as straddress6_1
+ lease->type_ = leasetype6_[7];
+ lease->prefixlen_ = 24;
+ lease->iaid_ = 42;
+ lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0xe5)));
+ lease->preferred_lft_ = 5600;
+ lease->valid_lft_ = 7975;
+ lease->cltt_ = 213876;
+ lease->updateCurrentExpirationTime();
+ lease->subnet_id_ = 19;
+ lease->fqdn_fwd_ = false;
+ lease->fqdn_rev_ = true;
+ lease->hostname_ = "hostname.example.com.";
+ lease->setContext(Element::fromJSON("{ \"bar\": false }"));
+
+ } else {
+ // Unknown address, return an empty pointer.
+ lease.reset();
+
+ }
+
+ return (lease);
+}
+
+template <typename T>
+void GenericLeaseMgrTest::checkLeasesDifferent(const std::vector<T>& leases) const {
+
+ // Check they were created
+ for (size_t i = 0; i < leases.size(); ++i) {
+ ASSERT_TRUE(leases[i]);
+ }
+
+ // Check they are different
+ for (size_t i = 0; i < (leases.size() - 1); ++i) {
+ for (size_t j = (i + 1); j < leases.size(); ++j) {
+ stringstream s;
+ s << "Comparing leases " << i << " & " << j << " for equality";
+ SCOPED_TRACE(s.str());
+ EXPECT_TRUE(*leases[i] != *leases[j]);
+ }
+ }
+}
+
+vector<Lease4Ptr>
+GenericLeaseMgrTest::createLeases4() {
+
+ // Create leases for each address
+ vector<Lease4Ptr> leases;
+ for (size_t i = 0; i < straddress4_.size(); ++i) {
+ leases.push_back(initializeLease4(straddress4_[i]));
+ }
+ EXPECT_EQ(8, leases.size());
+
+ // Check all were created and that they are different.
+ checkLeasesDifferent(leases);
+
+ return (leases);
+}
+
+vector<Lease6Ptr>
+GenericLeaseMgrTest::createLeases6() {
+
+ // Create leases for each address
+ vector<Lease6Ptr> leases;
+ for (size_t i = 0; i < straddress6_.size(); ++i) {
+ leases.push_back(initializeLease6(straddress6_[i]));
+ }
+ EXPECT_EQ(8, leases.size());
+
+ // Check all were created and that they are different.
+ checkLeasesDifferent(leases);
+
+ return (leases);
+}
+
+void
+GenericLeaseMgrTest::testGetLease4ClientId() {
+ // Let's initialize a specific lease ...
+ Lease4Ptr lease = initializeLease4(straddress4_[1]);
+ EXPECT_TRUE(lmptr_->addLease(lease));
+ Lease4Collection returned = lmptr_->getLease4(*lease->client_id_);
+
+ ASSERT_EQ(1, returned.size());
+ // We should retrieve our lease...
+ detailCompareLease(lease, *returned.begin());
+ lease = initializeLease4(straddress4_[2]);
+ returned = lmptr_->getLease4(*lease->client_id_);
+
+ ASSERT_EQ(0, returned.size());
+}
+
+void
+GenericLeaseMgrTest::testGetLease4NullClientId() {
+ // Let's initialize a specific lease ... But this time
+ // We keep its client id for further lookup and
+ // We clearly 'reset' it ...
+ Lease4Ptr leaseA = initializeLease4(straddress4_[4]);
+ ClientIdPtr client_id = leaseA->client_id_;
+ leaseA->client_id_ = ClientIdPtr();
+ ASSERT_TRUE(lmptr_->addLease(leaseA));
+
+ Lease4Collection returned = lmptr_->getLease4(*client_id);
+ // Shouldn't have our previous lease ...
+ ASSERT_TRUE(returned.empty());
+
+ // Add another lease with the non-NULL client id, and make sure that the
+ // lookup will not break due to existence of both leases with non-NULL and
+ // NULL client ids.
+ Lease4Ptr leaseB = initializeLease4(straddress4_[0]);
+ // Shouldn't throw any null pointer exception
+ ASSERT_TRUE(lmptr_->addLease(leaseB));
+ // Try to get the lease.
+ returned = lmptr_->getLease4(*client_id);
+ ASSERT_TRUE(returned.empty());
+
+ // Let's make it more interesting and add another lease with NULL client id.
+ Lease4Ptr leaseC = initializeLease4(straddress4_[5]);
+ leaseC->client_id_.reset();
+ ASSERT_TRUE(lmptr_->addLease(leaseC));
+ returned = lmptr_->getLease4(*client_id);
+ ASSERT_TRUE(returned.empty());
+
+ // But getting the lease with non-NULL client id should be successful.
+ returned = lmptr_->getLease4(*leaseB->client_id_);
+ ASSERT_EQ(1, returned.size());
+}
+
+void
+GenericLeaseMgrTest::testLease4NullClientId() {
+ // Get the leases to be used for the test.
+ vector<Lease4Ptr> leases = createLeases4();
+
+ // Let's clear client-id pointers
+ leases[1]->client_id_ = ClientIdPtr();
+ leases[2]->client_id_ = ClientIdPtr();
+ leases[3]->client_id_ = ClientIdPtr();
+
+ // Start the tests. Add three leases to the database, read them back and
+ // check they are what we think they are.
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ EXPECT_TRUE(lmptr_->addLease(leases[2]));
+ EXPECT_TRUE(lmptr_->addLease(leases[3]));
+ lmptr_->commit();
+
+ // Reopen the database to ensure that they actually got stored.
+ reopen();
+
+ Lease4Ptr l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ l_returned = lmptr_->getLease4(ioaddress4_[2]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[2], l_returned);
+
+ l_returned = lmptr_->getLease4(ioaddress4_[3]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[3], l_returned);
+
+ // Check that we can't add a second lease with the same address
+ EXPECT_FALSE(lmptr_->addLease(leases[1]));
+
+ // Check that we can get the lease by HWAddr
+ HWAddr tmp(*leases[2]->hwaddr_);
+ Lease4Collection returned = lmptr_->getLease4(tmp);
+ ASSERT_EQ(1, returned.size());
+ detailCompareLease(leases[2], *returned.begin());
+
+ l_returned = lmptr_->getLease4(tmp, leases[2]->subnet_id_);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[2], l_returned);
+
+ // Check that we can update the lease
+ // Modify some fields in lease 1 (not the address) and update it.
+ ++leases[1]->subnet_id_;
+ leases[1]->valid_lft_ *= 2;
+ lmptr_->updateLease4(leases[1]);
+
+ // ... and check that the lease is indeed updated
+ l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ // Delete a lease, check that it's gone, and that we can't delete it
+ // a second time.
+ EXPECT_TRUE(lmptr_->deleteLease(leases[1]));
+ l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ EXPECT_FALSE(l_returned);
+ EXPECT_FALSE(lmptr_->deleteLease(leases[1]));
+
+ // Check that the second address is still there.
+ l_returned = lmptr_->getLease4(ioaddress4_[2]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[2], l_returned);
+}
+
+void
+GenericLeaseMgrTest::testGetLease4HWAddr1() {
+ // Let's initialize two different leases 4 and just add the first ...
+ Lease4Ptr leaseA = initializeLease4(straddress4_[5]);
+ HWAddr hwaddrA(*leaseA->hwaddr_);
+ HWAddr hwaddrB(vector<uint8_t>(6, 0x80), HTYPE_ETHER);
+
+ EXPECT_TRUE(lmptr_->addLease(leaseA));
+
+ // we should not have a lease, with this MAC Addr
+ Lease4Collection returned = lmptr_->getLease4(hwaddrB);
+ ASSERT_EQ(0, returned.size());
+
+ // But with this one
+ returned = lmptr_->getLease4(hwaddrA);
+ ASSERT_EQ(1, returned.size());
+}
+
+void
+GenericLeaseMgrTest::testGetLease4HWAddr2() {
+ // Get the leases to be used for the test and add to the database
+ vector<Lease4Ptr> leases = createLeases4();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // Get the leases matching the hardware address of lease 1
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
+ HWAddr tmp(*leases[1]->hwaddr_);
+ Lease4Collection returned = lmptr_->getLease4(tmp);
+
+ // Should be three leases, matching leases[1], [3] and [5].
+ ASSERT_EQ(3, returned.size());
+
+ // Check the lease[5] (and only this one) has an user context.
+ size_t contexts = 0;
+ for (Lease4Collection::const_iterator i = returned.begin();
+ i != returned.end(); ++i) {
+ if ((*i)->getContext()) {
+ ++contexts;
+ EXPECT_EQ("{ \"foo\": true }", (*i)->getContext()->str());
+ }
+ }
+ EXPECT_EQ(1, contexts);
+
+ // Easiest way to check is to look at the addresses.
+ vector<string> addresses;
+ for (Lease4Collection::const_iterator i = returned.begin();
+ i != returned.end(); ++i) {
+ addresses.push_back((*i)->addr_.toText());
+ }
+ sort(addresses.begin(), addresses.end());
+ EXPECT_EQ(straddress4_[1], addresses[0]);
+ EXPECT_EQ(straddress4_[3], addresses[1]);
+ EXPECT_EQ(straddress4_[5], addresses[2]);
+
+ // Repeat test with just one expected match
+ returned = lmptr_->getLease4(*leases[2]->hwaddr_);
+ ASSERT_EQ(1, returned.size());
+ detailCompareLease(leases[2], *returned.begin());
+
+ // Check that an empty vector is valid
+ EXPECT_TRUE(leases[7]->hwaddr_->hwaddr_.empty());
+ EXPECT_FALSE(leases[7]->client_id_->getClientId().empty());
+ returned = lmptr_->getLease4(*leases[7]->hwaddr_);
+ ASSERT_EQ(1, returned.size());
+ detailCompareLease(leases[7], *returned.begin());
+
+ // Try to get something with invalid hardware address
+ vector<uint8_t> invalid(6, 0);
+ returned = lmptr_->getLease4(invalid);
+ EXPECT_EQ(0, returned.size());
+}
+
+void
+GenericLeaseMgrTest::testAddGetDelete6() {
+ const std::string addr234("2001:db8:1::234");
+ const std::string addr456("2001:db8:1::456");
+ const std::string addr789("2001:db8:1::789");
+ IOAddress addr(addr456);
+
+ uint8_t llt[] = {0, 1, 2, 3, 4, 5, 6, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};
+ DuidPtr duid(new DUID(llt, sizeof(llt)));
+
+ uint32_t iaid = 7; // just a number
+
+ SubnetID subnet_id = 8; // just another number
+
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid, iaid, 100, 200,
+ subnet_id));
+
+ EXPECT_TRUE(lmptr_->addLease(lease));
+
+ // should not be allowed to add a second lease with the same address
+ EXPECT_FALSE(lmptr_->addLease(lease));
+
+ Lease6Ptr x = lmptr_->getLease6(Lease::TYPE_NA, IOAddress(addr234));
+ EXPECT_EQ(Lease6Ptr(), x);
+
+ x = lmptr_->getLease6(Lease::TYPE_NA, IOAddress(addr456));
+ ASSERT_TRUE(x);
+
+ EXPECT_EQ(x->addr_, addr);
+ EXPECT_TRUE(*x->duid_ == *duid);
+ EXPECT_EQ(x->iaid_, iaid);
+ EXPECT_EQ(x->subnet_id_, subnet_id);
+
+ // These are not important from lease management perspective, but
+ // let's check them anyway.
+ EXPECT_EQ(Lease::TYPE_NA, x->type_);
+ EXPECT_EQ(100, x->preferred_lft_);
+ EXPECT_EQ(200, x->valid_lft_);
+
+ // Test getLease6(duid, iaid, subnet_id) - positive case
+ Lease6Ptr y = lmptr_->getLease6(Lease::TYPE_NA, *duid, iaid, subnet_id);
+ ASSERT_TRUE(y);
+ EXPECT_TRUE(*y->duid_ == *duid);
+ EXPECT_EQ(y->iaid_, iaid);
+ EXPECT_EQ(y->addr_, addr);
+
+ // Test getLease6(duid, iaid, subnet_id) - wrong iaid
+ uint32_t invalid_iaid = 9; // no such iaid
+ y = lmptr_->getLease6(Lease::TYPE_NA, *duid, invalid_iaid, subnet_id);
+ EXPECT_FALSE(y);
+
+ uint32_t invalid_subnet_id = 999;
+ y = lmptr_->getLease6(Lease::TYPE_NA, *duid, iaid, invalid_subnet_id);
+ EXPECT_FALSE(y);
+
+ // truncated duid
+ DuidPtr invalid_duid(new DUID(llt, sizeof(llt) - 1));
+ y = lmptr_->getLease6(Lease::TYPE_NA, *invalid_duid, iaid, subnet_id);
+ EXPECT_FALSE(y);
+
+ // should return false - there's no such address
+ Lease6Ptr non_existing_lease(new Lease6(Lease::TYPE_NA, IOAddress(addr789),
+ duid, iaid, 100, 200,
+ subnet_id));
+ EXPECT_FALSE(lmptr_->deleteLease(non_existing_lease));
+
+ // this one should succeed
+ EXPECT_TRUE(lmptr_->deleteLease(x));
+
+ // after the lease is deleted, it should really be gone
+ x = lmptr_->getLease6(Lease::TYPE_NA, IOAddress(addr456));
+ EXPECT_FALSE(x);
+
+ // Reopen the lease storage to make sure that lease is gone from the
+ // persistent storage.
+ reopen(V6);
+ x = lmptr_->getLease6(Lease::TYPE_NA, IOAddress(addr456));
+ EXPECT_FALSE(x);
+}
+
+void
+GenericLeaseMgrTest::testMaxDate4() {
+ // Get the leases to be used for the test.
+ vector<Lease4Ptr> leases = createLeases4();
+ Lease4Ptr lease = leases[1];
+
+ // Set valid_lft_ to 1 day, cllt_ to max time. This should make expire
+ // time too large to store.
+ lease->valid_lft_ = 24*60*60;
+ lease->cltt_ = DatabaseConnection::MAX_DB_TIME;
+
+ // Insert should throw.
+ ASSERT_THROW(lmptr_->addLease(leases[1]), DbOperationError);
+
+ // Set valid_lft_ to 0, which should make expire time small enough to
+ // store and try again.
+ lease->valid_lft_ = 0;
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ Lease4Ptr l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+}
+
+void
+GenericLeaseMgrTest::testInfiniteLifeTime4() {
+ // Get the leases to be used for the test.
+ vector<Lease4Ptr> leases = createLeases4();
+ Lease4Ptr lease = leases[1];
+
+ // Set valid_lft_ to infinite, cllt_ to now.
+ lease->valid_lft_ = Lease::INFINITY_LFT;
+ lease->cltt_ = time(NULL);
+
+ // Insert should not throw.
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ Lease4Ptr l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+}
+
+void
+GenericLeaseMgrTest::testBasicLease4() {
+ // Get the leases to be used for the test.
+ vector<Lease4Ptr> leases = createLeases4();
+ leases[2]->setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+
+ // Start the tests. Add three leases to the database, read them back and
+ // check they are what we think they are.
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ EXPECT_TRUE(lmptr_->addLease(leases[2]));
+ EXPECT_TRUE(lmptr_->addLease(leases[3]));
+ lmptr_->commit();
+
+ // Reopen the database to ensure that they actually got stored.
+ reopen(V4);
+
+ Lease4Ptr l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ l_returned = lmptr_->getLease4(ioaddress4_[2]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[2], l_returned);
+
+ l_returned = lmptr_->getLease4(ioaddress4_[3]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[3], l_returned);
+
+ // Check that we can't add a second lease with the same address
+ EXPECT_FALSE(lmptr_->addLease(leases[1]));
+
+ // Delete a lease, check that it's gone, and that we can't delete it
+ // a second time.
+ EXPECT_TRUE(lmptr_->deleteLease(leases[1]));
+ l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ EXPECT_FALSE(l_returned);
+ EXPECT_FALSE(lmptr_->deleteLease(leases[1]));
+
+ // Check that the second address is still there.
+ l_returned = lmptr_->getLease4(ioaddress4_[2]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[2], l_returned);
+
+ reopen(V4);
+
+ // The deleted lease should be still gone after we re-read leases from
+ // persistent storage.
+ l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ EXPECT_FALSE(l_returned);
+
+ l_returned = lmptr_->getLease4(ioaddress4_[2]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[2], l_returned);
+
+ l_returned = lmptr_->getLease4(ioaddress4_[3]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[3], l_returned);
+
+ // Update some FQDN data, so as we can check that update in
+ // persistent storage works as expected.
+ leases[2]->hostname_ = "memfile.example.com.";
+ leases[2]->fqdn_rev_ = true;
+
+ ASSERT_NO_THROW(lmptr_->updateLease4(leases[2]));
+
+ reopen(V4);
+
+ // The lease should be now updated in the storage.
+ l_returned = lmptr_->getLease4(ioaddress4_[2]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[2], l_returned);
+
+ l_returned = lmptr_->getLease4(ioaddress4_[3]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[3], l_returned);
+}
+
+
+void
+GenericLeaseMgrTest::testBasicLease6() {
+ // Get the leases to be used for the test.
+ vector<Lease6Ptr> leases = createLeases6();
+ leases[2]->setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+
+ // Start the tests. Add three leases to the database, read them back and
+ // check they are what we think they are.
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ EXPECT_TRUE(lmptr_->addLease(leases[2]));
+ EXPECT_TRUE(lmptr_->addLease(leases[3]));
+ lmptr_->commit();
+
+ // Reopen the database to ensure that they actually got stored.
+ reopen(V6);
+
+ Lease6Ptr l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ l_returned = lmptr_->getLease6(leasetype6_[2], ioaddress6_[2]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[2], l_returned);
+
+ l_returned = lmptr_->getLease6(leasetype6_[3], ioaddress6_[3]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[3], l_returned);
+
+ // Check that we can't add a second lease with the same address
+ EXPECT_FALSE(lmptr_->addLease(leases[1]));
+
+ // Delete a lease, check that it's gone, and that we can't delete it
+ // a second time.
+ EXPECT_TRUE(lmptr_->deleteLease(leases[1]));
+ l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
+ EXPECT_FALSE(l_returned);
+ EXPECT_FALSE(lmptr_->deleteLease(leases[1]));
+
+ // Check that the second address is still there.
+ l_returned = lmptr_->getLease6(leasetype6_[2], ioaddress6_[2]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[2], l_returned);
+
+ reopen(V6);
+
+ // The deleted lease should be still gone after we re-read leases from
+ // persistent storage.
+ l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
+ EXPECT_FALSE(l_returned);
+
+ // Check that the second address is still there.
+ l_returned = lmptr_->getLease6(leasetype6_[2], ioaddress6_[2]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[2], l_returned);
+
+ // Update some FQDN data, so as we can check that update in
+ // persistent storage works as expected.
+ leases[2]->hostname_ = "memfile.example.com.";
+ leases[2]->fqdn_rev_ = true;
+
+ ASSERT_NO_THROW(lmptr_->updateLease6(leases[2]));
+
+ reopen(V6);
+
+ // The lease should be now updated in the storage.
+ l_returned = lmptr_->getLease6(leasetype6_[2], ioaddress6_[2]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[2], l_returned);
+}
+
+void
+GenericLeaseMgrTest::testMaxDate6() {
+ // Get the leases to be used for the test.
+ vector<Lease6Ptr> leases = createLeases6();
+ Lease6Ptr lease = leases[1];
+
+ // Set valid_lft_ to 1 day, cllt_ to max time. This should make expire
+ // time too large to store.
+ lease->valid_lft_ = 24*60*60;
+ lease->cltt_ = DatabaseConnection::MAX_DB_TIME;
+
+ // Insert should throw.
+ ASSERT_THROW(lmptr_->addLease(leases[1]), DbOperationError);
+
+ // Set valid_lft_ to 0, which should make expire time small enough to
+ // store and try again.
+ lease->valid_lft_ = 0;
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ Lease6Ptr l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+}
+
+void
+GenericLeaseMgrTest::testInfiniteLifeTime6() {
+ // Get the leases to be used for the test.
+ vector<Lease6Ptr> leases = createLeases6();
+ Lease6Ptr lease = leases[1];
+
+ // Set valid_lft_ to infinite, cllt_ to now.
+ lease->valid_lft_ = Lease::INFINITY_LFT;
+ lease->cltt_ = time(NULL);
+
+ // Insert should not throw.
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ Lease6Ptr l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+}
+
+// Checks whether a MAC address can be stored and retrieved together with
+// a lease.
+void
+GenericLeaseMgrTest::testLease6MAC() {
+ // Get the leases to be used for the test.
+ vector<Lease6Ptr> leases = createLeases6();
+
+ HWAddrPtr hwaddr1(new HWAddr(vector<uint8_t>(6, 11), HTYPE_ETHER));
+ HWAddrPtr hwaddr2(new HWAddr(vector<uint8_t>(6, 22), HTYPE_DOCSIS));
+
+ leases[1]->hwaddr_ = hwaddr1; // Add hardware address to leases 1 and 2
+ leases[2]->hwaddr_ = hwaddr2;
+ leases[3]->hwaddr_ = HWAddrPtr(); // No hardware address for the third one
+
+ // Start the tests. Add three leases to the database, read them back and
+ // check they are what we think they are.
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ EXPECT_TRUE(lmptr_->addLease(leases[2]));
+ EXPECT_TRUE(lmptr_->addLease(leases[3]));
+ lmptr_->commit();
+
+ // Reopen the database to ensure that they actually got stored.
+ reopen(V6);
+
+ // First lease should have a hardware address in it
+ Lease6Ptr stored1 = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
+ ASSERT_TRUE(stored1);
+ ASSERT_TRUE(stored1->hwaddr_);
+ EXPECT_TRUE(*hwaddr1 == *stored1->hwaddr_);
+
+ // Second lease should have a hardware address in it
+ Lease6Ptr stored2 = lmptr_->getLease6(leasetype6_[2], ioaddress6_[2]);
+ ASSERT_TRUE(stored2);
+ ASSERT_TRUE(stored2->hwaddr_);
+ EXPECT_TRUE(*hwaddr2 == *stored2->hwaddr_);
+
+ // Third lease should NOT have any hardware address.
+ Lease6Ptr stored3 = lmptr_->getLease6(leasetype6_[3], ioaddress6_[3]);
+ ASSERT_TRUE(stored3);
+ EXPECT_FALSE(stored3->hwaddr_);
+}
+
+// Checks whether a hardware address type can be stored and retrieved.
+void
+GenericLeaseMgrTest::testLease6HWTypeAndSource() {
+ // Get the leases to be used for the test.
+ vector<Lease6Ptr> leases = createLeases6();
+
+ HWAddrPtr hwaddr1(new HWAddr(vector<uint8_t>(6, 11), 123));
+ HWAddrPtr hwaddr2(new HWAddr(vector<uint8_t>(6, 22), 456));
+
+ // Those should use defines from Pkt::HWADDR_SOURCE_*, but let's
+ // test an uncommon value (and 0 which means unknown).
+ hwaddr1->source_ = HWAddr::HWADDR_SOURCE_RAW;
+ hwaddr2->source_ = HWAddr::HWADDR_SOURCE_DUID;
+
+ leases[1]->hwaddr_ = hwaddr1; // Add hardware address to leases 1 and 2
+ leases[2]->hwaddr_ = hwaddr2;
+ leases[3]->hwaddr_ = HWAddrPtr(); // No hardware address for the third one
+
+ // Start the tests. Add three leases to the database, read them back and
+ // check they are what we think they are.
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ EXPECT_TRUE(lmptr_->addLease(leases[2]));
+ EXPECT_TRUE(lmptr_->addLease(leases[3]));
+ lmptr_->commit();
+
+ // Reopen the database to ensure that they actually got stored.
+ reopen(V6);
+
+ // First lease should have a hardware address in it
+ Lease6Ptr stored1 = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
+ ASSERT_TRUE(stored1);
+ ASSERT_TRUE(stored1->hwaddr_);
+ EXPECT_EQ(123, stored1->hwaddr_->htype_);
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_RAW, stored1->hwaddr_->source_);
+
+ // Second lease should have a hardware address in it
+ Lease6Ptr stored2 = lmptr_->getLease6(leasetype6_[2], ioaddress6_[2]);
+ ASSERT_TRUE(stored2);
+ ASSERT_TRUE(stored2->hwaddr_);
+ EXPECT_EQ(456, stored2->hwaddr_->htype_);
+ EXPECT_EQ(HWAddr::HWADDR_SOURCE_DUID, stored2->hwaddr_->source_);
+
+ // Third lease should NOT have any hardware address.
+ Lease6Ptr stored3 = lmptr_->getLease6(leasetype6_[3], ioaddress6_[3]);
+ ASSERT_TRUE(stored3);
+ EXPECT_FALSE(stored3->hwaddr_);
+}
+
+void
+GenericLeaseMgrTest::testLease4InvalidHostname() {
+ // Get the leases to be used for the test.
+ vector<Lease4Ptr> leases = createLeases4();
+
+ // Create a dummy hostname, consisting of 255 characters.
+ leases[1]->hostname_.assign(255, 'a');
+ ASSERT_TRUE(lmptr_->addLease(leases[1]));
+
+ // The new lease must be in the database.
+ Lease4Ptr l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ detailCompareLease(leases[1], l_returned);
+
+ // Let's delete the lease, so as we can try to add it again with
+ // invalid hostname.
+ EXPECT_TRUE(lmptr_->deleteLease(leases[1]));
+
+ // Create a hostname with 256 characters. It should not be accepted.
+ leases[1]->hostname_.assign(256, 'a');
+ EXPECT_THROW(lmptr_->addLease(leases[1]), DbOperationError);
+}
+
+/// @brief Verify that too long hostname for Lease6 is not accepted.
+void
+GenericLeaseMgrTest::testLease6InvalidHostname() {
+ // Get the leases to be used for the test.
+ vector<Lease6Ptr> leases = createLeases6();
+
+ // Create a dummy hostname, consisting of 255 characters.
+ leases[1]->hostname_.assign(255, 'a');
+ ASSERT_TRUE(lmptr_->addLease(leases[1]));
+
+ // The new lease must be in the database.
+ Lease6Ptr l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
+ detailCompareLease(leases[1], l_returned);
+
+ // Let's delete the lease, so as we can try to add it again with
+ // invalid hostname.
+ EXPECT_TRUE(lmptr_->deleteLease(leases[1]));
+
+ // Create a hostname with 256 characters. It should not be accepted.
+ leases[1]->hostname_.assign(256, 'a');
+ EXPECT_THROW(lmptr_->addLease(leases[1]), DbOperationError);
+}
+
+void
+GenericLeaseMgrTest::testGetLease4HWAddrSize() {
+ // Create leases, although we need only one.
+ vector<Lease4Ptr> leases = createLeases4();
+
+ // Now add leases with increasing hardware address size.
+ for (uint8_t i = 0; i <= HWAddr::MAX_HWADDR_LEN; ++i) {
+ leases[1]->hwaddr_->hwaddr_.resize(i, i);
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
+ Lease4Collection returned =
+ lmptr_->getLease4(*leases[1]->hwaddr_);
+
+ ASSERT_EQ(1, returned.size());
+ detailCompareLease(leases[1], *returned.begin());
+ ASSERT_TRUE(lmptr_->deleteLease(leases[1]));
+ }
+
+ // Database should not let us add one that is too big
+ // (The 42 is a random value put in each byte of the address.)
+ leases[1]->hwaddr_->hwaddr_.resize(HWAddr::MAX_HWADDR_LEN + 100, 42);
+ EXPECT_THROW(lmptr_->addLease(leases[1]), isc::db::DbOperationError);
+}
+
+void
+GenericLeaseMgrTest::testGetLease4HWAddrSubnetId() {
+ // Get the leases to be used for the test and add to the database
+ vector<Lease4Ptr> leases = createLeases4();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // Get the leases matching the hardware address of lease 1 and
+ // subnet ID of lease 1. Result should be a single lease - lease 1.
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
+ Lease4Ptr returned = lmptr_->getLease4(*leases[1]->hwaddr_,
+ leases[1]->subnet_id_);
+
+ ASSERT_TRUE(returned);
+ detailCompareLease(leases[1], returned);
+
+ // Try for a match to the hardware address of lease 1 and the wrong
+ // subnet ID.
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
+ returned = lmptr_->getLease4(*leases[1]->hwaddr_, leases[1]->subnet_id_ + 1);
+ EXPECT_FALSE(returned);
+
+ // Try for a match to the subnet ID of lease 1 (and lease 4) but
+ // the wrong hardware address.
+ vector<uint8_t> invalid_hwaddr(15, 0x77);
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
+ returned = lmptr_->getLease4(HWAddr(invalid_hwaddr, HTYPE_ETHER),
+ leases[1]->subnet_id_);
+ EXPECT_FALSE(returned);
+
+ // Try for a match to an unknown hardware address and an unknown
+ // subnet ID.
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
+ returned = lmptr_->getLease4(HWAddr(invalid_hwaddr, HTYPE_ETHER),
+ leases[1]->subnet_id_ + 1);
+ EXPECT_FALSE(returned);
+
+ // Add a second lease with the same values as the first and check that
+ // an attempt to access the database by these parameters throws a
+ // "multiple records" exception. (We expect there to be only one record
+ // with that combination, so getting them via getLeaseX() (as opposed
+ // to getLeaseXCollection() should throw an exception.)
+ EXPECT_TRUE(lmptr_->deleteLease(leases[2]));
+ leases[1]->addr_ = leases[2]->addr_;
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
+ EXPECT_THROW(returned = lmptr_->getLease4(*leases[1]->hwaddr_,
+ leases[1]->subnet_id_),
+ isc::db::MultipleRecords);
+
+}
+
+void
+GenericLeaseMgrTest::testGetLease4HWAddrSubnetIdSize() {
+ // Create leases, although we need only one.
+ vector<Lease4Ptr> leases = createLeases4();
+
+ // Now add leases with increasing hardware address size and check
+ // that they can be retrieved.
+ for (uint8_t i = 0; i <= HWAddr::MAX_HWADDR_LEN; ++i) {
+ leases[1]->hwaddr_->hwaddr_.resize(i, i);
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
+ Lease4Ptr returned = lmptr_->getLease4(*leases[1]->hwaddr_,
+ leases[1]->subnet_id_);
+ ASSERT_TRUE(returned);
+ detailCompareLease(leases[1], returned);
+ ASSERT_TRUE(lmptr_->deleteLease(leases[1]));
+ }
+
+ // Database should not let us add one that is too big
+ // (The 42 is a random value put in each byte of the address.)
+ leases[1]->hwaddr_->hwaddr_.resize(HWAddr::MAX_HWADDR_LEN + 100, 42);
+ EXPECT_THROW(lmptr_->addLease(leases[1]), isc::db::DbOperationError);
+}
+
+void
+GenericLeaseMgrTest::testGetLease4ClientId2() {
+ // Get the leases to be used for the test and add to the database
+ vector<Lease4Ptr> leases = createLeases4();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // Get the leases matching the Client ID address of lease 1
+ Lease4Collection returned = lmptr_->getLease4(*leases[1]->client_id_);
+
+ // Should be four leases, matching leases[1], [4], [5] and [6].
+ ASSERT_EQ(4, returned.size());
+
+ // Check the lease[5] (and only this one) has an user context.
+ size_t contexts = 0;
+ for (Lease4Collection::const_iterator i = returned.begin();
+ i != returned.end(); ++i) {
+ if ((*i)->getContext()) {
+ ++contexts;
+ EXPECT_EQ("{ \"foo\": true }", (*i)->getContext()->str());
+ }
+ }
+ EXPECT_EQ(1, contexts);
+
+ // Easiest way to check is to look at the addresses.
+ vector<string> addresses;
+ for (Lease4Collection::const_iterator i = returned.begin();
+ i != returned.end(); ++i) {
+ addresses.push_back((*i)->addr_.toText());
+ }
+ sort(addresses.begin(), addresses.end());
+ EXPECT_EQ(straddress4_[1], addresses[0]);
+ EXPECT_EQ(straddress4_[4], addresses[1]);
+ EXPECT_EQ(straddress4_[5], addresses[2]);
+ EXPECT_EQ(straddress4_[6], addresses[3]);
+
+ // Repeat test with just one expected match
+ returned = lmptr_->getLease4(*leases[3]->client_id_);
+ ASSERT_EQ(1, returned.size());
+ detailCompareLease(leases[3], *returned.begin());
+
+ // Try to get something with invalid client ID
+ const uint8_t invalid_data[] = {0, 0, 0};
+ ClientId invalid(invalid_data, sizeof(invalid_data));
+ returned = lmptr_->getLease4(invalid);
+ EXPECT_EQ(0, returned.size());
+}
+
+void
+GenericLeaseMgrTest::testGetLease4ClientIdSize() {
+ // Create leases, although we need only one.
+ vector<Lease4Ptr> leases = createLeases4();
+
+ // Now add leases with increasing Client ID size can be retrieved.
+ // For speed, go from 0 to 128 is steps of 16.
+ // Intermediate client_id_max is to overcome problem if
+ // ClientId::MAX_CLIENT_ID_LEN is used in an EXPECT_EQ.
+ int client_id_max = ClientId::MAX_CLIENT_ID_LEN;
+ EXPECT_EQ(128, client_id_max);
+
+ int client_id_min = ClientId::MIN_CLIENT_ID_LEN;
+ EXPECT_EQ(2, client_id_min); // See RFC2132, section 9.14
+
+ for (uint8_t i = client_id_min; i <= client_id_max; i += 16) {
+ vector<uint8_t> clientid_vec(i, i);
+ leases[1]->client_id_.reset(new ClientId(clientid_vec));
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ Lease4Collection returned = lmptr_->getLease4(*leases[1]->client_id_);
+ ASSERT_EQ(returned.size(), 1u);
+ detailCompareLease(leases[1], *returned.begin());
+ ASSERT_TRUE(lmptr_->deleteLease(leases[1]));
+ }
+
+ // Don't bother to check client IDs longer than the maximum -
+ // these cannot be constructed, and that limitation is tested
+ // in the DUID/Client ID unit tests.
+}
+
+void
+GenericLeaseMgrTest::testGetLease4ClientIdSubnetId() {
+ // Get the leases to be used for the test and add to the database
+ vector<Lease4Ptr> leases = createLeases4();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // Get the leases matching the client ID of lease 1 and
+ // subnet ID of lease 1. Result should be a single lease - lease 1.
+ Lease4Ptr returned = lmptr_->getLease4(*leases[1]->client_id_,
+ leases[1]->subnet_id_);
+ ASSERT_TRUE(returned);
+ detailCompareLease(leases[1], returned);
+
+ // Try for a match to the client ID of lease 1 and the wrong
+ // subnet ID.
+ returned = lmptr_->getLease4(*leases[1]->client_id_,
+ leases[1]->subnet_id_ + 1);
+ EXPECT_FALSE(returned);
+
+ // Try for a match to the subnet ID of lease 1 (and lease 4) but
+ // the wrong client ID
+ const uint8_t invalid_data[] = {0, 0, 0};
+ ClientId invalid(invalid_data, sizeof(invalid_data));
+ returned = lmptr_->getLease4(invalid, leases[1]->subnet_id_);
+ EXPECT_FALSE(returned);
+
+ // Try for a match to an unknown hardware address and an unknown
+ // subnet ID.
+ returned = lmptr_->getLease4(invalid, leases[1]->subnet_id_ + 1);
+ EXPECT_FALSE(returned);
+}
+
+void
+GenericLeaseMgrTest::testGetLeases4SubnetId() {
+ // Get the leases to be used for the test and add to the database.
+ vector<Lease4Ptr> leases = createLeases4();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // There should be exactly two leases for the subnet id that the second
+ // lease belongs to.
+ Lease4Collection returned = lmptr_->getLeases4(leases[1]->subnet_id_);
+ ASSERT_EQ(2, returned.size());
+}
+
+void
+GenericLeaseMgrTest::testGetLeases4Hostname() {
+ // Get the leases to be used for the test and add to the database.
+ vector<Lease4Ptr> leases = createLeases4();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // There should be no lease for hostname foobar.
+ Lease4Collection returned = lmptr_->getLeases4(string("foobar"));
+ EXPECT_TRUE(returned.empty());
+
+ // There should be exactly 4 leases for the hostname of the second lease.
+ ASSERT_FALSE(leases[1]->hostname_.empty());
+ returned = lmptr_->getLeases4(leases[1]->hostname_);
+ EXPECT_EQ(4, returned.size());
+
+ // And 3 for the forth lease.
+ ASSERT_FALSE(leases[3]->hostname_.empty());
+ returned = lmptr_->getLeases4(leases[3]->hostname_);
+ EXPECT_EQ(3, returned.size());
+}
+
+void
+GenericLeaseMgrTest::testGetLeases4() {
+ // Get the leases to be used for the test and add to the database
+ vector<Lease4Ptr> leases = createLeases4();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // All leases should be returned.
+ Lease4Collection returned = lmptr_->getLeases4();
+ ASSERT_EQ(leases.size(), returned.size());
+}
+
+void
+GenericLeaseMgrTest::testGetLeases4Paged() {
+ // Get the leases to be used for the test and add to the database.
+ vector<Lease4Ptr> leases = createLeases4();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ Lease4Collection all_leases;
+
+ IOAddress last_address = IOAddress("0.0.0.0");
+ for (auto i = 0; i < 4; ++i) {
+ Lease4Collection page = lmptr_->getLeases4(last_address, LeasePageSize(3));
+
+ // Collect leases in a common structure. They may be out of order.
+ for (Lease4Ptr lease : page) {
+ all_leases.push_back(lease);
+ }
+
+ // Empty page means there are no more leases.
+ if (page.empty()) {
+ break;
+
+ } else {
+ // Record last returned address because it is going to be used
+ // as an argument for the next call.
+ last_address = page[page.size() - 1]->addr_;
+ }
+ }
+
+ // Make sure that we got exactly the number of leases that we earlier
+ // stored in the database.
+ EXPECT_EQ(leases.size(), all_leases.size());
+
+ // Make sure that all leases that we stored in the lease database
+ // have been retrieved.
+ for (Lease4Ptr lease : leases) {
+ bool found = false;
+ for (Lease4Ptr returned_lease : all_leases) {
+ if (lease->addr_ == returned_lease->addr_) {
+ found = true;
+ break;
+ }
+ }
+ EXPECT_TRUE(found) << "lease for address " << lease->addr_.toText()
+ << " was not returned in any of the pages";
+ }
+
+ boost::scoped_ptr<LeasePageSize> lease_page_size;
+
+ // The maximum allowed value for the limit is max for uint32_t.
+ size_t oor = static_cast<size_t>(std::numeric_limits<uint32_t>::max()) + 1;
+ EXPECT_THROW(lease_page_size.reset(new LeasePageSize(oor)), OutOfRange);
+
+ // Zero page size is illegal too.
+ EXPECT_THROW(lease_page_size.reset(new LeasePageSize(0)), OutOfRange);
+
+ // Only IPv4 address can be used.
+ EXPECT_THROW(lmptr_->getLeases4(IOAddress("2001:db8::1"), LeasePageSize(3)),
+ InvalidAddressFamily);
+}
+
+void
+GenericLeaseMgrTest::testGetLeases6SubnetId() {
+ // Get the leases to be used for the test and add to the database.
+ vector<Lease6Ptr> leases = createLeases6();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // There should be exactly two leases for the subnet id that the second
+ // lease belongs to.
+ Lease6Collection returned = lmptr_->getLeases6(leases[1]->subnet_id_);
+ EXPECT_EQ(2, returned.size());
+}
+
+void
+GenericLeaseMgrTest::testGetLeases6Hostname() {
+ // Get the leases to be used for the test and add to the database.
+ vector<Lease6Ptr> leases = createLeases6();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // There should be no lease for hostname foobar.
+ Lease6Collection returned = lmptr_->getLeases6(string("foobar"));
+ EXPECT_TRUE(returned.empty());
+
+ // There should be exactly 4 leases for the hostname of the second lease.
+ ASSERT_FALSE(leases[1]->hostname_.empty());
+ returned = lmptr_->getLeases6(leases[1]->hostname_);
+ EXPECT_EQ(4, returned.size());
+
+ // One for the fifth lease.
+ ASSERT_FALSE(leases[4]->hostname_.empty());
+ returned = lmptr_->getLeases6(leases[4]->hostname_);
+ EXPECT_EQ(1, returned.size());
+
+ // And 3 for the sixth lease.
+ ASSERT_FALSE(leases[5]->hostname_.empty());
+ returned = lmptr_->getLeases6(leases[5]->hostname_);
+ EXPECT_EQ(3, returned.size());
+}
+
+void
+GenericLeaseMgrTest::testGetLeases6() {
+ // Get the leases to be used for the test and add to the database
+ vector<Lease6Ptr> leases = createLeases6();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // All leases should be returned.
+ Lease6Collection returned = lmptr_->getLeases6();
+ ASSERT_EQ(leases.size(), returned.size());
+}
+
+void
+GenericLeaseMgrTest::testGetLeases6Paged() {
+ // Get the leases to be used for the test and add to the database.
+ vector<Lease6Ptr> leases = createLeases6();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ Lease6Collection all_leases;
+
+ IOAddress last_address = IOAddress::IPV6_ZERO_ADDRESS();
+ for (auto i = 0; i < 4; ++i) {
+ Lease6Collection page = lmptr_->getLeases6(last_address, LeasePageSize(3));
+
+ // Collect leases in a common structure. They may be out of order.
+ for (Lease6Ptr lease : page) {
+ all_leases.push_back(lease);
+ }
+
+ // Empty page means there are no more leases.
+ if (page.empty()) {
+ break;
+
+ } else {
+ // Record last returned address because it is going to be used
+ // as an argument for the next call.
+ last_address = page[page.size() - 1]->addr_;
+ }
+ }
+
+ // Make sure that we got exactly the number of leases that we earlier
+ // stored in the database.
+ EXPECT_EQ(leases.size(), all_leases.size());
+
+ // Make sure that all leases that we stored in the lease database
+ // have been retrieved.
+ for (Lease6Ptr lease : leases) {
+ bool found = false;
+ for (Lease6Ptr returned_lease : all_leases) {
+ if (lease->addr_ == returned_lease->addr_) {
+ found = true;
+ break;
+ }
+ }
+ EXPECT_TRUE(found) << "lease for address " << lease->addr_.toText()
+ << " was not returned in any of the pages";
+ }
+
+ // Only IPv6 address can be used.
+ EXPECT_THROW(lmptr_->getLeases6(IOAddress("192.0.2.0"), LeasePageSize(3)),
+ InvalidAddressFamily);
+}
+
+void
+GenericLeaseMgrTest::testGetLeases6DuidIaid() {
+ // Get the leases to be used for the test.
+ vector<Lease6Ptr> leases = createLeases6();
+ ASSERT_LE(6, leases.size()); // Expect to access leases 0 through 5
+
+ // Add them to the database
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // Get the leases matching the DUID and IAID of lease[1].
+ Lease6Collection returned = lmptr_->getLeases6(leasetype6_[1],
+ *leases[1]->duid_,
+ leases[1]->iaid_);
+
+ // Should be two leases, matching leases[1] and [4].
+ ASSERT_EQ(2, returned.size());
+
+ // Easiest way to check is to look at the addresses.
+ vector<string> addresses;
+ for (Lease6Collection::const_iterator i = returned.begin();
+ i != returned.end(); ++i) {
+ addresses.push_back((*i)->addr_.toText());
+ }
+ sort(addresses.begin(), addresses.end());
+ EXPECT_EQ(straddress6_[1], addresses[0]);
+ EXPECT_EQ(straddress6_[4], addresses[1]);
+
+ // Check that nothing is returned when either the IAID or DUID match
+ // nothing.
+ returned = lmptr_->getLeases6(leasetype6_[1], *leases[1]->duid_,
+ leases[1]->iaid_ + 1);
+ EXPECT_EQ(0, returned.size());
+
+ // Alter the leases[1] DUID to match nothing in the database.
+ vector<uint8_t> duid_vector = leases[1]->duid_->getDuid();
+ ++duid_vector[0];
+ DUID new_duid(duid_vector);
+ returned = lmptr_->getLeases6(leasetype6_[1], new_duid, leases[1]->iaid_);
+ EXPECT_EQ(0, returned.size());
+}
+
+void
+GenericLeaseMgrTest::testGetLeases6DuidSize() {
+ // Create leases, although we need only one.
+ vector<Lease6Ptr> leases = createLeases6();
+
+ // Now add leases with increasing DUID size can be retrieved.
+ // For speed, go from 0 to 128 is steps of 16.
+ int duid_max = DUID::MAX_DUID_LEN;
+ EXPECT_EQ(128, duid_max);
+
+ int duid_min = DUID::MIN_DUID_LEN;
+ EXPECT_EQ(1, duid_min);
+
+ for (uint8_t i = duid_min; i <= duid_max; i += 16) {
+ vector<uint8_t> duid_vec(i, i);
+ leases[1]->duid_.reset(new DUID(duid_vec));
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ Lease6Collection returned = lmptr_->getLeases6(leasetype6_[1],
+ *leases[1]->duid_,
+ leases[1]->iaid_);
+ ASSERT_EQ(1, returned.size());
+ detailCompareLease(leases[1], *returned.begin());
+ ASSERT_TRUE(lmptr_->deleteLease(leases[1]));
+ }
+
+ // Don't bother to check DUIDs longer than the maximum - these cannot be
+ // constructed, and that limitation is tested in the DUID/Client ID unit
+ // tests.
+
+}
+
+void
+GenericLeaseMgrTest::testLease6LeaseTypeCheck() {
+ Lease6Ptr empty_lease(new Lease6());
+
+ DuidPtr duid(new DUID(vector<uint8_t>(8, 0x77)));
+
+ empty_lease->iaid_ = 142;
+ empty_lease->duid_ = DuidPtr(new DUID(*duid));
+ empty_lease->subnet_id_ = 23;
+ empty_lease->preferred_lft_ = 100;
+ empty_lease->valid_lft_ = 100;
+ empty_lease->cltt_ = 100;
+ empty_lease->fqdn_fwd_ = true;
+ empty_lease->fqdn_rev_ = true;
+ empty_lease->hostname_ = "myhost.example.com.";
+ empty_lease->prefixlen_ = 4;
+
+ // Make Two leases per lease type, all with the same DUID, IAID but
+ // alternate the subnet_ids.
+ vector<Lease6Ptr> leases;
+ for (int i = 0; i < 6; ++i) {
+ Lease6Ptr lease(new Lease6(*empty_lease));
+ lease->type_ = leasetype6_[i / 2];
+ lease->addr_ = IOAddress(straddress6_[i]);
+ lease->subnet_id_ += (i % 2);
+ leases.push_back(lease);
+ EXPECT_TRUE(lmptr_->addLease(lease));
+ }
+
+ // Verify getting a single lease by type and address.
+ for (int i = 0; i < 6; ++i) {
+ // Look for exact match for each lease type.
+ Lease6Ptr returned = lmptr_->getLease6(leasetype6_[i / 2],
+ leases[i]->addr_);
+ // We should match one per lease type.
+ ASSERT_TRUE(returned);
+ EXPECT_TRUE(*returned == *leases[i]);
+
+ // Same address but wrong lease type, should not match.
+ returned = lmptr_->getLease6(leasetype6_[i / 2 + 1], leases[i]->addr_);
+ ASSERT_FALSE(returned);
+ }
+
+ // Verify getting a collection of leases by type, DUID, and IAID.
+ // Iterate over the lease types, asking for leases based on
+ // lease type, DUID, and IAID.
+ for (int i = 0; i < 3; ++i) {
+ Lease6Collection returned = lmptr_->getLeases6(leasetype6_[i],
+ *duid, 142);
+ // We should match two per lease type.
+ ASSERT_EQ(2, returned.size());
+
+ // Collection order returned is not guaranteed.
+ // Easiest way to check is to look at the addresses.
+ vector<string> addresses;
+ for (Lease6Collection::const_iterator it = returned.begin();
+ it != returned.end(); ++it) {
+ addresses.push_back((*it)->addr_.toText());
+ }
+ sort(addresses.begin(), addresses.end());
+
+ // Now verify that the lease addresses match.
+ EXPECT_EQ(addresses[0], leases[(i * 2)]->addr_.toText());
+ EXPECT_EQ(addresses[1], leases[(i * 2 + 1)]->addr_.toText());
+ }
+
+ // Verify getting a collection of leases by type, DUID, IAID, and subnet id.
+ // Iterate over the lease types, asking for leases based on
+ // lease type, DUID, IAID, and subnet_id.
+ for (int i = 0; i < 3; ++i) {
+ Lease6Collection returned = lmptr_->getLeases6(leasetype6_[i],
+ *duid, 142, 23);
+ // We should match one per lease type.
+ ASSERT_EQ(1, returned.size());
+ EXPECT_TRUE(*(returned[0]) == *leases[i * 2]);
+ }
+
+ // Verify getting a single lease by type, duid, iad, and subnet id.
+ for (int i = 0; i < 6; ++i) {
+ Lease6Ptr returned = lmptr_->getLease6(leasetype6_[i / 2],
+ *duid, 142, (23 + (i % 2)));
+ // We should match one per lease type.
+ ASSERT_TRUE(returned);
+ EXPECT_TRUE(*returned == *leases[i]);
+ }
+}
+
+void
+GenericLeaseMgrTest::testLease6LargeIaidCheck() {
+
+ DuidPtr duid(new DUID(vector<uint8_t>(8, 0x77)));
+ IOAddress addr(std::string("2001:db8:1::111"));
+ SubnetID subnet_id = 8; // random number
+
+ // Use a value we know is larger than 32-bit signed max.
+ uint32_t large_iaid = 0xFFFFFFFE;
+
+ // We should be able to add with no problems.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid, large_iaid,
+ 100, 200, subnet_id));
+ ASSERT_TRUE(lmptr_->addLease(lease));
+
+ // Sanity check that we added it.
+ Lease6Ptr found_lease = lmptr_->getLease6(Lease::TYPE_NA, addr);
+ ASSERT_TRUE(found_lease);
+ EXPECT_TRUE(*found_lease == *lease);
+
+ // Verify getLease6() duid/iaid finds it.
+ found_lease = lmptr_->getLease6(Lease::TYPE_NA, *duid, large_iaid, subnet_id);
+ ASSERT_TRUE(found_lease);
+ EXPECT_TRUE(*found_lease == *lease);
+
+ // Verify getLeases6() duid/iaid finds it.
+ Lease6Collection found_leases = lmptr_->getLeases6(Lease::TYPE_NA,
+ *duid, large_iaid);
+ // We should match the lease.
+ ASSERT_EQ(1, found_leases.size());
+ EXPECT_TRUE(*(found_leases[0]) == *lease);
+}
+
+void
+GenericLeaseMgrTest::testGetLease6DuidIaidSubnetId() {
+ // Get the leases to be used for the test and add them to the database.
+ vector<Lease6Ptr> leases = createLeases6();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // Get the leases matching the DUID and IAID of lease[1].
+ Lease6Ptr returned = lmptr_->getLease6(leasetype6_[1], *leases[1]->duid_,
+ leases[1]->iaid_,
+ leases[1]->subnet_id_);
+ ASSERT_TRUE(returned);
+ EXPECT_TRUE(*returned == *leases[1]);
+
+ // Modify each of the three parameters (DUID, IAID, Subnet ID) and
+ // check that nothing is returned.
+ returned = lmptr_->getLease6(leasetype6_[1], *leases[1]->duid_,
+ leases[1]->iaid_ + 1, leases[1]->subnet_id_);
+ EXPECT_FALSE(returned);
+
+ returned = lmptr_->getLease6(leasetype6_[1], *leases[1]->duid_,
+ leases[1]->iaid_, leases[1]->subnet_id_ + 1);
+ EXPECT_FALSE(returned);
+
+ // Alter the leases[1] DUID to match nothing in the database.
+ vector<uint8_t> duid_vector = leases[1]->duid_->getDuid();
+ ++duid_vector[0];
+ DUID new_duid(duid_vector);
+ returned = lmptr_->getLease6(leasetype6_[1], new_duid, leases[1]->iaid_,
+ leases[1]->subnet_id_);
+ EXPECT_FALSE(returned);
+}
+
+/// @brief verifies getLeases6(DUID)
+void
+GenericLeaseMgrTest::testGetLeases6Duid() {
+ //add leases
+ IOAddress addr1(std::string("2001:db8:1::111"));
+ IOAddress addr2(std::string("2001:db8:1::222"));
+ IOAddress addr3(std::string("2001:db8:1::333"));
+
+ DuidPtr duid1(new DUID({0, 1, 1, 1, 1, 1, 1, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf}));
+ DuidPtr duid2(new DUID({0, 2, 2, 2, 2, 2, 2, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf}));
+ DuidPtr duid3(new DUID({0, 3, 3, 3, 3, 3, 3, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf}));
+ DuidPtr duid4(new DUID({0, 4, 4, 4, 4, 4, 4, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf}));
+
+ uint32_t iaid = 7; // random number
+
+ SubnetID subnet_id = 8; // random number
+
+ Lease6Ptr lease1(new Lease6(Lease::TYPE_NA, addr1, duid1, iaid, 100, 200,
+ subnet_id));
+ Lease6Ptr lease2(new Lease6(Lease::TYPE_NA, addr2, duid2, iaid, 100, 200,
+ subnet_id));
+ Lease6Ptr lease3(new Lease6(Lease::TYPE_PD, addr3, duid3, iaid, 100, 200,
+ subnet_id, HWAddrPtr(), 64));
+
+ EXPECT_TRUE(lmptr_->addLease(lease1));
+ EXPECT_TRUE(lmptr_->addLease(lease2));
+ EXPECT_TRUE(lmptr_->addLease(lease3));
+
+ Lease6Collection returned1 = lmptr_->getLeases6(*(lease1->duid_));
+ Lease6Collection returned2 = lmptr_->getLeases6(*(lease2->duid_));
+ Lease6Collection returned3 = lmptr_->getLeases6(*(lease3->duid_));
+
+ //verify if the returned lease mathces
+ ASSERT_EQ(returned1.size(), 1);
+ ASSERT_EQ(returned2.size(), 1);
+ ASSERT_EQ(returned3.size(), 1);
+
+ //verify that the returned lease are same
+ EXPECT_TRUE(returned1[0]->addr_ == lease1->addr_);
+ EXPECT_TRUE(returned2[0]->addr_ == lease2->addr_);
+ EXPECT_TRUE(returned3[0]->addr_ == lease3->addr_);
+
+ //now verify we return empty for a lease that has not been stored
+ returned3 = lmptr_->getLeases6(*duid4);
+ EXPECT_TRUE(returned3.empty());
+
+ //clean up
+ ASSERT_TRUE(lmptr_->deleteLease(lease1));
+ ASSERT_TRUE(lmptr_->deleteLease(lease2));
+ ASSERT_TRUE(lmptr_->deleteLease(lease3));
+
+ //now verify we return empty for a lease that has not been stored
+ returned3 = lmptr_->getLeases6(*duid4);
+ EXPECT_TRUE(returned3.empty());
+}
+
+/// @brief Checks that getLease6() works with different DUID sizes
+void
+GenericLeaseMgrTest::testGetLease6DuidIaidSubnetIdSize() {
+
+ // Create leases, although we need only one.
+ vector<Lease6Ptr> leases = createLeases6();
+
+ // Now add leases with increasing DUID size can be retrieved.
+ // For speed, go from 0 to 128 is steps of 16.
+ int duid_max = DUID::MAX_DUID_LEN;
+ EXPECT_EQ(128, duid_max);
+
+ int duid_min = DUID::MIN_DUID_LEN;
+ EXPECT_EQ(1, duid_min);
+
+ for (uint8_t i = duid_min; i <= duid_max; i += 16) {
+ vector<uint8_t> duid_vec(i, i);
+ leases[1]->duid_.reset(new DUID(duid_vec));
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ Lease6Ptr returned = lmptr_->getLease6(leasetype6_[1], *leases[1]->duid_,
+ leases[1]->iaid_,
+ leases[1]->subnet_id_);
+ ASSERT_TRUE(returned);
+ detailCompareLease(leases[1], returned);
+ ASSERT_TRUE(lmptr_->deleteLease(leases[1]));
+ }
+
+ // Don't bother to check DUIDs longer than the maximum - these cannot be
+ // constructed, and that limitation is tested in the DUID/Client ID unit
+ // tests.
+}
+
+void
+GenericLeaseMgrTest::testUpdateLease4() {
+ // Get the leases to be used for the test and add them to the database.
+ vector<Lease4Ptr> leases = createLeases4();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+ lmptr_->commit();
+
+ Lease4Ptr l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ // Modify some fields in lease 1 (not the address) and update it.
+ ++leases[1]->subnet_id_;
+ leases[1]->valid_lft_ *= 2;
+ leases[1]->hostname_ = "modified.hostname.";
+ leases[1]->fqdn_fwd_ = !leases[1]->fqdn_fwd_;
+ leases[1]->fqdn_rev_ = !leases[1]->fqdn_rev_;;
+ leases[1]->setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+ lmptr_->updateLease4(leases[1]);
+
+ // ... and check what is returned is what is expected.
+ l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ // Alter the lease again and check.
+ ++leases[1]->subnet_id_;
+ leases[1]->cltt_ += 6;
+ leases[1]->setContext(Element::fromJSON("{ \"foo\": \"bar\" }"));
+ lmptr_->updateLease4(leases[1]);
+
+ // Explicitly clear the returned pointer before getting new data to ensure
+ // that the new data is returned.
+ l_returned.reset();
+ l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ // Check we can do an update without changing data.
+ lmptr_->updateLease4(leases[1]);
+ l_returned.reset();
+ l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ // Try to update the lease with the too long hostname.
+ leases[1]->hostname_.assign(256, 'a');
+ EXPECT_THROW(lmptr_->updateLease4(leases[1]), isc::db::DbOperationError);
+
+ // Try updating a lease not in the database.
+ ASSERT_TRUE(lmptr_->deleteLease(leases[2]));
+ EXPECT_THROW(lmptr_->updateLease4(leases[2]), isc::dhcp::NoSuchLease);
+}
+
+void
+GenericLeaseMgrTest::testConcurrentUpdateLease4() {
+ // Get the leases to be used for the test and add them to the database.
+ vector<Lease4Ptr> leases = createLeases4();
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+ lmptr_->commit();
+
+ Lease4Ptr l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ // Save initial lease to be used for concurrent update
+ Lease4Ptr initialLease = boost::make_shared<Lease4>(*leases[1]);
+ detailCompareLease(leases[1], initialLease);
+
+ // Modify some fields in lease 1 (not the address) and update it.
+ ++leases[1]->subnet_id_;
+ leases[1]->valid_lft_ *= 2;
+ leases[1]->hostname_ = "modified.hostname.";
+ leases[1]->fqdn_fwd_ = !leases[1]->fqdn_fwd_;
+ leases[1]->fqdn_rev_ = !leases[1]->fqdn_rev_;;
+ leases[1]->setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+ lmptr_->updateLease4(leases[1]);
+
+ // ... and check what is returned is what is expected.
+ l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ // Concurrently updating lease should fail
+ EXPECT_THROW(lmptr_->updateLease4(initialLease), isc::dhcp::NoSuchLease);
+}
+
+void
+GenericLeaseMgrTest::testUpdateLease6() {
+ // Get the leases to be used for the test.
+ vector<Lease6Ptr> leases = createLeases6();
+ ASSERT_LE(3, leases.size()); // Expect to access leases 0 through 2
+
+ // Add a lease to the database and check that the lease is there.
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ lmptr_->commit();
+
+ Lease6Ptr l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ // Modify some fields in lease 1 (not the address) and update it.
+ ++leases[1]->iaid_;
+ leases[1]->type_ = Lease::TYPE_PD;
+ leases[1]->valid_lft_ *= 2;
+ leases[1]->hostname_ = "modified.hostname.v6.";
+ leases[1]->fqdn_fwd_ = !leases[1]->fqdn_fwd_;
+ leases[1]->fqdn_rev_ = !leases[1]->fqdn_rev_;;
+ leases[1]->setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+ lmptr_->updateLease6(leases[1]);
+
+ // ... and check what is returned is what is expected.
+ l_returned.reset();
+ l_returned = lmptr_->getLease6(Lease::TYPE_PD, ioaddress6_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ // Alter the lease again and check.
+ ++leases[1]->iaid_;
+ leases[1]->type_ = Lease::TYPE_TA;
+ leases[1]->cltt_ += 6;
+ leases[1]->prefixlen_ = 93;
+ leases[1]->setContext(Element::fromJSON("{ \"foo\": \"bar\" }"));
+ lmptr_->updateLease6(leases[1]);
+
+ l_returned.reset();
+ l_returned = lmptr_->getLease6(Lease::TYPE_TA, ioaddress6_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ // Check we can do an update without changing data.
+ lmptr_->updateLease6(leases[1]);
+ l_returned.reset();
+ l_returned = lmptr_->getLease6(Lease::TYPE_TA, ioaddress6_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ // Try to update the lease with the too long hostname.
+ leases[1]->hostname_.assign(256, 'a');
+ EXPECT_THROW(lmptr_->updateLease6(leases[1]), isc::db::DbOperationError);
+
+ // Try updating a lease not in the database.
+ EXPECT_THROW(lmptr_->updateLease6(leases[2]), isc::dhcp::NoSuchLease);
+}
+
+void
+GenericLeaseMgrTest::testConcurrentUpdateLease6() {
+ // Get the leases to be used for the test.
+ vector<Lease6Ptr> leases = createLeases6();
+ ASSERT_LE(3, leases.size()); // Expect to access leases 0 through 2
+
+ // Add a lease to the database and check that the lease is there.
+ EXPECT_TRUE(lmptr_->addLease(leases[1]));
+ lmptr_->commit();
+
+ Lease6Ptr l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ // Save initial lease to be used for concurrent update
+ Lease6Ptr initialLease = boost::make_shared<Lease6>(*leases[1]);
+ detailCompareLease(leases[1], initialLease);
+
+ // Modify some fields in lease 1 (not the address) and update it.
+ ++leases[1]->iaid_;
+ leases[1]->type_ = Lease::TYPE_PD;
+ leases[1]->valid_lft_ *= 2;
+ leases[1]->hostname_ = "modified.hostname.v6.";
+ leases[1]->fqdn_fwd_ = !leases[1]->fqdn_fwd_;
+ leases[1]->fqdn_rev_ = !leases[1]->fqdn_rev_;;
+ leases[1]->setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+ lmptr_->updateLease6(leases[1]);
+
+ // ... and check what is returned is what is expected.
+ l_returned.reset();
+ l_returned = lmptr_->getLease6(Lease::TYPE_PD, ioaddress6_[1]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(leases[1], l_returned);
+
+ // Concurrently updating lease should fail
+ EXPECT_THROW(lmptr_->updateLease6(initialLease), isc::dhcp::NoSuchLease);
+}
+
+void
+GenericLeaseMgrTest::testRecreateLease4() {
+ // Create a lease.
+ std::vector<Lease4Ptr> leases = createLeases4();
+ // Copy the lease so as we can freely modify it.
+ Lease4Ptr lease(new Lease4(*leases[0]));
+
+ // Add a lease.
+ EXPECT_TRUE(lmptr_->addLease(lease));
+ lmptr_->commit();
+
+ // Check that the lease has been successfully added.
+ Lease4Ptr l_returned = lmptr_->getLease4(ioaddress4_[0]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(lease, l_returned);
+
+ // Delete a lease, check that it's gone.
+ EXPECT_TRUE(lmptr_->deleteLease(leases[0]));
+ EXPECT_FALSE(lmptr_->getLease4(ioaddress4_[0]));
+
+ // Modify the copy of the lease. Increasing values or negating them ensures
+ // that they are really modified, because we will never get the same values.
+ ++lease->subnet_id_;
+ ++lease->valid_lft_;
+ lease->fqdn_fwd_ = !lease->fqdn_fwd_;
+ // Make sure that the lease has been really modified.
+ ASSERT_NE(*lease, *leases[0]);
+ // Add the updated lease.
+ EXPECT_TRUE(lmptr_->addLease(lease));
+ lmptr_->commit();
+
+ // Reopen the lease database, so as the lease is re-read.
+ reopen(V4);
+
+ // The lease in the database should be modified.
+ l_returned = lmptr_->getLease4(ioaddress4_[0]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(lease, l_returned);
+}
+
+void
+GenericLeaseMgrTest::testRecreateLease6() {
+ // Create a lease.
+ std::vector<Lease6Ptr> leases = createLeases6();
+ // Copy the lease so as we can freely modify it.
+ Lease6Ptr lease(new Lease6(*leases[0]));
+
+ // Add a lease.
+ EXPECT_TRUE(lmptr_->addLease(lease));
+ lmptr_->commit();
+
+ // Check that the lease has been successfully added.
+ Lease6Ptr l_returned = lmptr_->getLease6(Lease::TYPE_NA, ioaddress6_[0]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(lease, l_returned);
+
+ // Delete a lease, check that it's gone.
+ EXPECT_TRUE(lmptr_->deleteLease(leases[0]));
+ EXPECT_FALSE(lmptr_->getLease6(Lease::TYPE_NA, ioaddress6_[0]));
+
+ // Modify the copy of the lease. Increasing values or negating them ensures
+ // that they are really modified, because we will never get the same values.
+ ++lease->subnet_id_;
+ ++lease->valid_lft_;
+ lease->fqdn_fwd_ = !lease->fqdn_fwd_;
+ // Make sure that the lease has been really modified.
+ ASSERT_NE(*lease, *leases[0]);
+ // Add the updated lease.
+ EXPECT_TRUE(lmptr_->addLease(lease));
+ lmptr_->commit();
+
+ // Reopen the lease database, so as the lease is re-read.
+ reopen(V6);
+
+ // The lease in the database should be modified.
+ l_returned = lmptr_->getLease6(Lease::TYPE_NA, ioaddress6_[0]);
+ ASSERT_TRUE(l_returned);
+ detailCompareLease(lease, l_returned);
+}
+
+void
+GenericLeaseMgrTest::testNullDuid() {
+ // Create leases, although we need only one.
+ vector<Lease6Ptr> leases = createLeases6();
+
+ // Set DUID to empty pointer.
+ leases[1]->duid_.reset();
+
+ // Insert should throw.
+ ASSERT_THROW(lmptr_->addLease(leases[1]), DbOperationError);
+}
+
+void
+GenericLeaseMgrTest::testVersion(int major, int minor) {
+ EXPECT_EQ(major, lmptr_->getVersion().first);
+ EXPECT_EQ(minor, lmptr_->getVersion().second);
+}
+
+void
+GenericLeaseMgrTest::testGetExpiredLeases4() {
+ // Get the leases to be used for the test.
+ vector<Lease4Ptr> leases = createLeases4();
+ // Make sure we have at least 6 leases there.
+ ASSERT_GE(leases.size(), 6);
+
+ // Use the same current time for all leases.
+ time_t current_time = time(NULL);
+
+ // Add them to the database
+ for (size_t i = 0; i < leases.size(); ++i) {
+ // Mark every other lease as expired.
+ if (i % 2 == 0) {
+ // Set client last transmission time to the value older than the
+ // valid lifetime to make it expired. The expiration time also
+ // depends on the lease index, so as we can later check that the
+ // leases are ordered by the expiration time.
+ leases[i]->cltt_ = current_time - leases[i]->valid_lft_ - 10 - i;
+
+ } else {
+ // Set current time as cltt for remaining leases. These leases are
+ // not expired.
+ leases[i]->cltt_ = current_time;
+ }
+ ASSERT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // Retrieve at most 1000 expired leases.
+ Lease4Collection expired_leases;
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases4(expired_leases, 1000));
+ // Leases with even indexes should be returned as expired.
+ ASSERT_EQ(static_cast<size_t>(leases.size() / 2), expired_leases.size());
+
+ // The expired leases should be returned from the most to least expired.
+ // This matches the reverse order to which they have been added.
+ for (Lease4Collection::reverse_iterator lease = expired_leases.rbegin();
+ lease != expired_leases.rend(); ++lease) {
+ int index = static_cast<int>(std::distance(expired_leases.rbegin(), lease));
+ // Multiple current index by two, because only leases with even indexes
+ // should have been returned.
+ ASSERT_LE(2 * index, leases.size());
+ EXPECT_EQ(leases[2 * index]->addr_, (*lease)->addr_);
+ }
+
+ // Update current time for the next test.
+ current_time = time(NULL);
+ // Also, remove expired leases collected during the previous test.
+ expired_leases.clear();
+
+ // This time let's reverse the expiration time and see if they will be returned
+ // in the correct order.
+ for (int i = 0; i < leases.size(); ++i) {
+ // Update the time of expired leases with even indexes.
+ if (i % 2 == 0) {
+ leases[i]->cltt_ = current_time - leases[i]->valid_lft_ - 1000 + i;
+ } else {
+ // Make sure remaining leases remain unexpired.
+ leases[i]->cltt_ = current_time + 100;
+ }
+ ASSERT_NO_THROW(lmptr_->updateLease4(leases[i]));
+ }
+
+ // Retrieve expired leases again. The limit of 0 means return all expired
+ // leases.
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases4(expired_leases, 0));
+ // The same leases should be returned.
+ ASSERT_EQ(static_cast<size_t>(leases.size() / 2), expired_leases.size());
+
+ // This time leases should be returned in the non-reverse order.
+ for (Lease4Collection::iterator lease = expired_leases.begin();
+ lease != expired_leases.end(); ++lease) {
+ int index = static_cast<int>(std::distance(expired_leases.begin(), lease));
+ ASSERT_LE(2 * index, leases.size());
+ EXPECT_EQ(leases[2 * index]->addr_, (*lease)->addr_);
+ }
+
+ // Remember expired leases returned.
+ std::vector<Lease4Ptr> saved_expired_leases = expired_leases;
+
+ // Remove expired leases again.
+ expired_leases.clear();
+
+ // Limit the number of leases to be returned to 2.
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases4(expired_leases, 2));
+
+ // Make sure we have exactly 2 leases returned.
+ ASSERT_EQ(2, expired_leases.size());
+
+ // Test that most expired leases have been returned.
+ for (Lease4Collection::iterator lease = expired_leases.begin();
+ lease != expired_leases.end(); ++lease) {
+ int index = static_cast<int>(std::distance(expired_leases.begin(), lease));
+ ASSERT_LE(2 * index, leases.size());
+ EXPECT_EQ(leases[2 * index]->addr_, (*lease)->addr_);
+ }
+
+ // Mark every other expired lease as reclaimed.
+ for (int i = 0; i < saved_expired_leases.size(); ++i) {
+ if (i % 2 != 0) {
+ saved_expired_leases[i]->state_ = Lease::STATE_EXPIRED_RECLAIMED;
+ }
+ ASSERT_NO_THROW(lmptr_->updateLease4(saved_expired_leases[i]));
+ }
+
+ expired_leases.clear();
+
+ // This the returned leases should exclude reclaimed ones. So the number
+ // of returned leases should be roughly half of the expired leases.
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases4(expired_leases, 0));
+ ASSERT_EQ(static_cast<size_t>(saved_expired_leases.size() / 2),
+ expired_leases.size());
+
+ // Make sure that returned leases are those that are not reclaimed, i.e.
+ // those that have even index.
+ for (Lease4Collection::iterator lease = expired_leases.begin();
+ lease != expired_leases.end(); ++lease) {
+ int index = static_cast<int>(std::distance(expired_leases.begin(), lease));
+ EXPECT_EQ(saved_expired_leases[2 * index]->addr_, (*lease)->addr_);
+ }
+}
+
+void
+GenericLeaseMgrTest::testGetExpiredLeases6() {
+ // Get the leases to be used for the test.
+ vector<Lease6Ptr> leases = createLeases6();
+ // Make sure we have at least 6 leases there.
+ ASSERT_GE(leases.size(), 6);
+
+ // Use the same current time for all leases.
+ time_t current_time = time(NULL);
+
+ // Add them to the database
+ for (size_t i = 0; i < leases.size(); ++i) {
+ // Mark every other lease as expired.
+ if (i % 2 == 0) {
+ // Set client last transmission time to the value older than the
+ // valid lifetime to make it expired. The expiration time also
+ // depends on the lease index, so as we can later check that the
+ // leases are ordered by the expiration time.
+ leases[i]->cltt_ = current_time - leases[i]->valid_lft_ - 10 - i;
+
+ } else {
+ // Set current time as cltt for remaining leases. These leases are
+ // not expired.
+ leases[i]->cltt_ = current_time;
+ }
+ ASSERT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // Retrieve at most 1000 expired leases.
+ Lease6Collection expired_leases;
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases6(expired_leases, 1000));
+ // Leases with even indexes should be returned as expired.
+ ASSERT_EQ(static_cast<size_t>(leases.size() / 2), expired_leases.size());
+
+ // The expired leases should be returned from the most to least expired.
+ // This matches the reverse order to which they have been added.
+ for (Lease6Collection::reverse_iterator lease = expired_leases.rbegin();
+ lease != expired_leases.rend(); ++lease) {
+ int index = static_cast<int>(std::distance(expired_leases.rbegin(), lease));
+ // Multiple current index by two, because only leases with even indexes
+ // should have been returned.
+ EXPECT_EQ(leases[2 * index]->addr_, (*lease)->addr_);
+ }
+
+ // Update current time for the next test.
+ current_time = time(NULL);
+ // Also, remove expired leases collected during the previous test.
+ expired_leases.clear();
+
+ // This time let's reverse the expiration time and see if they will be returned
+ // in the correct order.
+ for (int i = 0; i < leases.size(); ++i) {
+ // Update the time of expired leases with even indexes.
+ if (i % 2 == 0) {
+ leases[i]->cltt_ = current_time - leases[i]->valid_lft_ - 1000 + i;
+
+ } else {
+ // Make sure remaining leases remain unexpired.
+ leases[i]->cltt_ = current_time + 100;
+ }
+ ASSERT_NO_THROW(lmptr_->updateLease6(leases[i]));
+ }
+
+ // Retrieve expired leases again. The limit of 0 means return all expired
+ // leases.
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases6(expired_leases, 0));
+ // The same leases should be returned.
+ ASSERT_EQ(static_cast<size_t>(leases.size() / 2), expired_leases.size());
+
+ // This time leases should be returned in the non-reverse order.
+ for (Lease6Collection::iterator lease = expired_leases.begin();
+ lease != expired_leases.end(); ++lease) {
+ int index = static_cast<int>(std::distance(expired_leases.begin(), lease));
+ EXPECT_EQ(leases[2 * index]->addr_, (*lease)->addr_);
+ }
+
+ // Remember expired leases returned.
+ std::vector<Lease6Ptr> saved_expired_leases = expired_leases;
+
+ // Remove expired leases again.
+ expired_leases.clear();
+
+ // Limit the number of leases to be returned to 2.
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases6(expired_leases, 2));
+
+ // Make sure we have exactly 2 leases returned.
+ ASSERT_EQ(2, expired_leases.size());
+
+ // Test that most expired leases have been returned.
+ for (Lease6Collection::iterator lease = expired_leases.begin();
+ lease != expired_leases.end(); ++lease) {
+ int index = static_cast<int>(std::distance(expired_leases.begin(), lease));
+ EXPECT_EQ(leases[2 * index]->addr_, (*lease)->addr_);
+ }
+
+ // Mark every other expired lease as reclaimed.
+ for (int i = 0; i < saved_expired_leases.size(); ++i) {
+ if (i % 2 != 0) {
+ saved_expired_leases[i]->state_ = Lease::STATE_EXPIRED_RECLAIMED;
+ }
+ ASSERT_NO_THROW(lmptr_->updateLease6(saved_expired_leases[i]));
+ }
+
+ expired_leases.clear();
+
+ // This the returned leases should exclude reclaimed ones. So the number
+ // of returned leases should be roughly half of the expired leases.
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases6(expired_leases, 0));
+ ASSERT_EQ(static_cast<size_t>(saved_expired_leases.size() / 2),
+ expired_leases.size());
+
+ // Make sure that returned leases are those that are not reclaimed, i.e.
+ // those that have even index.
+ for (Lease6Collection::iterator lease = expired_leases.begin();
+ lease != expired_leases.end(); ++lease) {
+ int index = static_cast<int>(std::distance(expired_leases.begin(), lease));
+ EXPECT_EQ(saved_expired_leases[2 * index]->addr_, (*lease)->addr_);
+ }
+}
+
+void
+GenericLeaseMgrTest::testInfiniteAreNotExpired4() {
+ // Get the leases to be used for the test.
+ vector<Lease4Ptr> leases = createLeases4();
+ Lease4Ptr lease = leases[1];
+
+ // Set valid_lft_ to infinite. Leave the small cltt even it won't happen
+ // in the real world to catch more possible issues.
+ lease->valid_lft_ = Lease::INFINITY_LFT;
+
+ // Add it to the database.
+ ASSERT_TRUE(lmptr_->addLease(leases[1]));
+
+ // Retrieve at most 10 expired leases.
+ Lease4Collection expired_leases;
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases4(expired_leases, 10));
+
+ // No lease should be returned.
+ EXPECT_EQ(0, expired_leases.size());
+}
+
+void
+GenericLeaseMgrTest::testInfiniteAreNotExpired6() {
+ // Get the leases to be used for the test.
+ vector<Lease6Ptr> leases = createLeases6();
+ Lease6Ptr lease = leases[1];
+
+ // Set valid_lft_ to infinite. Leave the small cltt even it won't happen
+ // in the real world to catch more possible issues.
+ lease->valid_lft_ = Lease::INFINITY_LFT;
+
+ // Add it to the database.
+ ASSERT_TRUE(lmptr_->addLease(leases[1]));
+
+ // Retrieve at most 10 expired leases.
+ Lease6Collection expired_leases;
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases6(expired_leases, 10));
+
+ // No lease should be returned.
+ EXPECT_EQ(0, expired_leases.size());
+}
+
+void
+GenericLeaseMgrTest::testDeleteExpiredReclaimedLeases4() {
+ // Get the leases to be used for the test.
+ vector<Lease4Ptr> leases = createLeases4();
+ // Make sure we have at least 6 leases there.
+ ASSERT_GE(leases.size(), 6);
+
+ time_t current_time = time(NULL);
+
+ // Add them to the database
+ for (size_t i = 0; i < leases.size(); ++i) {
+ // Mark every other lease as expired.
+ if (i % 2 == 0) {
+ // Set client last transmission time to the value older than the
+ // valid lifetime to make it expired. We also substract the value
+ // of 10, 20, 30, 40 etc, depending on the lease index. This
+ // simulates different expiration times for various leases.
+ leases[i]->cltt_ = current_time - leases[i]->valid_lft_ - i * 10;
+ // Set reclaimed state.
+ leases[i]->state_ = Lease::STATE_EXPIRED_RECLAIMED;
+
+ } else {
+ // Other leases are left as not expired - client last transmission
+ // time set to current time.
+ leases[i]->cltt_ = current_time;
+ }
+ ASSERT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // Keep reclaimed lease for 15 seconds after expiration.
+ const uint32_t lease_affinity_time = 15;
+
+ // Delete expired and reclaimed leases which have expired earlier than
+ // 15 seconds ago. This should affect leases with index 2, 3, 4 etc.
+ uint64_t deleted_num;
+ uint64_t should_delete_num = 0;
+ ASSERT_NO_THROW(
+ deleted_num = lmptr_->deleteExpiredReclaimedLeases4(lease_affinity_time)
+ );
+
+ for (size_t i = 0; i < leases.size(); ++i) {
+ // Obtain lease from the server.
+ Lease4Ptr lease = lmptr_->getLease4(leases[i]->addr_);
+
+ // If the lease is reclaimed and the expiration time passed more than
+ // 15 seconds ago, the lease should have been deleted.
+ if (leases[i]->stateExpiredReclaimed() &&
+ ((leases[i]->getExpirationTime() + lease_affinity_time) < current_time)) {
+ EXPECT_FALSE(lease) << "The following lease should have been"
+ " deleted: " << leases[i]->toText();
+ ++should_delete_num;
+ } else {
+ // If the lease is not reclaimed or it has expired less than
+ // 15 seconds ago, the lease should still be there.
+ EXPECT_TRUE(lease) << "The following lease shouldn't have been"
+ " deleted: " << leases[i]->toText();
+ }
+ }
+
+ // Check that the number of leases deleted is correct.
+ EXPECT_EQ(deleted_num, should_delete_num);
+
+ // Make sure we can make another attempt, when there are no more leases
+ // to be deleted.
+ ASSERT_NO_THROW(
+ deleted_num = lmptr_->deleteExpiredReclaimedLeases4(lease_affinity_time)
+ );
+ // No lease should have been deleted.
+ EXPECT_EQ(0, deleted_num);
+
+ // Reopen the database. This to ensure that the leases have been deleted
+ // from the persistent storage.
+ reopen(V4);
+
+ for (size_t i = 0; i < leases.size(); ++i) {
+ /// @todo Leases with empty HW address are dropped by the memfile
+ /// backend. We will have to reevaluate if this is right behavior
+ /// of the backend when client identifier is present.
+ if (leases[i]->hwaddr_ && leases[i]->hwaddr_->hwaddr_.empty()) {
+ continue;
+ }
+ // Obtain lease from the server.
+ Lease4Ptr lease = lmptr_->getLease4(leases[i]->addr_);
+
+ // If the lease is reclaimed and the expiration time passed more than
+ // 15 seconds ago, the lease should have been deleted.
+ if (leases[i]->stateExpiredReclaimed() &&
+ ((leases[i]->getExpirationTime() + lease_affinity_time) < current_time)) {
+ EXPECT_FALSE(lease) << "The following lease should have been"
+ " deleted: " << leases[i]->toText();
+
+ } else {
+ // If the lease is not reclaimed or it has expired less than
+ // 15 seconds ago, the lease should still be there.
+ EXPECT_TRUE(lease) << "The following lease shouldn't have been"
+ " deleted: " << leases[i]->toText();
+ }
+ }
+}
+
+void
+GenericLeaseMgrTest::testDeleteExpiredReclaimedLeases6() {
+ // Get the leases to be used for the test.
+ vector<Lease6Ptr> leases = createLeases6();
+ // Make sure we have at least 6 leases there.
+ ASSERT_GE(leases.size(), 6);
+
+ time_t current_time = time(NULL);
+
+ // Add them to the database
+ for (size_t i = 0; i < leases.size(); ++i) {
+ // Mark every other lease as expired.
+ if (i % 2 == 0) {
+ // Set client last transmission time to the value older than the
+ // valid lifetime to make it expired. We also substract the value
+ // of 10, 20, 30, 40 etc, depending on the lease index. This
+ // simulates different expiration times for various leases.
+ leases[i]->cltt_ = current_time - leases[i]->valid_lft_ - i * 10;
+ // Set reclaimed state.
+ leases[i]->state_ = Lease::STATE_EXPIRED_RECLAIMED;
+
+ } else {
+ // Other leases are left as not expired - client last transmission
+ // time set to current time.
+ leases[i]->cltt_ = current_time;
+ }
+ ASSERT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // Keep reclaimed lease for 15 seconds after expiration.
+ const uint32_t lease_affinity_time = 15;
+
+ // Delete expired and reclaimed leases which have expired earlier than
+ // 15 seconds ago. This should affect leases with index 2, 3, 4 etc.
+ uint64_t deleted_num;
+ uint64_t should_delete_num = 0;
+ ASSERT_NO_THROW(
+ deleted_num = lmptr_->deleteExpiredReclaimedLeases6(lease_affinity_time)
+ );
+
+ for (size_t i = 0; i < leases.size(); ++i) {
+ // Obtain lease from the server.
+ Lease6Ptr lease = lmptr_->getLease6(leases[i]->type_, leases[i]->addr_);
+
+ // If the lease is reclaimed and the expiration time passed more than
+ // 15 seconds ago, the lease should have been deleted.
+ if (leases[i]->stateExpiredReclaimed() &&
+ ((leases[i]->getExpirationTime() + lease_affinity_time) < current_time)) {
+ EXPECT_FALSE(lease) << "The following lease should have been"
+ " deleted: " << leases[i]->toText();
+ ++should_delete_num;
+
+ } else {
+ // If the lease is not reclaimed or it has expired less than
+ // 15 seconds ago, the lease should still be there.
+ EXPECT_TRUE(lease) << "The following lease shouldn't have been"
+ " deleted: " << leases[i]->toText();
+ }
+ }
+ // Check that the number of deleted leases is correct.
+ EXPECT_EQ(should_delete_num, deleted_num);
+
+ // Make sure we can make another attempt, when there are no more leases
+ // to be deleted.
+ ASSERT_NO_THROW(
+ deleted_num = lmptr_->deleteExpiredReclaimedLeases6(lease_affinity_time)
+ );
+ // No lease should have been deleted.
+ EXPECT_EQ(0, deleted_num);
+
+ // Reopen the database. This to ensure that the leases have been deleted
+ // from the persistent storage.
+ reopen(V6);
+
+ for (size_t i = 0; i < leases.size(); ++i) {
+ // Obtain lease from the server.
+ Lease6Ptr lease = lmptr_->getLease6(leases[i]->type_, leases[i]->addr_);
+
+ // If the lease is reclaimed and the expiration time passed more than
+ // 15 seconds ago, the lease should have been deleted.
+ if (leases[i]->stateExpiredReclaimed() &&
+ ((leases[i]->getExpirationTime() + lease_affinity_time) < current_time)) {
+ EXPECT_FALSE(lease) << "The following lease should have been"
+ " deleted: " << leases[i]->toText();
+
+ } else {
+ // If the lease is not reclaimed or it has expired less than
+ // 15 seconds ago, the lease should still be there.
+ EXPECT_TRUE(lease) << "The following lease shouldn't have been"
+ " deleted: " << leases[i]->toText();
+ }
+ }
+}
+
+void
+GenericLeaseMgrTest::testGetDeclinedLeases4() {
+ // Get the leases to be used for the test.
+ vector<Lease4Ptr> leases = createLeases4();
+
+ // Make sure we have at least 8 leases there.
+ ASSERT_GE(leases.size(), 8);
+
+ // Use the same current time for all leases.
+ time_t current_time = time(NULL);
+
+ // Add them to the database
+ for (size_t i = 0; i < leases.size(); ++i) {
+
+ // Mark the first half of the leases as DECLINED
+ if (i < leases.size()/2) {
+ // Mark as declined with 1000 seconds of probation-period
+ leases[i]->decline(1000);
+ }
+
+ // Mark every second lease as expired.
+ if (i % 2 == 0) {
+ // Set client last transmission time to the value older than the
+ // valid lifetime to make it expired. The expiration time also
+ // depends on the lease index, so as we can later check that the
+ // leases are ordered by the expiration time.
+ leases[i]->cltt_ = current_time - leases[i]->valid_lft_ - 10 - i;
+
+ } else {
+ // Set current time as cltt for remaining leases. These leases are
+ // not expired.
+ leases[i]->cltt_ = current_time;
+ }
+
+ ASSERT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // The leases are:
+ // 0 - declined, expired
+ // 1 - declined, not expired
+ // 2 - declined, expired
+ // 3 - declined, not expired
+ // 4 - default, expired
+ // 5 - default, not expired
+ // 6 - default, expired
+ // 7 - default, not expired
+
+ // Retrieve at most 1000 expired leases.
+ Lease4Collection expired_leases;
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases4(expired_leases, 1000));
+
+ // Leases with even indexes should be returned as expired. It shouldn't
+ // matter if the state is default or expired. The getExpiredLeases4 does
+ // not pay attention to state, just expiration timers.
+ ASSERT_EQ(static_cast<size_t>(leases.size() / 2), expired_leases.size());
+
+ unsigned int declined_state = 0;
+ unsigned int default_state = 0;
+
+ // The expired leases should be returned from the most to least expired.
+ // This matches the reverse order to which they have been added.
+ for (Lease4Collection::reverse_iterator lease = expired_leases.rbegin();
+ lease != expired_leases.rend(); ++lease) {
+ int index = static_cast<int>(std::distance(expired_leases.rbegin(), lease));
+ // Multiple current index by two, because only leases with even indexes
+ // should have been returned.
+ EXPECT_EQ(leases[2 * index]->addr_, (*lease)->addr_);
+
+ // Count leases in default and declined states
+ if ((*lease)->state_ == Lease::STATE_DEFAULT) {
+ default_state++;
+ } else if ((*lease)->state_ == Lease::STATE_DECLINED) {
+ declined_state++;
+ }
+ }
+
+ // LeaseMgr is supposed to return both default and declined leases
+ EXPECT_NE(0, declined_state);
+ EXPECT_NE(0, default_state);
+
+ // Update current time for the next test.
+ current_time = time(NULL);
+ // Also, remove expired leases collected during the previous test.
+ expired_leases.clear();
+
+ // This time let's reverse the expiration time and see if they will be returned
+ // in the correct order.
+ leases = createLeases4();
+ for (int i = 0; i < leases.size(); ++i) {
+
+ // Mark the second half of the leases as DECLINED
+ if (i >= leases.size()/2) {
+ // Mark as declined with 1000 seconds of probation-period
+ leases[i]->decline(1000);
+ }
+
+ // Update the time of expired leases with even indexes.
+ if (i % 2 == 0) {
+ leases[i]->cltt_ = current_time - leases[i]->valid_lft_ - 1000 + i;
+
+ } else {
+ // Make sure remaining leases remain unexpired.
+ leases[i]->cltt_ = current_time + 100;
+ }
+ ASSERT_NO_THROW(lmptr_->updateLease4(leases[i]));
+ }
+
+ // Retrieve expired leases again. The limit of 0 means return all expired
+ // leases.
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases4(expired_leases, 0));
+ // The same leases should be returned.
+ ASSERT_EQ(static_cast<size_t>(leases.size() / 2), expired_leases.size());
+
+ // This time leases should be returned in the non-reverse order.
+ declined_state = 0;
+ default_state = 0;
+ for (Lease4Collection::iterator lease = expired_leases.begin();
+ lease != expired_leases.end(); ++lease) {
+ int index = static_cast<int>(std::distance(expired_leases.begin(), lease));
+ EXPECT_EQ(leases[2 * index]->addr_, (*lease)->addr_);
+
+ // Count leases in default and declined states
+ if ((*lease)->state_ == Lease::STATE_DEFAULT) {
+ default_state++;
+ } else if ((*lease)->state_ == Lease::STATE_DECLINED) {
+ declined_state++;
+ }
+ }
+
+ // Check that both declined and default state leases were returned.
+ EXPECT_NE(0, declined_state);
+ EXPECT_NE(0, default_state);
+
+ // Remove expired leases again.
+ expired_leases.clear();
+
+ // Limit the number of leases to be returned to 2.
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases4(expired_leases, 2));
+
+ // Make sure we have exactly 2 leases returned.
+ ASSERT_EQ(2, expired_leases.size());
+
+ // Test that most expired leases have been returned.
+ for (Lease4Collection::iterator lease = expired_leases.begin();
+ lease != expired_leases.end(); ++lease) {
+ int index = static_cast<int>(std::distance(expired_leases.begin(), lease));
+ EXPECT_EQ(leases[2 * index]->addr_, (*lease)->addr_);
+ }
+}
+
+void
+GenericLeaseMgrTest::testGetDeclinedLeases6() {
+ // Get the leases to be used for the test.
+ vector<Lease6Ptr> leases = createLeases6();
+
+ // Make sure we have at least 8 leases there.
+ ASSERT_GE(leases.size(), 8);
+
+ // Use the same current time for all leases.
+ time_t current_time = time(NULL);
+
+ // Add them to the database
+ for (size_t i = 0; i < leases.size(); ++i) {
+
+ // Mark the first half of the leases as DECLINED
+ if (i < leases.size()/2) {
+ // Mark as declined with 1000 seconds of probation-period
+ leases[i]->decline(1000);
+ }
+
+ // Mark every second lease as expired.
+ if (i % 2 == 0) {
+ // Set client last transmission time to the value older than the
+ // valid lifetime to make it expired. The expiration time also
+ // depends on the lease index, so as we can later check that the
+ // leases are ordered by the expiration time.
+ leases[i]->cltt_ = current_time - leases[i]->valid_lft_ - 10 - i;
+
+ } else {
+ // Set current time as cltt for remaining leases. These leases are
+ // not expired.
+ leases[i]->cltt_ = current_time;
+ }
+
+ ASSERT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // The leases are:
+ // 0 - declined, expired
+ // 1 - declined, not expired
+ // 2 - declined, expired
+ // 3 - declined, not expired
+ // 4 - default, expired
+ // 5 - default, not expired
+ // 6 - default, expired
+ // 7 - default, not expired
+
+ // Retrieve at most 1000 expired leases.
+ Lease6Collection expired_leases;
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases6(expired_leases, 1000));
+
+ // Leases with even indexes should be returned as expired. It shouldn't
+ // matter if the state is default or expired. The getExpiredLeases4 does
+ // not pay attention to state, just expiration timers.
+ ASSERT_EQ(static_cast<size_t>(leases.size() / 2), expired_leases.size());
+
+ unsigned int declined_state = 0;
+ unsigned int default_state = 0;
+
+ // The expired leases should be returned from the most to least expired.
+ // This matches the reverse order to which they have been added.
+ for (Lease6Collection::reverse_iterator lease = expired_leases.rbegin();
+ lease != expired_leases.rend(); ++lease) {
+ int index = static_cast<int>(std::distance(expired_leases.rbegin(), lease));
+ // Multiple current index by two, because only leases with even indexes
+ // should have been returned.
+ EXPECT_EQ(leases[2 * index]->addr_, (*lease)->addr_);
+
+ // Count leases in default and declined states
+ if ((*lease)->state_ == Lease::STATE_DEFAULT) {
+ default_state++;
+ } else if ((*lease)->state_ == Lease::STATE_DECLINED) {
+ declined_state++;
+ }
+ }
+
+ // LeaseMgr is supposed to return both default and declined leases
+ EXPECT_NE(0, declined_state);
+ EXPECT_NE(0, default_state);
+
+ // Update current time for the next test.
+ current_time = time(NULL);
+ // Also, remove expired leases collected during the previous test.
+ expired_leases.clear();
+
+ // This time let's reverse the expiration time and see if they will be returned
+ // in the correct order.
+ leases = createLeases6();
+ for (int i = 0; i < leases.size(); ++i) {
+
+ // Mark the second half of the leases as DECLINED
+ if (i >= leases.size()/2) {
+ // Mark as declined with 1000 seconds of probation-period
+ leases[i]->decline(1000);
+ }
+
+ // Update the time of expired leases with even indexes.
+ if (i % 2 == 0) {
+ leases[i]->cltt_ = current_time - leases[i]->valid_lft_ - 1000 + i;
+
+ } else {
+ // Make sure remaining leases remain unexpired.
+ leases[i]->cltt_ = current_time + 100;
+ }
+ ASSERT_NO_THROW(lmptr_->updateLease6(leases[i]));
+ }
+
+ // Retrieve expired leases again. The limit of 0 means return all expired
+ // leases.
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases6(expired_leases, 0));
+ // The same leases should be returned.
+ ASSERT_EQ(static_cast<size_t>(leases.size() / 2), expired_leases.size());
+
+ // This time leases should be returned in the non-reverse order.
+ declined_state = 0;
+ default_state = 0;
+ for (Lease6Collection::iterator lease = expired_leases.begin();
+ lease != expired_leases.end(); ++lease) {
+ int index = static_cast<int>(std::distance(expired_leases.begin(), lease));
+ EXPECT_EQ(leases[2 * index]->addr_, (*lease)->addr_);
+
+ // Count leases in default and declined states
+ if ((*lease)->state_ == Lease::STATE_DEFAULT) {
+ default_state++;
+ } else if ((*lease)->state_ == Lease::STATE_DECLINED) {
+ declined_state++;
+ }
+ }
+
+ // Check that both declined and default state leases were returned.
+ EXPECT_NE(0, declined_state);
+ EXPECT_NE(0, default_state);
+
+ // Remove expired leases again.
+ expired_leases.clear();
+
+ // Limit the number of leases to be returned to 2.
+ ASSERT_NO_THROW(lmptr_->getExpiredLeases6(expired_leases, 2));
+
+ // Make sure we have exactly 2 leases returned.
+ ASSERT_EQ(2, expired_leases.size());
+
+ // Test that most expired leases have been returned.
+ for (Lease6Collection::iterator lease = expired_leases.begin();
+ lease != expired_leases.end(); ++lease) {
+ int index = static_cast<int>(std::distance(expired_leases.begin(), lease));
+ EXPECT_EQ(leases[2 * index]->addr_, (*lease)->addr_);
+ }
+}
+
+void
+GenericLeaseMgrTest::checkStat(const std::string& name,
+ const int64_t expected_value) {
+ stats::ObservationPtr obs =
+ stats::StatsMgr::instance().getObservation(name);
+
+ ASSERT_TRUE(obs) << " stat: " << name << " not found ";
+ ASSERT_EQ(expected_value, obs->getInteger().first)
+ << " stat: " << name << " value wrong";
+}
+
+void
+GenericLeaseMgrTest::checkLeaseStats(const StatValMapList& expectedStats) {
+ // Global accumulators
+ int64_t declined_addresses = 0;
+ int64_t reclaimed_declined_addresses = 0;
+
+ // Iterate over all stats for each subnet
+ for (int subnet_idx = 0; subnet_idx < expectedStats.size(); ++subnet_idx) {
+ BOOST_FOREACH(StatValPair expectedStat, expectedStats[subnet_idx]) {
+ // Verify the per subnet value.
+ checkStat(stats::StatsMgr::generateName("subnet", subnet_idx+1,
+ expectedStat.first),
+ expectedStat.second);
+
+ // Add the value to globals as needed.
+ if (expectedStat.first == "declined-addresses") {
+ declined_addresses += expectedStat.second;
+ } else if (expectedStat.first == "reclaimed-declined-addresses") {
+ reclaimed_declined_addresses += expectedStat.second;
+ }
+ }
+ }
+
+ // Verify the globals.
+ checkStat("declined-addresses", declined_addresses);
+ checkStat("reclaimed-declined-addresses", reclaimed_declined_addresses);
+}
+
+Lease4Ptr
+GenericLeaseMgrTest::makeLease4(const std::string& address,
+ const SubnetID& subnet_id,
+ const uint32_t state,
+ const ConstElementPtr user_context /* = ConstElementPtr() */) {
+ Lease4Ptr lease(new Lease4());
+
+ // set the address
+ lease->addr_ = IOAddress(address);
+
+ // make a MAC from the address
+ std::vector<uint8_t> hwaddr = lease->addr_.toBytes();
+ hwaddr.push_back(0);
+ hwaddr.push_back(0);
+
+ lease->hwaddr_.reset(new HWAddr(hwaddr, HTYPE_ETHER));
+ lease->valid_lft_ = 86400;
+ lease->cltt_ = 168256;
+ lease->subnet_id_ = subnet_id;
+ lease->state_ = state;
+ if (user_context) {
+ lease->setContext(user_context);
+ }
+
+ EXPECT_TRUE(lmptr_->addLease(lease));
+ return lease;
+}
+
+Lease6Ptr
+GenericLeaseMgrTest::makeLease6(const Lease::Type& type,
+ const std::string& address,
+ uint8_t prefix_len,
+ const SubnetID& subnet_id,
+ const uint32_t state,
+ const ConstElementPtr user_context /* = ConstElementPtr() */) {
+ IOAddress addr(address);
+
+ // make a DUID from the address
+ std::vector<uint8_t> bytes = addr.toBytes();
+ bytes.push_back(prefix_len);
+
+ Lease6Ptr lease(new Lease6(type, addr, DuidPtr(new DUID(bytes)), 77,
+ 16000, 24000, subnet_id, HWAddrPtr(),
+ prefix_len));
+ lease->state_ = state;
+ if (user_context) {
+ lease->setContext(user_context);
+ }
+
+ EXPECT_TRUE(lmptr_->addLease(lease));
+ return lease;
+}
+
+void
+GenericLeaseMgrTest::testRecountLeaseStats4() {
+ using namespace stats;
+
+ StatsMgr::instance().removeAll();
+
+ // Create two subnets.
+ int num_subnets = 2;
+ CfgSubnets4Ptr cfg = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ Subnet4Ptr subnet;
+ Pool4Ptr pool;
+
+ subnet.reset(new Subnet4(IOAddress("192.0.1.0"), 24, 1, 2, 3, 1));
+ pool.reset(new Pool4(IOAddress("192.0.1.0"), 24));
+ subnet->addPool(pool);
+ cfg->add(subnet);
+
+ subnet.reset(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3, 2));
+ pool.reset(new Pool4(IOAddress("192.0.2.0"), 24));
+ subnet->addPool(pool);
+ cfg->add(subnet);
+
+
+ ASSERT_NO_THROW(CfgMgr::instance().commit());
+
+ // Create the expected stats list. At this point, the only stat
+ // that should be non-zero is total-addresses.
+ StatValMapList expectedStats(num_subnets);
+ for (int i = 0; i < num_subnets; ++i) {
+ expectedStats[i]["total-addresses"] = 256;
+ expectedStats[i]["assigned-addresses"] = 0;
+ expectedStats[i]["declined-addresses"] = 0;
+ expectedStats[i]["reclaimed-declined-addresses"] = 0;
+ expectedStats[i]["reclaimed-leases"] = 0;
+ }
+
+ // Make sure stats are as expected.
+ ASSERT_NO_FATAL_FAILURE(checkLeaseStats(expectedStats));
+
+ // Recount stats. We should have the same results.
+ ASSERT_NO_THROW(lmptr_->recountLeaseStats4());
+
+ // Make sure stats are as expected.
+ ASSERT_NO_FATAL_FAILURE(checkLeaseStats(expectedStats));
+
+ // Check that cumulative global stats always exist.
+ EXPECT_TRUE(StatsMgr::instance().getObservation("cumulative-assigned-addresses"));
+
+ // Now let's insert some leases into subnet 1.
+ int subnet_id = 1;
+
+ // Insert one lease in default state, i.e. assigned.
+ Lease4Ptr lease1 = makeLease4("192.0.1.1", subnet_id);
+
+ // Insert one lease in declined state.
+ Lease4Ptr lease2 = makeLease4("192.0.1.2", subnet_id, Lease::STATE_DECLINED);
+
+ // Insert one lease in the expired state.
+ makeLease4("192.0.1.3", subnet_id, Lease::STATE_EXPIRED_RECLAIMED);
+
+ // Insert another lease in default state, i.e. assigned.
+ makeLease4("192.0.1.4", subnet_id);
+
+ // Update the expected stats list for subnet 1.
+ expectedStats[subnet_id - 1]["assigned-addresses"] = 3; // 2 + 1 declined
+ expectedStats[subnet_id - 1]["declined-addresses"] = 1;
+
+ // Now let's add leases to subnet 2.
+ subnet_id = 2;
+
+ // Insert one declined lease.
+ makeLease4("192.0.2.2", subnet_id, Lease::STATE_DECLINED);
+
+ // Update the expected stats.
+ expectedStats[subnet_id - 1]["assigned-addresses"] = 1; // 0 + 1 declined
+ expectedStats[subnet_id - 1]["declined-addresses"] = 1;
+
+ // Now Recount the stats.
+ ASSERT_NO_THROW(lmptr_->recountLeaseStats4());
+
+ // Make sure stats are as expected.
+ ASSERT_NO_FATAL_FAILURE(checkLeaseStats(expectedStats));
+
+ // Delete some leases from subnet, and update the expected stats.
+ EXPECT_TRUE(lmptr_->deleteLease(lease1));
+ expectedStats[0]["assigned-addresses"] = 2;
+
+ EXPECT_TRUE(lmptr_->deleteLease(lease2));
+ expectedStats[0]["assigned-addresses"] = 1;
+ expectedStats[0]["declined-addresses"] = 0;
+
+ // Recount the stats.
+ ASSERT_NO_THROW(lmptr_->recountLeaseStats4());
+
+ // Make sure stats are as expected.
+ ASSERT_NO_FATAL_FAILURE(checkLeaseStats(expectedStats));
+}
+
+
+void
+GenericLeaseMgrTest::testRecountLeaseStats6() {
+ using namespace stats;
+
+ StatsMgr::instance().removeAll();
+
+ // Create two subnets.
+ int num_subnets = 2;
+ CfgSubnets6Ptr cfg = CfgMgr::instance().getStagingCfg()->getCfgSubnets6();
+ Subnet6Ptr subnet;
+ Pool6Ptr pool;
+ StatValMapList expectedStats(num_subnets);
+
+ int subnet_id = 1;
+ subnet.reset(new Subnet6(IOAddress("3001:1::"), 64, 1, 2, 3, 4, subnet_id));
+ pool.reset(new Pool6(Lease::TYPE_NA, IOAddress("3001:1::"),
+ IOAddress("3001:1::FF")));
+ subnet->addPool(pool);
+ expectedStats[subnet_id - 1]["total-nas"] = 256;
+
+ pool.reset(new Pool6(Lease::TYPE_PD, IOAddress("3001:1:2::"),96,112));
+ subnet->addPool(pool);
+ expectedStats[subnet_id - 1]["total-pds"] = 65536;
+ cfg->add(subnet);
+
+ ++subnet_id;
+ subnet.reset(new Subnet6(IOAddress("2001:db8:1::"), 64, 1, 2, 3, 4,
+ subnet_id));
+ pool.reset(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::"), 120));
+ subnet->addPool(pool);
+ expectedStats[subnet_id - 1]["total-nas"] = 256;
+ expectedStats[subnet_id - 1]["total-pds"] = 0;
+ cfg->add(subnet);
+
+ ASSERT_NO_THROW(CfgMgr::instance().commit());
+
+
+ // Create the expected stats list. At this point, the only stat
+ // that should be non-zero is total-nas/total-pds.
+ for (int i = 0; i < num_subnets; ++i) {
+ expectedStats[i]["assigned-nas"] = 0;
+ expectedStats[i]["declined-addresses"] = 0;
+ expectedStats[i]["reclaimed-declined-addresses"] = 0;
+ expectedStats[i]["assigned-pds"] = 0;
+ expectedStats[i]["reclaimed-leases"] = 0;
+ }
+
+ // Make sure stats are as expected.
+ ASSERT_NO_FATAL_FAILURE(checkLeaseStats(expectedStats));
+
+
+ // Recount stats. We should have the same results.
+ ASSERT_NO_THROW(lmptr_->recountLeaseStats6());
+
+ // Make sure stats are as expected.
+ ASSERT_NO_FATAL_FAILURE(checkLeaseStats(expectedStats));
+
+ // Check that cumulative global stats always exist.
+ EXPECT_TRUE(StatsMgr::instance().getObservation("cumulative-assigned-nas"));
+ EXPECT_TRUE(StatsMgr::instance().getObservation("cumulative-assigned-pds"));
+
+ // Now let's insert some leases into subnet 1.
+ subnet_id = 1;
+
+ // Insert three assigned NAs.
+ makeLease6(Lease::TYPE_NA, "3001:1::1", 0, subnet_id);
+ Lease6Ptr lease2 = makeLease6(Lease::TYPE_NA, "3001:1::2", 0, subnet_id);
+ makeLease6(Lease::TYPE_NA, "3001:1::3", 0, subnet_id);
+ expectedStats[subnet_id - 1]["assigned-nas"] = 5; // 3 + 2 declined
+
+ // Insert two declined NAs.
+ makeLease6(Lease::TYPE_NA, "3001:1::4", 0, subnet_id,
+ Lease::STATE_DECLINED);
+ makeLease6(Lease::TYPE_NA, "3001:1::5", 0, subnet_id,
+ Lease::STATE_DECLINED);
+ expectedStats[subnet_id - 1]["declined-addresses"] = 2;
+
+ // Insert one expired NA.
+ makeLease6(Lease::TYPE_NA, "3001:1::6", 0, subnet_id,
+ Lease::STATE_EXPIRED_RECLAIMED);
+
+ // Insert two assigned PDs.
+ makeLease6(Lease::TYPE_PD, "3001:1:2:0100::", 112, subnet_id);
+ makeLease6(Lease::TYPE_PD, "3001:1:2:0200::", 112, subnet_id);
+ expectedStats[subnet_id - 1]["assigned-pds"] = 2;
+
+ // Insert two expired PDs.
+ makeLease6(Lease::TYPE_PD, "3001:1:2:0300::", 112, subnet_id,
+ Lease::STATE_EXPIRED_RECLAIMED);
+ makeLease6(Lease::TYPE_PD, "3001:1:2:0400::", 112, subnet_id,
+ Lease::STATE_EXPIRED_RECLAIMED);
+
+ // Now let's add leases to subnet 2.
+ subnet_id = 2;
+
+ // Insert two assigned NAs.
+ makeLease6(Lease::TYPE_NA, "2001:db81::1", 0, subnet_id);
+ makeLease6(Lease::TYPE_NA, "2001:db81::2", 0, subnet_id);
+ expectedStats[subnet_id - 1]["assigned-nas"] = 3; // 2 + 1 declined
+
+ // Insert one declined NA.
+ Lease6Ptr lease3 = makeLease6(Lease::TYPE_NA, "2001:db81::3", 0, subnet_id,
+ Lease::STATE_DECLINED);
+ expectedStats[subnet_id - 1]["declined-addresses"] = 1;
+
+ // Now Recount the stats.
+ ASSERT_NO_THROW(lmptr_->recountLeaseStats6());
+
+ // Make sure stats are as expected.
+ ASSERT_NO_FATAL_FAILURE(checkLeaseStats(expectedStats));
+
+ // Delete some leases and update the expected stats.
+ EXPECT_TRUE(lmptr_->deleteLease(lease2));
+ expectedStats[0]["assigned-nas"] = 4;
+
+ EXPECT_TRUE(lmptr_->deleteLease(lease3));
+ expectedStats[1]["assigned-nas"] = 2;
+ expectedStats[1]["declined-addresses"] = 0;
+
+ // Recount the stats.
+ ASSERT_NO_THROW(lmptr_->recountLeaseStats6());
+
+ // Make sure stats are as expected.
+ ASSERT_NO_FATAL_FAILURE(checkLeaseStats(expectedStats));
+}
+
+void
+GenericLeaseMgrTest::testWipeLeases6() {
+ // Get the leases to be used for the test and add to the database
+ vector<Lease6Ptr> leases = createLeases6();
+ leases[0]->subnet_id_ = 1;
+ leases[1]->subnet_id_ = 1;
+ leases[2]->subnet_id_ = 1;
+ leases[3]->subnet_id_ = 22;
+ leases[4]->subnet_id_ = 333;
+ leases[5]->subnet_id_ = 333;
+ leases[6]->subnet_id_ = 333;
+ leases[7]->subnet_id_ = 333;
+
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // Let's try something simple. There shouldn't be any leases in
+ // subnet 2. The keep deleting the leases, perhaps in a different
+ // order they were added.
+ EXPECT_EQ(0, lmptr_->wipeLeases6(2));
+ EXPECT_EQ(4, lmptr_->wipeLeases6(333));
+ EXPECT_EQ(3, lmptr_->wipeLeases6(1));
+ EXPECT_EQ(1, lmptr_->wipeLeases6(22));
+
+ // All the leases should be gone now. Check that that repeated
+ // attempt to delete them will not result in any additional removals.
+ EXPECT_EQ(0, lmptr_->wipeLeases6(1));
+ EXPECT_EQ(0, lmptr_->wipeLeases6(22));
+ EXPECT_EQ(0, lmptr_->wipeLeases6(333));
+}
+
+void
+GenericLeaseMgrTest::testWipeLeases4() {
+ // Get the leases to be used for the test and add to the database
+ vector<Lease4Ptr> leases = createLeases4();
+ leases[0]->subnet_id_ = 1;
+ leases[1]->subnet_id_ = 1;
+ leases[2]->subnet_id_ = 1;
+ leases[3]->subnet_id_ = 22;
+ leases[4]->subnet_id_ = 333;
+ leases[5]->subnet_id_ = 333;
+ leases[6]->subnet_id_ = 333;
+ leases[7]->subnet_id_ = 333;
+
+ for (size_t i = 0; i < leases.size(); ++i) {
+ EXPECT_TRUE(lmptr_->addLease(leases[i]));
+ }
+
+ // Let's try something simple. There shouldn't be any leases in
+ // subnet 2. The keep deleting the leases, perhaps in a different
+ // order they were added.
+ EXPECT_EQ(0, lmptr_->wipeLeases4(2));
+ EXPECT_EQ(4, lmptr_->wipeLeases4(333));
+ EXPECT_EQ(3, lmptr_->wipeLeases4(1));
+ EXPECT_EQ(1, lmptr_->wipeLeases4(22));
+
+ // All the leases should be gone now. Check that that repeated
+ // attempt to delete them will not result in any additional removals.
+ EXPECT_EQ(0, lmptr_->wipeLeases4(1));
+ EXPECT_EQ(0, lmptr_->wipeLeases4(22));
+ EXPECT_EQ(0, lmptr_->wipeLeases4(333));
+}
+
+void
+LeaseMgrDbLostCallbackTest::SetUp() {
+ destroySchema();
+ createSchema();
+ isc::dhcp::LeaseMgrFactory::destroy();
+}
+
+void
+LeaseMgrDbLostCallbackTest::TearDown() {
+ destroySchema();
+ isc::dhcp::LeaseMgrFactory::destroy();
+}
+
+void
+LeaseMgrDbLostCallbackTest::testNoCallbackOnOpenFailure() {
+ DatabaseConnection::db_lost_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_lost_callback, this, ph::_1);
+
+ // Set the connectivity recovered callback.
+ DatabaseConnection::db_recovered_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_recovered_callback, this, ph::_1);
+
+ // Set the connectivity failed callback.
+ DatabaseConnection::db_failed_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_failed_callback, this, ph::_1);
+
+ ASSERT_THROW(LeaseMgrFactory::create(invalidConnectString()),
+ DbOpenError);
+
+ io_service_->poll();
+
+ EXPECT_EQ(0, db_lost_callback_called_);
+ EXPECT_EQ(0, db_recovered_callback_called_);
+ EXPECT_EQ(0, db_failed_callback_called_);
+}
+
+void
+LeaseMgrDbLostCallbackTest::testDbLostAndRecoveredCallback() {
+ // Set the connectivity lost callback.
+ DatabaseConnection::db_lost_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_lost_callback, this, ph::_1);
+
+ // Set the connectivity recovered callback.
+ DatabaseConnection::db_recovered_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_recovered_callback, this, ph::_1);
+
+ // Set the connectivity failed callback.
+ DatabaseConnection::db_failed_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_failed_callback, this, ph::_1);
+
+ std::string access = validConnectString();
+ CfgMgr::instance().getCurrentCfg()->getCfgDbAccess()->setLeaseDbAccessString(access);
+
+ // Connect to the lease backend.
+ ASSERT_NO_THROW(LeaseMgrFactory::create(access));
+
+ // The most recently opened socket should be for our SQL client.
+ int sql_socket = test::findLastSocketFd();
+ ASSERT_TRUE(sql_socket > -1);
+
+ // Verify we can execute a query. We do not care if
+ // we find a lease or not.
+ LeaseMgr& lm = LeaseMgrFactory::instance();
+
+ Lease4Ptr lease;
+ ASSERT_NO_THROW(lease = lm.getLease4(IOAddress("192.0.1.0")));
+
+ // Now close the sql socket out from under backend client
+ ASSERT_EQ(0, close(sql_socket));
+
+ // A query should fail with DbConnectionUnusable.
+ ASSERT_THROW(lease = lm.getLease4(IOAddress("192.0.1.0")),
+ DbConnectionUnusable);
+
+ io_service_->poll();
+
+ // Our lost and recovered connectivity callback should have been invoked.
+ EXPECT_EQ(1, db_lost_callback_called_);
+ EXPECT_EQ(1, db_recovered_callback_called_);
+ EXPECT_EQ(0, db_failed_callback_called_);
+}
+
+void
+LeaseMgrDbLostCallbackTest::testDbLostAndFailedCallback() {
+ // Set the connectivity lost callback.
+ DatabaseConnection::db_lost_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_lost_callback, this, ph::_1);
+
+ // Set the connectivity recovered callback.
+ DatabaseConnection::db_recovered_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_recovered_callback, this, ph::_1);
+
+ // Set the connectivity failed callback.
+ DatabaseConnection::db_failed_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_failed_callback, this, ph::_1);
+
+ std::string access = validConnectString();
+ CfgMgr::instance().getCurrentCfg()->getCfgDbAccess()->setLeaseDbAccessString(access);
+
+ // Connect to the lease backend.
+ ASSERT_NO_THROW(LeaseMgrFactory::create(access));
+
+ // The most recently opened socket should be for our SQL client.
+ int sql_socket = test::findLastSocketFd();
+ ASSERT_TRUE(sql_socket > -1);
+
+ // Verify we can execute a query. We do not care if
+ // we find a lease or not.
+ LeaseMgr& lm = LeaseMgrFactory::instance();
+
+ Lease4Ptr lease;
+ ASSERT_NO_THROW(lease = lm.getLease4(IOAddress("192.0.1.0")));
+
+ access = invalidConnectString();
+ // by adding an invalid access will cause the manager factory to throw
+ // resulting in failure to recreate the manager
+ CfgMgr::instance().getCurrentCfg()->getCfgDbAccess()->setLeaseDbAccessString(access);
+
+ // Now close the sql socket out from under backend client
+ ASSERT_EQ(0, close(sql_socket));
+
+ // A query should fail with DbConnectionUnusable.
+ ASSERT_THROW(lease = lm.getLease4(IOAddress("192.0.1.0")),
+ DbConnectionUnusable);
+
+ io_service_->poll();
+
+ // Our lost and failed connectivity callback should have been invoked.
+ EXPECT_EQ(1, db_lost_callback_called_);
+ EXPECT_EQ(0, db_recovered_callback_called_);
+ EXPECT_EQ(1, db_failed_callback_called_);
+}
+
+void
+LeaseMgrDbLostCallbackTest::testDbLostAndRecoveredAfterTimeoutCallback() {
+ // Set the connectivity lost callback.
+ DatabaseConnection::db_lost_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_lost_callback, this, ph::_1);
+
+ // Set the connectivity recovered callback.
+ DatabaseConnection::db_recovered_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_recovered_callback, this, ph::_1);
+
+ // Set the connectivity failed callback.
+ DatabaseConnection::db_failed_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_failed_callback, this, ph::_1);
+
+ std::string access = validConnectString();
+ std::string extra = " max-reconnect-tries=3 reconnect-wait-time=1";
+ access += extra;
+ CfgMgr::instance().getCurrentCfg()->getCfgDbAccess()->setLeaseDbAccessString(access);
+
+ // Connect to the lease backend.
+ ASSERT_NO_THROW(LeaseMgrFactory::create(access));
+
+ // The most recently opened socket should be for our SQL client.
+ int sql_socket = test::findLastSocketFd();
+ ASSERT_TRUE(sql_socket > -1);
+
+ // Verify we can execute a query. We do not care if
+ // we find a lease or not.
+ LeaseMgr& lm = LeaseMgrFactory::instance();
+
+ Lease4Ptr lease;
+ ASSERT_NO_THROW(lease = lm.getLease4(IOAddress("192.0.1.0")));
+
+ access = invalidConnectString();
+ access += extra;
+ // by adding an invalid access will cause the manager factory to throw
+ // resulting in failure to recreate the manager
+ CfgMgr::instance().getCurrentCfg()->getCfgDbAccess()->setLeaseDbAccessString(access);
+
+ // Now close the sql socket out from under backend client
+ ASSERT_EQ(0, close(sql_socket));
+
+ // A query should fail with DbConnectionUnusable.
+ ASSERT_THROW(lease = lm.getLease4(IOAddress("192.0.1.0")),
+ DbConnectionUnusable);
+
+ io_service_->poll();
+
+ // Our lost connectivity callback should have been invoked.
+ EXPECT_EQ(1, db_lost_callback_called_);
+ EXPECT_EQ(0, db_recovered_callback_called_);
+ EXPECT_EQ(0, db_failed_callback_called_);
+
+ access = validConnectString();
+ access += extra;
+ CfgMgr::instance().getCurrentCfg()->getCfgDbAccess()->setLeaseDbAccessString(access);
+
+ sleep(1);
+
+ io_service_->poll();
+
+ // Our lost and recovered connectivity callback should have been invoked.
+ EXPECT_EQ(2, db_lost_callback_called_);
+ EXPECT_EQ(1, db_recovered_callback_called_);
+ EXPECT_EQ(0, db_failed_callback_called_);
+
+ sleep(1);
+
+ io_service_->poll();
+
+ // No callback should have been invoked.
+ EXPECT_EQ(2, db_lost_callback_called_);
+ EXPECT_EQ(1, db_recovered_callback_called_);
+ EXPECT_EQ(0, db_failed_callback_called_);
+}
+
+void
+LeaseMgrDbLostCallbackTest::testDbLostAndFailedAfterTimeoutCallback() {
+ // Set the connectivity lost callback.
+ DatabaseConnection::db_lost_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_lost_callback, this, ph::_1);
+
+ // Set the connectivity recovered callback.
+ DatabaseConnection::db_recovered_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_recovered_callback, this, ph::_1);
+
+ // Set the connectivity failed callback.
+ DatabaseConnection::db_failed_callback_ =
+ std::bind(&LeaseMgrDbLostCallbackTest::db_failed_callback, this, ph::_1);
+
+ std::string access = validConnectString();
+ std::string extra = " max-reconnect-tries=3 reconnect-wait-time=1";
+ access += extra;
+ CfgMgr::instance().getCurrentCfg()->getCfgDbAccess()->setLeaseDbAccessString(access);
+
+ // Connect to the lease backend.
+ ASSERT_NO_THROW(LeaseMgrFactory::create(access));
+
+ // The most recently opened socket should be for our SQL client.
+ int sql_socket = test::findLastSocketFd();
+ ASSERT_TRUE(sql_socket > -1);
+
+ // Verify we can execute a query. We do not care if
+ // we find a lease or not.
+ LeaseMgr& lm = LeaseMgrFactory::instance();
+
+ Lease4Ptr lease;
+ ASSERT_NO_THROW(lease = lm.getLease4(IOAddress("192.0.1.0")));
+
+ access = invalidConnectString();
+ access += extra;
+ // by adding an invalid access will cause the manager factory to throw
+ // resulting in failure to recreate the manager
+ CfgMgr::instance().getCurrentCfg()->getCfgDbAccess()->setLeaseDbAccessString(access);
+
+ // Now close the sql socket out from under backend client
+ ASSERT_EQ(0, close(sql_socket));
+
+ // A query should fail with DbConnectionUnusable.
+ ASSERT_THROW(lease = lm.getLease4(IOAddress("192.0.1.0")),
+ DbConnectionUnusable);
+
+ io_service_->poll();
+
+ // Our lost connectivity callback should have been invoked.
+ EXPECT_EQ(1, db_lost_callback_called_);
+ EXPECT_EQ(0, db_recovered_callback_called_);
+ EXPECT_EQ(0, db_failed_callback_called_);
+
+ sleep(1);
+
+ io_service_->poll();
+
+ // Our lost connectivity callback should have been invoked.
+ EXPECT_EQ(2, db_lost_callback_called_);
+ EXPECT_EQ(0, db_recovered_callback_called_);
+ EXPECT_EQ(0, db_failed_callback_called_);
+
+ sleep(1);
+
+ io_service_->poll();
+
+ // Our lost and failed connectivity callback should have been invoked.
+ EXPECT_EQ(3, db_lost_callback_called_);
+ EXPECT_EQ(0, db_recovered_callback_called_);
+ EXPECT_EQ(1, db_failed_callback_called_);
+}
+
+void
+GenericLeaseMgrTest::checkLeaseRange(const Lease4Collection& returned,
+ const std::vector<std::string>& expected_addresses) {
+ ASSERT_EQ(expected_addresses.size(), returned.size());
+
+ for (auto a = returned.cbegin(); a != returned.cend(); ++a) {
+ EXPECT_EQ(expected_addresses[std::distance(returned.cbegin(), a)],
+ (*a)->addr_.toText());
+ }
+}
+
+void
+GenericLeaseMgrTest::checkQueryAgainstRowSet(const LeaseStatsQueryPtr& query,
+ const RowSet& expected_rows) {
+ ASSERT_TRUE(query) << "query is null";
+
+ int rows_matched = 0;
+ LeaseStatsRow row;
+ while (query->getNextRow(row)) {
+ auto found_row = expected_rows.find(row);
+ if (found_row == expected_rows.end()) {
+ ADD_FAILURE() << "query row not in expected set"
+ << " id: " << row.subnet_id_
+ << " type: " << row.lease_type_
+ << " state: " << row.lease_state_
+ << " count: " << row.state_count_;
+ } else {
+ if (row.state_count_ != (*found_row).state_count_) {
+ ADD_FAILURE() << "row count wrong for"
+ << " id: " << row.subnet_id_
+ << " type: " << row.lease_type_
+ << " state: " << row.lease_state_
+ << " count: " << row.state_count_
+ << "; expected: " << (*found_row).state_count_;
+ } else {
+ ++rows_matched;
+ }
+ }
+ }
+
+ ASSERT_EQ(rows_matched, expected_rows.size()) << "rows mismatched";
+}
+
+void
+GenericLeaseMgrTest::testLeaseStatsQuery4() {
+ // Create three subnets.
+ CfgSubnets4Ptr cfg = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ Subnet4Ptr subnet;
+ Pool4Ptr pool;
+
+ subnet.reset(new Subnet4(IOAddress("192.0.1.0"), 24, 1, 2, 3, 1));
+ pool.reset(new Pool4(IOAddress("192.0.1.0"), 24));
+ subnet->addPool(pool);
+ cfg->add(subnet);
+
+ subnet.reset(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3, 2));
+ pool.reset(new Pool4(IOAddress("192.0.2.0"), 24));
+ subnet->addPool(pool);
+ cfg->add(subnet);
+
+ subnet.reset(new Subnet4(IOAddress("192.0.3.0"), 24, 1, 2, 3, 3));
+ pool.reset(new Pool4(IOAddress("192.0.3.0"), 24));
+ subnet->addPool(pool);
+ cfg->add(subnet);
+
+ ASSERT_NO_THROW(CfgMgr::instance().commit());
+
+ // Make sure invalid values throw.
+ LeaseStatsQueryPtr query;
+ ASSERT_THROW(query = lmptr_->startSubnetLeaseStatsQuery4(0), BadValue);
+ ASSERT_THROW(query = lmptr_->startSubnetRangeLeaseStatsQuery4(0,1), BadValue);
+ ASSERT_THROW(query = lmptr_->startSubnetRangeLeaseStatsQuery4(1,0), BadValue);
+ ASSERT_THROW(query = lmptr_->startSubnetRangeLeaseStatsQuery4(10,1), BadValue);
+
+ // Start tests with an empty expected row set.
+ RowSet expected_rows;
+
+ // Before we add leases, test an empty return for get all subnets
+ {
+ SCOPED_TRACE("GET ALL WITH NO LEASES");
+ ASSERT_NO_THROW(query = lmptr_->startLeaseStatsQuery4());
+ checkQueryAgainstRowSet(query, expected_rows);
+ }
+
+ // Now let's insert some leases into subnet 1.
+ // Two leases in the default state, i.e. assigned.
+ // One lease in declined state.
+ // One lease in the expired state.
+ int subnet_id = 1;
+ makeLease4("192.0.1.1", subnet_id);
+ makeLease4("192.0.1.2", subnet_id, Lease::STATE_DECLINED);
+ makeLease4("192.0.1.3", subnet_id, Lease::STATE_EXPIRED_RECLAIMED);
+ makeLease4("192.0.1.4", subnet_id);
+
+ // Now let's add leases to subnet 2.
+ // One declined lease.
+ subnet_id = 2;
+ makeLease4("192.0.2.2", subnet_id, Lease::STATE_DECLINED);
+
+ // Now add leases to subnet 3
+ // Two leases in default state, i.e. assigned.
+ // One declined lease.
+ subnet_id = 3;
+ makeLease4("192.0.3.1", subnet_id);
+ makeLease4("192.0.3.2", subnet_id);
+ makeLease4("192.0.3.3", subnet_id, Lease::STATE_DECLINED);
+
+ // Test single subnet for non-matching subnet
+ {
+ SCOPED_TRACE("NO MATCHING SUBNET");
+ ASSERT_NO_THROW(query = lmptr_->startSubnetLeaseStatsQuery4(777));
+ checkQueryAgainstRowSet(query, expected_rows);
+ }
+
+ // Test an empty range
+ {
+ SCOPED_TRACE("EMPTY SUBNET RANGE");
+ ASSERT_NO_THROW(query = lmptr_->startSubnetRangeLeaseStatsQuery4(777, 900));
+ checkQueryAgainstRowSet(query, expected_rows);
+ }
+
+ // Test a single subnet
+ {
+ SCOPED_TRACE("SINGLE SUBNET");
+ // Add expected rows for Subnet 2
+ expected_rows.insert(LeaseStatsRow(2, Lease::STATE_DECLINED, 1));
+ // Start the query
+ ASSERT_NO_THROW(query = lmptr_->startSubnetLeaseStatsQuery4(2));
+ // Verify contents
+ checkQueryAgainstRowSet(query, expected_rows);
+ }
+
+ // Test a range of subnets
+ {
+ SCOPED_TRACE("SUBNET RANGE");
+ // Add expected rows for Subnet 3
+ expected_rows.insert(LeaseStatsRow(3, Lease::STATE_DEFAULT, 2));
+ expected_rows.insert(LeaseStatsRow(3, Lease::STATE_DECLINED, 1));
+ // Start the query
+ ASSERT_NO_THROW(query = lmptr_->startSubnetRangeLeaseStatsQuery4(2,3));
+ // Verify contents
+ checkQueryAgainstRowSet(query, expected_rows);
+ }
+
+ // Test all subnets
+ {
+ SCOPED_TRACE("ALL SUBNETS");
+ // Add expected rows for Subnet 1
+ expected_rows.insert(LeaseStatsRow(1, Lease::STATE_DEFAULT, 2));
+ expected_rows.insert(LeaseStatsRow(1, Lease::STATE_DECLINED, 1));
+ // Start the query
+ ASSERT_NO_THROW(query = lmptr_->startLeaseStatsQuery4());
+ // Verify contents
+ checkQueryAgainstRowSet(query, expected_rows);
+ }
+}
+
+void
+GenericLeaseMgrTest::testLeaseStatsQuery6() {
+ // Create three subnets.
+ CfgSubnets6Ptr cfg = CfgMgr::instance().getStagingCfg()->getCfgSubnets6();
+ Subnet6Ptr subnet;
+ Pool6Ptr pool;
+
+ int subnet_id = 1;
+ subnet.reset(new Subnet6(IOAddress("3001:1::"), 64, 1, 2, 3, 4, subnet_id));
+ pool.reset(new Pool6(Lease::TYPE_NA, IOAddress("3001:1::"),
+ IOAddress("3001:1::FF")));
+ subnet->addPool(pool);
+
+ pool.reset(new Pool6(Lease::TYPE_PD, IOAddress("3001:1:2::"),96,112));
+ subnet->addPool(pool);
+ cfg->add(subnet);
+
+ ++subnet_id;
+ subnet.reset(new Subnet6(IOAddress("2001:db8:1::"), 64, 1, 2, 3, 4,
+ subnet_id));
+ pool.reset(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::"), 120));
+ subnet->addPool(pool);
+ cfg->add(subnet);
+
+ ++subnet_id;
+ subnet.reset(new Subnet6(IOAddress("2002:db8:1::"), 64, 1, 2, 3, 4,
+ subnet_id));
+ pool.reset(new Pool6(Lease::TYPE_NA, IOAddress("2002:db8:1::"), 120));
+ subnet->addPool(pool);
+ cfg->add(subnet);
+
+ ASSERT_NO_THROW(CfgMgr::instance().commit());
+
+ // Make sure invalid values throw.
+ LeaseStatsQueryPtr query;
+ ASSERT_THROW(query = lmptr_->startSubnetLeaseStatsQuery6(0), BadValue);
+ ASSERT_THROW(query = lmptr_->startSubnetRangeLeaseStatsQuery6(0,1), BadValue);
+ ASSERT_THROW(query = lmptr_->startSubnetRangeLeaseStatsQuery6(1,0), BadValue);
+ ASSERT_THROW(query = lmptr_->startSubnetRangeLeaseStatsQuery6(10,1), BadValue);
+
+ // Start tests with an empty expected row set.
+ RowSet expected_rows;
+
+ // Before we add leases, test an empty return for get all subnets
+ {
+ SCOPED_TRACE("GET ALL WITH NO LEASES");
+ ASSERT_NO_THROW(query = lmptr_->startLeaseStatsQuery6());
+ checkQueryAgainstRowSet(query, expected_rows);
+ }
+
+
+ // Now let's insert some leases into subnet 1.
+ // Three assigned NAs.
+ // Two declined NAs.
+ // One expired NA.
+ // Two assigned PDs.
+ // Two expired PDs.
+ subnet_id = 1;
+ makeLease6(Lease::TYPE_NA, "3001:1::1", 0, subnet_id);
+ makeLease6(Lease::TYPE_NA, "3001:1::2", 0, subnet_id);
+ makeLease6(Lease::TYPE_NA, "3001:1::3", 0, subnet_id);
+ makeLease6(Lease::TYPE_NA, "3001:1::4", 0, subnet_id,
+ Lease::STATE_DECLINED);
+ makeLease6(Lease::TYPE_NA, "3001:1::5", 0, subnet_id,
+ Lease::STATE_DECLINED);
+ makeLease6(Lease::TYPE_NA, "3001:1::6", 0, subnet_id,
+ Lease::STATE_EXPIRED_RECLAIMED);
+ makeLease6(Lease::TYPE_PD, "3001:1:2:0100::", 112, subnet_id);
+ makeLease6(Lease::TYPE_PD, "3001:1:2:0200::", 112, subnet_id);
+ makeLease6(Lease::TYPE_PD, "3001:1:2:0300::", 112, subnet_id,
+ Lease::STATE_EXPIRED_RECLAIMED);
+ makeLease6(Lease::TYPE_PD, "3001:1:2:0400::", 112, subnet_id,
+ Lease::STATE_EXPIRED_RECLAIMED);
+
+ // Now let's add leases to subnet 2.
+ // Two assigned NAs
+ // One declined NAs
+ subnet_id = 2;
+ makeLease6(Lease::TYPE_NA, "2001:db81::1", 0, subnet_id);
+ makeLease6(Lease::TYPE_NA, "2001:db81::2", 0, subnet_id);
+ makeLease6(Lease::TYPE_NA, "2001:db81::3", 0, subnet_id,
+ Lease::STATE_DECLINED);
+
+ // Now let's add leases to subnet 3.
+ // Two assigned NAs
+ // One declined NAs
+ subnet_id = 3;
+ makeLease6(Lease::TYPE_NA, "2002:db81::1", 0, subnet_id);
+ makeLease6(Lease::TYPE_NA, "2002:db81::2", 0, subnet_id);
+ makeLease6(Lease::TYPE_NA, "2002:db81::3", 0, subnet_id,
+ Lease::STATE_DECLINED);
+
+ // Test single subnet for non-matching subnet
+ {
+ SCOPED_TRACE("NO MATCHING SUBNET");
+ ASSERT_NO_THROW(query = lmptr_->startSubnetLeaseStatsQuery6(777));
+ checkQueryAgainstRowSet(query, expected_rows);
+ }
+
+ // Test an empty range
+ {
+ SCOPED_TRACE("EMPTY SUBNET RANGE");
+ ASSERT_NO_THROW(query = lmptr_->startSubnetRangeLeaseStatsQuery6(777, 900));
+ checkQueryAgainstRowSet(query, expected_rows);
+ }
+
+ // Test a single subnet
+ {
+ SCOPED_TRACE("SINGLE SUBNET");
+ // Add expected row for Subnet 2
+ expected_rows.insert(LeaseStatsRow(2, Lease::TYPE_NA, Lease::STATE_DEFAULT, 2));
+ expected_rows.insert(LeaseStatsRow(2, Lease::TYPE_NA, Lease::STATE_DECLINED, 1));
+ // Start the query
+ ASSERT_NO_THROW(query = lmptr_->startSubnetLeaseStatsQuery6(2));
+ // Verify contents
+ checkQueryAgainstRowSet(query, expected_rows);
+ }
+
+ // Test a range of subnets
+ {
+ SCOPED_TRACE("SUBNET RANGE");
+ // Add expected rows for Subnet 3
+ expected_rows.insert(LeaseStatsRow(3, Lease::TYPE_NA, Lease::STATE_DEFAULT, 2));
+ expected_rows.insert(LeaseStatsRow(3, Lease::TYPE_NA, Lease::STATE_DECLINED, 1));
+ // Start the query
+ ASSERT_NO_THROW(query = lmptr_->startSubnetRangeLeaseStatsQuery6(2,3));
+ // Verify contents
+ checkQueryAgainstRowSet(query, expected_rows);
+ }
+
+ // Test all subnets
+ {
+ SCOPED_TRACE("ALL SUBNETS");
+ // Add expected rows for Subnet 1
+ expected_rows.insert(LeaseStatsRow(1, Lease::TYPE_NA, Lease::STATE_DEFAULT, 3));
+ expected_rows.insert(LeaseStatsRow(1, Lease::TYPE_NA, Lease::STATE_DECLINED, 2));
+ expected_rows.insert(LeaseStatsRow(1, Lease::TYPE_PD, Lease::STATE_DEFAULT, 2));
+ // Start the query
+ ASSERT_NO_THROW(query = lmptr_->startLeaseStatsQuery6());
+
+ // Verify contents
+ checkQueryAgainstRowSet(query, expected_rows);
+ }
+}
+
+void
+GenericLeaseMgrTest::testLeaseStatsQueryAttribution4() {
+ // Create two subnets for the same range.
+ CfgSubnets4Ptr cfg = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ Subnet4Ptr subnet;
+
+ subnet.reset(new Subnet4(IOAddress("192.0.1.0"), 24, 1, 2, 3, 1));
+ cfg->add(subnet);
+
+ // Note it is even allowed to use 192.0.1.1/24 here...
+ subnet.reset(new Subnet4(IOAddress("192.0.1.0"), 25, 1, 2, 3, 2));
+ cfg->add(subnet);
+
+ ASSERT_NO_THROW(CfgMgr::instance().commit());
+
+ LeaseStatsQueryPtr query;
+ RowSet expected_rows;
+
+ // Now let's insert two leases into subnet 1.
+ int subnet_id = 1;
+ makeLease4("192.0.1.1", subnet_id);
+ Lease4Ptr lease = makeLease4("192.0.1.2", subnet_id);
+
+ // And one lease into subnet 2.
+ subnet_id = 2;
+ makeLease4("192.0.1.3", subnet_id);
+
+ // Move a lease to the second subnet.
+ lease->subnet_id_ = subnet_id;
+ EXPECT_NO_THROW(lmptr_->updateLease4(lease));
+
+ // Add expected rows for Subnets.
+ expected_rows.insert(LeaseStatsRow(1, Lease::STATE_DEFAULT, 1));
+ expected_rows.insert(LeaseStatsRow(2, Lease::STATE_DEFAULT, 2));
+
+ // Start the query
+ ASSERT_NO_THROW(query = lmptr_->startLeaseStatsQuery4());
+
+ // Verify contents
+ checkQueryAgainstRowSet(query, expected_rows);
+}
+
+void
+GenericLeaseMgrTest::testLeaseStatsQueryAttribution6() {
+ // Create two subnets.
+ CfgSubnets6Ptr cfg = CfgMgr::instance().getStagingCfg()->getCfgSubnets6();
+ Subnet6Ptr subnet;
+
+ int subnet_id = 1;
+ subnet.reset(new Subnet6(IOAddress("2001:db8:1::"), 64, 1, 2, 3, 4,
+ subnet_id));
+ cfg->add(subnet);
+
+ ++subnet_id;
+ subnet.reset(new Subnet6(IOAddress("2001:db8:1::1"), 64, 1, 2, 3, 4,
+ subnet_id));
+ cfg->add(subnet);
+
+ ASSERT_NO_THROW(CfgMgr::instance().commit());
+
+ LeaseStatsQueryPtr query;
+ RowSet expected_rows;
+
+ // Now let's insert two leases into subnet 1.
+ subnet_id = 1;
+ makeLease6(Lease::TYPE_NA, "2001:db81::1", 0, subnet_id);
+ Lease6Ptr lease = makeLease6(Lease::TYPE_NA, "2001:db81::2", 0, subnet_id);
+
+ // And one lease into subnet 2.
+ subnet_id = 2;
+ makeLease6(Lease::TYPE_NA, "2001:db81::3", 0, subnet_id);
+
+ // Move a lease to the second subnet.
+ lease->subnet_id_ = subnet_id;
+ EXPECT_NO_THROW(lmptr_->updateLease6(lease));
+
+ // Add expected rows for Subnets.
+ expected_rows.insert(LeaseStatsRow(1, Lease::TYPE_NA,
+ Lease::STATE_DEFAULT, 1));
+ expected_rows.insert(LeaseStatsRow(2, Lease::TYPE_NA,
+ Lease::STATE_DEFAULT, 2));
+ // Start the query
+ ASSERT_NO_THROW(query = lmptr_->startLeaseStatsQuery6());
+
+ // Verify contents
+ checkQueryAgainstRowSet(query, expected_rows);
+}
+
+void
+GenericLeaseMgrTest::testLeaseLimits4() {
+ std::string text;
+ ElementPtr user_context;
+
+ // -- A limit of 0 always denies a lease. --
+
+ user_context = Element::fromJSON(R"({ "ISC": { "limits": {
+ "client-classes": [ { "name": "foo", "address-limit": 0 } ] } } })");
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits4(user_context));
+ EXPECT_EQ(text, "address limit 0 for client class \"foo\", current lease count 0");
+
+ user_context = Element::fromJSON(R"({ "ISC": { "limits": {
+ "subnet": { "id": 1, "address-limit": 0 } } } })");
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits4(user_context));
+ EXPECT_EQ(text, "address limit 0 for subnet ID 1, current lease count 0");
+
+ // -- A limit of 1 with no leases should allow a lease. --
+
+ user_context = Element::fromJSON(R"({ "ISC": { "limits": {
+ "client-classes": [ { "name": "foo", "address-limit": 1 } ] } } })");
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits4(user_context));
+ EXPECT_EQ(text, "");
+
+ user_context = Element::fromJSON(R"({ "ISC": { "limits": {
+ "subnet": { "id": 1, "address-limit": 1 } } } })");
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits4(user_context));
+ EXPECT_EQ(text, "");
+
+ // -- A limit of 1 with 1 current lease should deny further leases. --
+
+ makeLease4("192.0.1.1", 1, Lease::STATE_DEFAULT, Element::fromJSON(
+ R"({ "ISC": { "client-classes": [ "foo" ] } })"));
+
+ // Since we did not go through allocation engine stats won't be altered.
+ ASSERT_NO_THROW(lmptr_->recountLeaseStats4());
+
+ user_context = Element::fromJSON(R"({ "ISC": { "limits": {
+ "client-classes": [ { "name": "foo", "address-limit": 1 } ] } } })");
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits4(user_context));
+ EXPECT_EQ(text, "address limit 1 for client class \"foo\", current lease count 1");
+
+ user_context = Element::fromJSON(R"({ "ISC": { "limits": {
+ "subnet": { "id": 1, "address-limit": 1 } } } })");
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits4(user_context));
+ EXPECT_EQ(text, "address limit 1 for subnet ID 1, current lease count 1");
+}
+
+void
+GenericLeaseMgrTest::testLeaseLimits6() {
+ std::string text;
+ ElementPtr user_context;
+
+ // -- A limit of 0 always denies a lease. --
+
+ user_context = Element::fromJSON(R"({ "ISC": { "limits": {
+ "client-classes": [ { "name": "foo", "address-limit": 0 } ] } } })");
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits6(user_context));
+ EXPECT_EQ(text, "address limit 0 for client class \"foo\", current lease count 0");
+
+ user_context = Element::fromJSON(R"({ "ISC": { "limits": {
+ "subnet": { "id": 1, "address-limit": 0 } } } })");
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits6(user_context));
+ EXPECT_EQ(text, "address limit 0 for subnet ID 1, current lease count 0");
+
+ // -- A limit of 1 with no leases should allow a lease. --
+
+ user_context = Element::fromJSON(R"({ "ISC": { "limits": {
+ "client-classes": [ { "name": "foo", "address-limit": 1 } ] } } })");
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits6(user_context));
+ EXPECT_EQ(text, "");
+
+ user_context = Element::fromJSON(R"({ "ISC": { "limits": {
+ "subnet": { "id": 1, "address-limit": 1 } } } })");
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits6(user_context));
+ EXPECT_EQ(text, "");
+
+ // -- A limit of 1 with 1 current lease should deny further leases. --
+ makeLease6(Lease::TYPE_NA, "2001:db8::", 0, 1, Lease::STATE_DEFAULT, Element::fromJSON(
+ R"({ "ISC": { "client-classes": [ "foo" ] } })"));
+
+ makeLease6(Lease::TYPE_PD, "2001:db8:1::", 64, 1, Lease::STATE_DEFAULT, Element::fromJSON(
+ R"({ "ISC": { "client-classes": [ "foo" ] } })"));
+
+ // Since we did not go through allocation engine stats won't be altered.
+ ASSERT_NO_THROW(lmptr_->recountLeaseStats6());
+
+ user_context = Element::fromJSON(R"({ "ISC": { "limits": {
+ "client-classes": [ { "name": "foo", "address-limit": 1 } ] } } })");
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits6(user_context));
+ EXPECT_EQ(text, "address limit 1 for client class \"foo\", current lease count 1");
+
+ user_context = Element::fromJSON(R"({ "ISC": { "limits": {
+ "client-classes": [ { "name": "foo", "prefix-limit": 1 } ] } } })");
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits6(user_context));
+ EXPECT_EQ(text, "prefix limit 1 for client class \"foo\", current lease count 1");
+
+ user_context = Element::fromJSON(R"({ "ISC": { "limits": {
+ "subnet": { "id": 1, "address-limit": 1 } } } })");
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits6(user_context));
+ EXPECT_EQ(text, "address limit 1 for subnet ID 1, current lease count 1");
+
+ user_context = Element::fromJSON(R"({ "ISC": { "limits": {
+ "subnet": { "id": 1, "prefix-limit": 1 } } } })");
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits6(user_context));
+ EXPECT_EQ(text, "prefix limit 1 for subnet ID 1, current lease count 1");
+}
+
+ElementPtr
+GenericLeaseMgrTest::makeContextWithClasses(const std::list<ClientClass>& classes) {
+ ElementPtr ctx = Element::createMap();
+ if (classes.size()) {
+ ElementPtr clist = Element::createList();
+ for (auto client_class : classes ) {
+ clist->add(Element::create(client_class));
+ }
+
+ ElementPtr client_classes = Element::createMap();
+ client_classes->set("client-classes", clist);
+ ctx->set("ISC", client_classes);
+ }
+
+ return (ctx);
+}
+
+void
+GenericLeaseMgrTest::testClassLeaseCount4() {
+ // Make user-contexts with different class lists.
+ std::list<ClientClass> classes1{"water"};
+ ElementPtr ctx1 = makeContextWithClasses(classes1);
+
+ std::list<ClientClass> classes2{"melon"};
+ ElementPtr ctx2 = makeContextWithClasses(classes2);
+
+ // Counts should be 0.
+ EXPECT_EQ(0, lmptr_->getClassLeaseCount("water"));
+ EXPECT_EQ(0, lmptr_->getClassLeaseCount("melon"));
+
+ // Create a lease to add to the lease store.
+ vector<Lease4Ptr> leases = createLeases4();
+ Lease4Ptr lease = leases[1];
+
+ // Set the lease state to STATE_DEFAULT so it classes should be counted.
+ lease->state_ = Lease::STATE_DEFAULT;
+
+ // Add class list 1 to the lease.
+ lease->setContext(ctx1);
+
+ // Add the lease to the lease store and verify class lease counts.
+ ASSERT_NO_THROW_LOG(lmptr_->addLease(lease));
+ EXPECT_EQ(1, lmptr_->getClassLeaseCount("water"));
+ EXPECT_EQ(0, lmptr_->getClassLeaseCount("melon"));
+
+ // Re-fetch lease. This returns a copy of the persisted lease, which is
+ // what Kea logic always does. Fetches a copy. Otherwise we're changing
+ // the persisted lease which would make old and new the same thing.
+ lease = lmptr_->getLease4(lease->addr_);
+ ASSERT_TRUE(lease);
+
+ // Change the class list.
+ lease->setContext(ctx2);
+
+ // Update the lease in the lease store and verify class lease counts.
+ ASSERT_NO_THROW_LOG(lmptr_->updateLease4(lease));
+ EXPECT_EQ(0, lmptr_->getClassLeaseCount("water"));
+ EXPECT_EQ(1, lmptr_->getClassLeaseCount("melon"));
+
+ lease = lmptr_->getLease4(lease->addr_);
+ ASSERT_TRUE(lease);
+
+ // Now delete the lease from the store and verify counts.
+ ASSERT_NO_THROW_LOG(lmptr_->deleteLease(lease));
+ EXPECT_EQ(0, lmptr_->getClassLeaseCount("water"));
+ EXPECT_EQ(0, lmptr_->getClassLeaseCount("melon"));
+
+ lease = lmptr_->getLease4(lease->addr_);
+ ASSERT_FALSE(lease);
+}
+
+void
+GenericLeaseMgrTest::testClassLeaseCount6(Lease::Type ltype) {
+ ASSERT_TRUE(ltype == Lease::TYPE_NA || ltype == Lease::TYPE_PD);
+
+ // Make user-contexts with different class lists.
+ std::list<ClientClass> classes1{"water"};
+ ElementPtr ctx1 = makeContextWithClasses(classes1);
+
+ std::list<ClientClass> classes2{"melon"};
+ ElementPtr ctx2 = makeContextWithClasses(classes2);
+
+ // Counts should be 0.
+ EXPECT_EQ(0, lmptr_->getClassLeaseCount("water", ltype));
+ EXPECT_EQ(0, lmptr_->getClassLeaseCount("melon", ltype));
+
+ // Create a lease to add to the lease store.
+ vector<Lease6Ptr> leases = createLeases6();
+ Lease6Ptr lease = leases[1];
+ lease->type_ = ltype;
+
+ // Set the lease state to STATE_DEFAULT so it classes should be counted.
+ lease->state_ = Lease::STATE_DEFAULT;
+
+ // Add class list 1 to the lease.
+ lease->setContext(ctx1);
+
+ // Add the lease to the lease store and verify class lease counts.
+ ASSERT_NO_THROW_LOG(lmptr_->addLease(lease));
+ EXPECT_EQ(1, lmptr_->getClassLeaseCount("water", ltype));
+ EXPECT_EQ(0, lmptr_->getClassLeaseCount("melon", ltype));
+
+ // Re-fetch lease. This returns a copy of the persisted lease, which is
+ // what Kea logic always does. Fetches a copy. Otherwise we're changing
+ // the persisted lease which would make old and new the same thing.
+ lease = lmptr_->getLease6(ltype, lease->addr_);
+ ASSERT_TRUE(lease);
+
+ // Change the class list.
+ lease->setContext(ctx2);
+
+ // Update the lease in the lease store and verify class lease counts.
+ ASSERT_NO_THROW_LOG(lmptr_->updateLease6(lease));
+ EXPECT_EQ(0, lmptr_->getClassLeaseCount("water", ltype));
+ EXPECT_EQ(1, lmptr_->getClassLeaseCount("melon", ltype));
+
+ lease = lmptr_->getLease6(ltype, lease->addr_);
+ ASSERT_TRUE(lease);
+
+ // Now delete the lease from the store and verify counts.
+ ASSERT_NO_THROW_LOG(lmptr_->deleteLease(lease));
+ EXPECT_EQ(0, lmptr_->getClassLeaseCount("water", ltype));
+ EXPECT_EQ(0, lmptr_->getClassLeaseCount("melon", ltype));
+
+ lease = lmptr_->getLease6(ltype, lease->addr_);
+ ASSERT_FALSE(lease);
+}
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
diff --git a/src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.h b/src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.h
new file mode 100644
index 0000000..fc45b86
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.h
@@ -0,0 +1,694 @@
+// Copyright (C) 2014-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef GENERIC_LEASE_MGR_UNITTEST_H
+#define GENERIC_LEASE_MGR_UNITTEST_H
+
+#include <asiolink/io_service.h>
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/timer_mgr.h>
+
+#include <gtest/gtest.h>
+
+#include <boost/make_shared.hpp>
+#include <vector>
+#include <set>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief typedefs to simplify lease statistic testing
+typedef std::map<std::string, int64_t> StatValMap;
+typedef std::pair<std::string, int64_t> StatValPair;
+typedef std::vector<StatValMap> StatValMapList;
+typedef std::set<LeaseStatsRow> RowSet;
+
+/// @brief Test Fixture class with utility functions for LeaseMgr backends
+///
+/// It contains utility functions, like dummy lease creation.
+/// All concrete LeaseMgr test classes should be derived from it.
+class GenericLeaseMgrTest : public ::testing::Test {
+public:
+ /// @brief Universe (V4 or V6).
+ enum Universe { V4, V6 };
+
+ /// @brief Default constructor.
+ GenericLeaseMgrTest();
+
+ /// @brief Virtual destructor.
+ virtual ~GenericLeaseMgrTest();
+
+ /// @brief Reopen the database
+ ///
+ /// Closes the database and re-opens it. It must be implemented
+ /// in derived classes.
+ ///
+ /// @param u Universe (V4 or V6), required by some backends.
+ virtual void reopen(Universe u = V4) = 0;
+
+ /// @brief Initialize Lease4 Fields
+ ///
+ /// Returns a pointer to a Lease4 structure. Different values are put into
+ /// the lease according to the address passed.
+ ///
+ /// This is just a convenience function for the test methods.
+ ///
+ /// @param address Address to use for the initialization
+ ///
+ /// @return Lease4Ptr. This will not point to anything if the
+ /// initialization failed (e.g. unknown address).
+ Lease4Ptr initializeLease4(std::string address);
+
+ /// @brief Initialize Lease6 Fields
+ ///
+ /// Returns a pointer to a Lease6 structure. Different values are put into
+ /// the lease according to the address passed.
+ ///
+ /// This is just a convenience function for the test methods.
+ ///
+ /// @param address Address to use for the initialization
+ ///
+ /// @return Lease6Ptr. This will not point to anything if the
+ /// initialization failed (e.g. unknown address).
+ Lease6Ptr initializeLease6(std::string address);
+
+ /// @brief Check Leases present and different
+ ///
+ /// Checks a vector of lease pointers and ensures that all the leases
+ /// they point to are present and different. If not, a GTest assertion
+ /// will fail.
+ ///
+ /// @param leases Vector of pointers to leases
+ /// @tparam Type of the leases held in the vector: @c Lease4 or
+ /// @c Lease6.
+ template <typename T>
+ void checkLeasesDifferent(const std::vector<T>& leases) const;
+
+ /// @brief Creates leases for the test
+ ///
+ /// Creates all leases for the test and checks that they are different.
+ ///
+ /// @return vector<Lease4Ptr> Vector of pointers to leases
+ std::vector<Lease4Ptr> createLeases4();
+
+ /// @brief Creates leases for the test
+ ///
+ /// Creates all leases for the test and checks that they are different.
+ ///
+ /// @return vector<Lease6Ptr> Vector of pointers to leases
+ std::vector<Lease6Ptr> createLeases6();
+
+ /// @brief Compares a StatsMgr statistic to an expected value
+ ///
+ /// Attempt to fetch the named statistic from the StatsMgr and if
+ /// found, compare its observed value to the given value.
+ /// Fails if the stat is not found or if the values do not match.
+ ///
+ /// @param name StatsMgr name for the statistic to check
+ /// @param expected_value expected value of the statistic
+ void checkStat(const std::string& name, const int64_t expected_value);
+
+ /// @brief Compares StatsMgr statistics against an expected list of values
+ ///
+ /// Iterates over a list of statistic names and expected values, attempting
+ /// to fetch each from the StatsMgr and if found, compare its observed value
+ /// to the expected value. Fails if any of the expected stats are not
+ /// found or if the values do not match.
+ ///
+ /// @param expected_stats Map of expected static names and values.
+ void checkLeaseStats(const StatValMapList& expected_stats);
+
+ /// @brief Constructs a minimal IPv4 lease and adds it to the lease storage
+ ///
+ /// @param address - IPv4 address for the lease
+ /// @param subnet_id - subnet ID to which the lease belongs
+ /// @param state - the state of the lease
+ /// @param user_context - the lease's user context
+ ///
+ /// @return pointer to created Lease4
+ Lease4Ptr makeLease4(const std::string& address,
+ const SubnetID& subnet_id,
+ const uint32_t state = Lease::STATE_DEFAULT,
+ const data::ConstElementPtr user_context = data::ConstElementPtr());
+
+ /// @brief Constructs a minimal IPv6 lease and adds it to the lease storage
+ ///
+ /// The DUID is constructed from the address and prefix length.
+ ///
+ /// @param type - type of lease to create (TYPE_NA, TYPE_PD...)
+ /// @param address - IPv6 address/prefix for the lease
+ /// @param prefix_len = length of the prefix (should be 0 for TYPE_NA)
+ /// @param subnet_id - subnet ID to which the lease belongs
+ /// @param state - the state of the lease
+ /// @param user_context - the lease's user context
+ ///
+ /// @return pointer to created Lease6
+ Lease6Ptr makeLease6(const Lease::Type& type,
+ const std::string& address,
+ uint8_t prefix_len,
+ const SubnetID& subnet_id,
+ const uint32_t state = Lease::STATE_DEFAULT,
+ const data::ConstElementPtr user_context = data::ConstElementPtr());
+
+ /// @brief checks that addLease, getLease4(addr) and deleteLease() works
+ void testBasicLease4();
+
+ /// @brief checks that invalid dates are safely handled.
+ void testMaxDate4();
+
+ /// @brief checks that infinite lifetimes do not overflow.
+ void testInfiniteLifeTime4();
+
+ /// @brief Test lease retrieval using client id.
+ void testGetLease4ClientId();
+
+ /// @brief Test lease retrieval when leases with NULL client id are present.
+ void testGetLease4NullClientId();
+
+ /// @brief Test lease retrieval using HW address.
+ void testGetLease4HWAddr1();
+
+ /// @brief Check GetLease4 methods - access by Hardware Address
+ ///
+ /// Adds leases to the database and checks that they can be accessed using
+ /// HWAddr information.
+ void testGetLease4HWAddr2();
+
+ /// @brief Get lease4 by hardware address (2)
+ ///
+ /// Check that the system can cope with getting a hardware address of
+ /// any size.
+ void testGetLease4HWAddrSize();
+
+ /// @brief Check GetLease4 methods - access by Hardware Address & Subnet ID
+ ///
+ /// Adds leases to the database and checks that they can be accessed via
+ /// a combination of hardware address and subnet ID
+ void testGetLease4HWAddrSubnetId();
+
+ /// @brief Get lease4 by hardware address and subnet ID (2)
+ ///
+ /// Check that the system can cope with getting a hardware address of
+ /// any size.
+ void testGetLease4HWAddrSubnetIdSize();
+
+ /// @brief Check GetLease4 methods - access by Client ID
+ ///
+ /// Adds leases to the database and checks that they can be accessed via
+ /// the Client ID.
+ void testGetLease4ClientId2();
+
+ /// @brief Get Lease4 by client ID (2)
+ ///
+ /// Check that the system can cope with a client ID of any size.
+ void testGetLease4ClientIdSize();
+
+ /// @brief Check GetLease4 methods - access by Client ID & Subnet ID
+ ///
+ /// Adds leases to the database and checks that they can be accessed via
+ /// a combination of client and subnet IDs.
+ void testGetLease4ClientIdSubnetId();
+
+ /// @brief Test method which returns all IPv4 leases for Subnet ID.
+ void testGetLeases4SubnetId();
+
+ /// @brief Test method which returns all IPv4 leases for Hostname.
+ void testGetLeases4Hostname();
+
+ /// @brief Test method which returns all IPv4 leases.
+ void testGetLeases4();
+
+ /// @brief Test method which returns range of IPv4 leases with paging.
+ void testGetLeases4Paged();
+
+ /// @brief Test method which returns all IPv6 leases for Subnet ID.
+ void testGetLeases6SubnetId();
+
+ /// @brief Test method which returns all IPv6 leases for Hostname.
+ void testGetLeases6Hostname();
+
+ /// @brief Test making/fetching leases with IAIDs > signed 32-bit max.
+ void testLease6LargeIaidCheck();
+
+ /// @brief Test method which returns all IPv6 leases.
+ void testGetLeases6();
+
+ /// @brief Test method which returns range of IPv6 leases with paging.
+ void testGetLeases6Paged();
+
+ /// @brief Basic Lease4 Checks
+ ///
+ /// Checks that the addLease, getLease4(by address), getLease4(hwaddr,subnet_id),
+ /// updateLease4() and deleteLease (IPv4 address) can handle NULL client-id.
+ /// (client-id is optional and may not be present)
+ ///
+ /// @todo: check if it does overlap with @ref testGetLease4NullClientId()
+ void testLease4NullClientId();
+
+ /// @brief Check that the DHCPv4 lease can be added, removed and recreated.
+ ///
+ /// This test creates a lease, removes it and then recreates it with some
+ /// of the attributes changed. Next it verifies that the lease in the
+ /// persistent storage has been updated as expected.
+ void testRecreateLease4();
+
+ /// @brief Basic Lease6 Checks
+ ///
+ /// Checks that the addLease, getLease6 (by address) and deleteLease (with an
+ /// IPv6 address) works.
+ void testBasicLease6();
+
+ /// @brief Checks that invalid dates are safely handled.
+ void testMaxDate6();
+
+ /// @brief checks that infinite lifetimes do not overflow.
+ void testInfiniteLifeTime6();
+
+ /// @brief Checks that Lease6 can be stored with and without a hardware address.
+ void testLease6MAC();
+
+ /// @brief Checks that Lease6 stores hardware type and hardware source.
+ void testLease6HWTypeAndSource();
+
+ /// @brief Test that IPv6 lease can be added, retrieved and deleted.
+ void testAddGetDelete6();
+
+ /// @brief Check GetLease6 methods - access by DUID/IAID
+ ///
+ /// Adds leases to the database and checks that they can be accessed via
+ /// a combination of DUID and IAID.
+ void testGetLeases6DuidIaid();
+
+ /// @brief Check that the system can cope with a DUID of allowed size.
+ void testGetLeases6DuidSize();
+
+ /// @brief Check that getLease6 methods discriminate by lease type.
+ ///
+ /// Adds six leases, two per lease type all with the same duid and iad but
+ /// with alternating subnet_ids.
+ /// It then verifies that all of getLeases6() method variants correctly
+ /// discriminate between the leases based on lease type alone.
+ void testLease6LeaseTypeCheck();
+
+ /// @brief Check GetLease6 methods - access by DUID/IAID/SubnetID
+ ///
+ /// Adds leases to the database and checks that they can be accessed via
+ /// a combination of DIUID and IAID.
+ void testGetLease6DuidIaidSubnetId();
+
+ /// @brief verifies getLeases6 method by DUID
+ ///
+ /// Adds 3 leases to backend and retrieves, verifes empty
+ /// retrieval of non existent DUID.
+ void testGetLeases6Duid();
+
+ /// @brief Checks that getLease6() works with different DUID sizes
+ void testGetLease6DuidIaidSubnetIdSize();
+
+ /// @brief Verify that too long hostname for Lease4 is not accepted.
+ ///
+ /// Checks that the it is not possible to create a lease when the hostname
+ /// length exceeds 255 characters.
+ void testLease4InvalidHostname();
+
+ /// @brief Verify that too long hostname for Lease6 is not accepted.
+ ///
+ /// Checks that the it is not possible to create a lease when the hostname
+ /// length exceeds 255 characters.
+ void testLease6InvalidHostname();
+
+ /// @brief Lease4 update test
+ ///
+ /// Checks that the code is able to update an IPv4 lease in the database.
+ void testUpdateLease4();
+
+ /// @brief Lease4 concurrent update test
+ ///
+ /// Checks that the code is not able to concurrently update an IPv4 lease in
+ /// the database.
+ void testConcurrentUpdateLease4();
+
+ /// @brief Lease6 update test
+ ///
+ /// Checks that the code is able to update an IPv6 lease in the database.
+ void testUpdateLease6();
+
+ /// @brief Lease6 concurrent update test
+ ///
+ /// Checks that the code is not able to concurrently update an IPv6 lease in
+ /// the database.
+ void testConcurrentUpdateLease6();
+
+ /// @brief Check that the IPv6 lease can be added, removed and recreated.
+ ///
+ /// This test creates a lease, removes it and then recreates it with some
+ /// of the attributes changed. Next it verifies that the lease in the
+ /// persistent storage has been updated as expected.
+ void testRecreateLease6();
+
+ /// @brief Verifies that a null DUID is not allowed.
+ void testNullDuid();
+
+ /// @brief Verifies that the backend reports expected version numbers.
+ /// @param major Expected major version to be reported.
+ /// @param minor Expected minor version to be reported.
+ void testVersion(int major, int minor);
+
+ /// @brief Checks that the expired DHCPv4 leases can be retrieved.
+ ///
+ /// This test checks the following:
+ /// - all expired and not reclaimed leases are returned
+ /// - number of leases returned can be limited
+ /// - leases are returned in the order from the most expired to the
+ /// least expired
+ /// - reclaimed leases are not returned.
+ void testGetExpiredLeases4();
+
+ /// @brief Checks that the expired IPv6 leases can be retrieved.
+ ///
+ /// This test checks the following:
+ /// - all expired and not reclaimed leases are returned
+ /// - number of leases returned can be limited
+ /// - leases are returned in the order from the most expired to the
+ /// least expired
+ /// - reclaimed leases are not returned.
+ void testGetExpiredLeases6();
+
+ /// @brief Checks that DHCPv4 leases with infinite valid lifetime
+ /// will never expire.
+ void testInfiniteAreNotExpired4();
+
+ /// @brief Checks that DHCPv6 leases with infinite valid lifetime
+ /// will never expire.
+ void testInfiniteAreNotExpired6();
+
+ /// @brief Checks that declined IPv4 leases that have expired can be retrieved.
+ ///
+ /// This test checks that the following:
+ /// - all expired and not reclaimed leases are returned, regardless if
+ /// they're normal or declined
+ /// - the order in which they're updated in LeaseMgr doesn't matter
+ /// - leases are returned in the order from most expired to the least
+ /// expired
+ void testGetDeclinedLeases4();
+
+ /// @brief Checks that declined IPv6 leases that have expired can be retrieved.
+ ///
+ /// This test checks that the following:
+ /// - all expired and not reclaimed leases are returned, regardless if
+ /// they're normal or declined
+ /// - the order in which they're updated in LeaseMgr doesn't matter
+ /// - leases are returned in the order from most expired to the least
+ /// expired
+ void testGetDeclinedLeases6();
+
+ /// @brief Checks that selected expired-reclaimed IPv6 leases
+ /// are removed.
+ ///
+ /// This creates a number of DHCPv6 leases and marks some of them
+ /// as expired-reclaimed. It later verifies that the expired-reclaimed
+ /// leases can be removed.
+ void testDeleteExpiredReclaimedLeases6();
+
+ /// @brief Checks that selected expired-reclaimed IPv4 leases
+ /// are removed.
+ ///
+ /// This creates a number of DHCPv4 leases and marks some of them
+ /// as expired-reclaimed. It later verifies that the expired-reclaimed
+ /// leases can be removed.
+ void testDeleteExpiredReclaimedLeases4();
+
+ /// @brief Check that the IPv4 lease statistics can be recounted
+ ///
+ /// This test creates two subnets and several leases associated with
+ /// them, then verifies that lease statistics are recalculated correctly
+ /// after altering the lease states in various ways.
+ void testRecountLeaseStats4();
+
+ /// @brief Check that the IPv6 lease statistics can be recounted
+ ///
+ /// This test creates two subnets and several leases associated with
+ /// them, then verifies that lease statistics are recalculated correctly
+ /// after altering the lease states in various ways.
+ void testRecountLeaseStats6();
+
+
+ /// @brief Check if wipeLeases4 works properly.
+ ///
+ /// This test creates a bunch of leases in several subnets and then
+ /// attempts to delete them, one subnet at a time.
+ void testWipeLeases4();
+
+ /// @brief Check if wipeLeases6 works properly.
+ ///
+ /// This test creates a bunch of leases in several subnets and then
+ /// attempts to delete them, one subnet at a time.
+ void testWipeLeases6();
+
+ /// @brief Checks operation of v4 LeaseStatsQuery variants
+ ///
+ /// It creates three subnets with leases in various states in
+ /// each. It runs and verifies the returned query contents for
+ /// each of the v4 startLeaseQuery variants:
+ ///
+ /// - startSubnetLeaseQuery()
+ /// - startSubneRangetLeaseQuery()
+ /// - startLeaseQuery()
+ ///
+ void testLeaseStatsQuery4();
+
+ /// @brief Checks operation of v6 LeaseStatsQuery variants
+ ///
+ /// It creates three subnets with leases in various states in
+ /// each. It runs and verifies the returned query contents for
+ /// each of the v6 startLeaseQuery variants:
+ ///
+ /// - startSubnetLeaseQuery()
+ /// - startSubneRangetLeaseQuery()
+ /// - startLeaseQuery()
+ ///
+ void testLeaseStatsQuery6();
+
+ /// @brief Checks if v4 LeaseStatsQuery can get bad attribution.
+ ///
+ /// It creates two subnets with leases and move one from the first
+ /// to the second. If counters are not updated this can lead to
+ /// bad attribution i.e. a lease is counted in a subnet when it
+ /// belongs to another one.
+ ///
+ void testLeaseStatsQueryAttribution4();
+
+ /// @brief Checks if v6 LeaseStatsQuery can get bad attribution.
+ ///
+ /// It creates two subnets with leases and move one from the first
+ /// to the second. If counters are not updated this can lead to
+ /// bad attribution i.e. a lease is counted in a subnet when it
+ /// belongs to another one.
+ ///
+ /// @note We can check the lease type change too but in the real
+ /// world this never happens.
+ void testLeaseStatsQueryAttribution6();
+
+ /// @brief Compares LeaseQueryStats content to expected set of rows
+ ///
+ /// @param qry - a started LeaseStatsQuery
+ /// @param row_set - set of rows expected to be found in the query rows
+ void checkQueryAgainstRowSet(const LeaseStatsQueryPtr& qry, const RowSet& row_set);
+
+ /// @brief Checks if specified range of leases was returned.
+ ///
+ /// @param returned collection of leases returned.
+ /// @param expected_addresses ordered collection of expected addresses.
+ void checkLeaseRange(const Lease4Collection& returned,
+ const std::vector<std::string>& expected_addresses);
+
+ /// @brief Create a user-context with a given list of classes
+ ///
+ /// Creates an Element::map with the following content:
+ ///
+ /// {
+ /// "ISC": {
+ /// "classes": [ "class0", "class1", ... ]
+ /// }
+ /// }
+ ///
+ /// @param classes list of classes to include in the context
+ /// @return ElementPtr containing the user-context
+ data::ElementPtr makeContextWithClasses(const std::list<ClientClass>& classes);
+
+ /// @brief Tests class lease counts when adding, updating, and deleting
+ /// leases with class lists.
+ void testClassLeaseCount4();
+
+ /// @brief Tests class lease counts when adding, updating, and deleting
+ /// leases with class lists.
+ ///
+ /// @param ltype type of lease, either Lease::TYPE_NA or Lease::TYPE_PD
+ void testClassLeaseCount6(Lease::Type ltype);
+
+ /// @brief Checks a few v4 lease limit checking scenarios.
+ void testLeaseLimits4();
+
+ /// @brief Checks a few v6 lease limit checking scenarios.
+ void testLeaseLimits6();
+
+ /// @brief String forms of IPv4 addresses
+ std::vector<std::string> straddress4_;
+
+ /// @brief IOAddress forms of IPv4 addresses
+ std::vector<isc::asiolink::IOAddress> ioaddress4_;
+
+ /// @brief String forms of IPv6 addresses
+ std::vector<std::string> straddress6_;
+
+ /// @brief Types of IPv6 Leases
+ std::vector<Lease::Type> leasetype6_;
+
+ /// @brief IOAddress forms of IPv6 addresses
+ std::vector<isc::asiolink::IOAddress> ioaddress6_;
+
+ /// @brief Pointer to the lease manager
+ LeaseMgr* lmptr_;
+};
+
+class LeaseMgrDbLostCallbackTest : public ::testing::Test {
+public:
+ LeaseMgrDbLostCallbackTest()
+ : db_lost_callback_called_(0), db_recovered_callback_called_(0),
+ db_failed_callback_called_(0),
+ io_service_(boost::make_shared<isc::asiolink::IOService>()) {
+ db::DatabaseConnection::db_lost_callback_ = 0;
+ db::DatabaseConnection::db_recovered_callback_ = 0;
+ db::DatabaseConnection::db_failed_callback_ = 0;
+ LeaseMgr::setIOService(io_service_);
+ TimerMgr::instance()->setIOService(io_service_);
+ }
+
+ virtual ~LeaseMgrDbLostCallbackTest() {
+ db::DatabaseConnection::db_lost_callback_ = 0;
+ db::DatabaseConnection::db_recovered_callback_ = 0;
+ db::DatabaseConnection::db_failed_callback_ = 0;
+ LeaseMgr::setIOService(isc::asiolink::IOServicePtr());
+ TimerMgr::instance()->unregisterTimers();
+ }
+
+ /// @brief Prepares the class for a test.
+ ///
+ /// Invoked by gtest prior test entry, we create the
+ /// appropriate schema and wipe out any residual lease manager
+ virtual void SetUp();
+
+ /// @brief Pre-text exit clean up
+ ///
+ /// Invoked by gtest upon test exit, we destroy the schema
+ /// we created and toss our lease manager.
+ virtual void TearDown();
+
+ /// @brief Abstract method for destroying the back end specific schema
+ virtual void destroySchema() = 0;
+
+ /// @brief Abstract method for creating the back end specific schema
+ virtual void createSchema() = 0;
+
+ /// @brief Abstract method which returns the back end specific connection
+ /// string
+ virtual std::string validConnectString() = 0;
+
+ /// @brief Abstract method which returns invalid back end specific connection
+ /// string
+ virtual std::string invalidConnectString() = 0;
+
+ /// @brief Verifies open failures do NOT invoke db lost callback
+ ///
+ /// The db lost callback should only be invoked after successfully
+ /// opening the DB and then subsequently losing it. Failing to
+ /// open should be handled directly by the application layer.
+ void testNoCallbackOnOpenFailure();
+
+ /// @brief Verifies the lease manager's behavior if DB connection is lost
+ ///
+ /// This function creates a lease manager with a back end that supports
+ /// connectivity lost callback (currently only MySQL and PostgreSQL). It
+ /// verifies connectivity by issuing a known valid query. Next it simulates
+ /// connectivity lost by identifying and closing the socket connection to
+ /// the CB backend. It then reissues the query and verifies that:
+ /// -# The Query throws DbOperationError (rather than exiting)
+ /// -# The registered DbLostCallback was invoked
+ /// -# The registered DbRecoveredCallback was invoked
+ void testDbLostAndRecoveredCallback();
+
+ /// @brief Verifies the lease manager's behavior if DB connection is lost
+ ///
+ /// This function creates a lease manager with a back end that supports
+ /// connectivity lost callback (currently only MySQL and PostgreSQL). It
+ /// verifies connectivity by issuing a known valid query. Next it simulates
+ /// connectivity lost by identifying and closing the socket connection to
+ /// the CB backend. It then reissues the query and verifies that:
+ /// -# The Query throws DbOperationError (rather than exiting)
+ /// -# The registered DbLostCallback was invoked
+ /// -# The registered DbFailedCallback was invoked
+ void testDbLostAndFailedCallback();
+
+ /// @brief Verifies the lease manager's behavior if DB connection is lost
+ ///
+ /// This function creates a lease manager with a back end that supports
+ /// connectivity lost callback (currently only MySQL and PostgreSQL). It
+ /// verifies connectivity by issuing a known valid query. Next it simulates
+ /// connectivity lost by identifying and closing the socket connection to
+ /// the CB backend. It then reissues the query and verifies that:
+ /// -# The Query throws DbOperationError (rather than exiting)
+ /// -# The registered DbLostCallback was invoked
+ /// -# The registered DbRecoveredCallback was invoked after two reconnect
+ /// attempts (once failing and second triggered by timer)
+ void testDbLostAndRecoveredAfterTimeoutCallback();
+
+ /// @brief Verifies the lease manager's behavior if DB connection is lost
+ ///
+ /// This function creates a lease manager with a back end that supports
+ /// connectivity lost callback (currently only MySQL and PostgreSQL). It
+ /// verifies connectivity by issuing a known valid query. Next it simulates
+ /// connectivity lost by identifying and closing the socket connection to
+ /// the CB backend. It then reissues the query and verifies that:
+ /// -# The Query throws DbOperationError (rather than exiting)
+ /// -# The registered DbLostCallback was invoked
+ /// -# The registered DbFailedCallback was invoked after two reconnect
+ /// attempts (once failing and second triggered by timer)
+ void testDbLostAndFailedAfterTimeoutCallback();
+
+ /// @brief Callback function registered with the lease manager
+ bool db_lost_callback(util::ReconnectCtlPtr /* not_used */) {
+ return (++db_lost_callback_called_);
+ }
+
+ /// @brief Flag used to detect calls to db_lost_callback function
+ uint32_t db_lost_callback_called_;
+
+ /// @brief Callback function registered with the lease manager
+ bool db_recovered_callback(util::ReconnectCtlPtr /* not_used */) {
+ return (++db_recovered_callback_called_);
+ }
+
+ /// @brief Flag used to detect calls to db_recovered_callback function
+ uint32_t db_recovered_callback_called_;
+
+ /// @brief Callback function registered with the lease manager
+ bool db_failed_callback(util::ReconnectCtlPtr /* not_used */) {
+ return (++db_failed_callback_called_);
+ }
+
+ /// @brief Flag used to detect calls to db_failed_callback function
+ uint32_t db_failed_callback_called_;
+
+ /// The IOService object, used for all ASIO operations.
+ isc::asiolink::IOServicePtr io_service_;
+};
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
+
+#endif
diff --git a/src/lib/dhcpsrv/tests/host_cache_unittest.cc b/src/lib/dhcpsrv/tests/host_cache_unittest.cc
new file mode 100644
index 0000000..c6ad920
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/host_cache_unittest.cc
@@ -0,0 +1,996 @@
+// Copyright (C) 2018-2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcpsrv/host_data_source_factory.h>
+#include <dhcpsrv/cache_host_data_source.h>
+#include <dhcpsrv/host_mgr.h>
+#include <dhcpsrv/testutils/host_data_source_utils.h>
+#include <dhcpsrv/testutils/memory_host_data_source.h>
+#include <exceptions/exceptions.h>
+
+#include <gtest/gtest.h>
+
+#include <iostream>
+#include <sstream>
+
+using namespace std;
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::db;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+
+namespace {
+
+/// @brief Test dump cache class.
+class TestHostCache : public MemHostDataSource, public CacheHostDataSource {
+public:
+
+ /// Constructor
+ TestHostCache() : adds_(0), inserts_(0) { }
+
+ /// Destructor
+ virtual ~TestHostCache() { }
+
+ /// Override add
+ void add(const HostPtr& host) {
+ isc_throw(NotImplemented,
+ "add is not implemented: " << host->toText());
+ }
+
+ /// Insert
+ size_t insert(const ConstHostPtr& host, bool overwrite) {
+ if (overwrite) {
+ ++inserts_;
+ } else {
+ ++adds_;
+ }
+ HostPtr host_copy(new Host(*host));
+ store_.push_back(host_copy);
+ return (0);
+ }
+
+ /// Remove
+ bool remove(const HostPtr& host) {
+ for (auto h = store_.begin(); h != store_.end(); ++h) {
+ if (*h == host) {
+ store_.erase(h);
+ return (true);
+ }
+ }
+ return (false);
+ }
+
+ /// Flush
+ void flush(size_t) {
+ isc_throw(NotImplemented, "flush is not implemented");
+ }
+
+ /// Size
+ size_t size() const {
+ return (store_.size());
+ }
+
+ /// Capacity
+ size_t capacity() const {
+ return (0);
+ }
+
+ /// Type
+ string getType() const {
+ return ("cache");
+ }
+
+ /// Add counter
+ size_t adds_;
+
+ /// Insert counter
+ size_t inserts_;
+};
+
+/// @brief TestHostCache pointer type
+typedef boost::shared_ptr<TestHostCache> TestHostCachePtr;
+
+/// @brief Test data source class.
+class TestHostDataSource : public MemHostDataSource {
+public:
+
+ /// Destructor
+ virtual ~TestHostDataSource() { }
+
+ /// Type
+ string getType() const {
+ return ("test");
+ }
+};
+
+/// @brief TestHostDataSource pointer type
+typedef boost::shared_ptr<TestHostDataSource> TestHostDataSourcePtr;
+
+/// @brief Test fixture for testing cache feature.
+class HostCacheTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ HostCacheTest() {
+ HostMgr::create();
+
+ // Host cache.
+ hcptr_.reset(new TestHostCache());
+ auto cacheFactory = [this](const DatabaseConnection::ParameterMap&) {
+ return (hcptr_);
+ };
+ HostDataSourceFactory::registerFactory("cache", cacheFactory);
+ HostMgr::addBackend("type=cache");
+
+ // Host data source.
+ memptr_.reset(new TestHostDataSource());
+ auto testFactory = [this](const DatabaseConnection::ParameterMap&) {
+ return (memptr_);
+ };
+ HostDataSourceFactory::registerFactory("test", testFactory);
+ HostMgr::addBackend("type=test");
+ }
+
+ /// @brief Destructor.
+ virtual ~HostCacheTest() {
+ HostDataSourceFactory::deregisterFactory("test");
+ HostDataSourceFactory::deregisterFactory("cache");
+ }
+
+ /// @brief Test host cache.
+ TestHostCachePtr hcptr_;
+
+ /// @brief Test host data source.
+ TestHostDataSourcePtr memptr_;
+};
+
+// Check basic cache feature for IPv4.
+TEST_F(HostCacheTest, identifier4) {
+ // Check we have what we need.
+ ASSERT_TRUE(hcptr_);
+ EXPECT_TRUE(HostMgr::checkCacheBackend());
+ EXPECT_EQ(0, hcptr_->size());
+ EXPECT_EQ(0, hcptr_->inserts_);
+ ASSERT_TRUE(memptr_);
+
+ // Create a host reservation.
+ HostPtr host = HostDataSourceUtils::initializeHost4("192.0.2.1",
+ Host::IDENT_HWADDR);
+ ASSERT_TRUE(host); // Make sure the host is generated properly.
+ const IOAddress& address = host->getIPv4Reservation();
+
+ // Try to add it to the host data source.
+ ASSERT_NO_THROW(memptr_->add(host));
+
+ // Try to get it cached.
+ ConstHostPtr got = HostMgr::instance().get4(host->getIPv4SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_TRUE(got);
+ HostDataSourceUtils::compareHosts(got, host);
+
+ // Verify it was cached.
+ EXPECT_EQ(1, hcptr_->size());
+ EXPECT_EQ(1, hcptr_->inserts_);
+
+ // Remove it from test host data source.
+ EXPECT_TRUE(memptr_->del(host->getIPv4SubnetID(), address));
+
+ // It is cached so we can still get it.
+ got = HostMgr::instance().get4(host->getIPv4SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_TRUE(got);
+ HostDataSourceUtils::compareHosts(got, host);
+
+ // Even by address.
+ got = HostMgr::instance().get4(host->getIPv4SubnetID(), address);
+ ASSERT_TRUE(got);
+ HostDataSourceUtils::compareHosts(got, host);
+
+ // Verify cache status.
+ EXPECT_EQ(1, hcptr_->size());
+ EXPECT_EQ(1, hcptr_->inserts_);
+}
+
+// Check basic cache feature for IPv6.
+TEST_F(HostCacheTest, identifier6) {
+ // Check we have what we need.
+ ASSERT_TRUE(hcptr_);
+ EXPECT_TRUE(HostMgr::checkCacheBackend());
+ EXPECT_EQ(0, hcptr_->size());
+ EXPECT_EQ(0, hcptr_->inserts_);
+ ASSERT_TRUE(memptr_);
+
+ // Create a host reservation.
+ HostPtr host = HostDataSourceUtils::initializeHost6("2001:db8::1",
+ Host::IDENT_DUID,
+ false);
+ ASSERT_TRUE(host); // Make sure the host is generated properly.
+
+ // Get the address.
+ IPv6ResrvRange resrvs = host->getIPv6Reservations();
+ ASSERT_EQ(1, std::distance(resrvs.first, resrvs.second));
+ const IOAddress& address = resrvs.first->second.getPrefix();
+ ASSERT_EQ("2001:db8::1", address.toText());
+
+ // Try to add it to the host data source.
+ ASSERT_NO_THROW(memptr_->add(host));
+
+ // Try to get it cached.
+ ConstHostPtr got = HostMgr::instance().get6(host->getIPv6SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_TRUE(got);
+ HostDataSourceUtils::compareHosts(got, host);
+
+ // Verify it was cached.
+ EXPECT_EQ(1, hcptr_->size());
+ EXPECT_EQ(1, hcptr_->inserts_);
+
+ // Remove it from test host data source.
+ EXPECT_TRUE(memptr_->del(host->getIPv6SubnetID(), address));
+
+ // It is cached so we can still get it.
+ got = HostMgr::instance().get6(host->getIPv6SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_TRUE(got);
+ HostDataSourceUtils::compareHosts(got, host);
+
+ // Even by address.
+ got = HostMgr::instance().get6(host->getIPv6SubnetID(), address);
+ ASSERT_TRUE(got);
+ HostDataSourceUtils::compareHosts(got, host);
+
+ // Verify cache status.
+ EXPECT_EQ(1, hcptr_->size());
+ EXPECT_EQ(1, hcptr_->inserts_);
+}
+
+// Check by address caching for IPv4.
+TEST_F(HostCacheTest, address4) {
+ // Check we have what we need.
+ ASSERT_TRUE(hcptr_);
+ EXPECT_TRUE(HostMgr::checkCacheBackend());
+ EXPECT_EQ(0, hcptr_->size());
+ EXPECT_EQ(0, hcptr_->inserts_);
+ ASSERT_TRUE(memptr_);
+
+ // Create a host reservation.
+ HostPtr host = HostDataSourceUtils::initializeHost4("192.0.2.1",
+ Host::IDENT_HWADDR);
+ ASSERT_TRUE(host); // Make sure the host is generated properly.
+ const IOAddress& address = host->getIPv4Reservation();
+
+ // Try to add it to the host data source.
+ ASSERT_NO_THROW(memptr_->add(host));
+
+ // Try to get it cached.
+ ConstHostPtr got = HostMgr::instance().get4(host->getIPv4SubnetID(),
+ address);
+ ASSERT_TRUE(got);
+ HostDataSourceUtils::compareHosts(got, host);
+
+ // Verify it was cached.
+ EXPECT_EQ(1, hcptr_->size());
+ EXPECT_EQ(1, hcptr_->inserts_);
+
+ // Remove it from test host data source.
+ EXPECT_TRUE(memptr_->del(host->getIPv4SubnetID(), address));
+
+ // It is cached so we can still get it.
+ got = HostMgr::instance().get4(host->getIPv4SubnetID(), address);
+ ASSERT_TRUE(got);
+ HostDataSourceUtils::compareHosts(got, host);
+
+ // Even by identifier.
+ got = HostMgr::instance().get4(host->getIPv4SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_TRUE(got);
+ HostDataSourceUtils::compareHosts(got, host);
+
+ // Verify cache status.
+ EXPECT_EQ(1, hcptr_->size());
+ EXPECT_EQ(1, hcptr_->inserts_);
+}
+
+// Check by address caching for IPv6.
+TEST_F(HostCacheTest, address6) {
+ // Check we have what we need.
+ ASSERT_TRUE(hcptr_);
+ EXPECT_TRUE(HostMgr::checkCacheBackend());
+ EXPECT_EQ(0, hcptr_->size());
+ EXPECT_EQ(0, hcptr_->inserts_);
+ ASSERT_TRUE(memptr_);
+
+ // Create a host reservation.
+ HostPtr host = HostDataSourceUtils::initializeHost6("2001:db8::1",
+ Host::IDENT_DUID,
+ false);
+ ASSERT_TRUE(host); // Make sure the host is generated properly.
+
+ // Get the address.
+ IPv6ResrvRange resrvs = host->getIPv6Reservations();
+ ASSERT_EQ(1, std::distance(resrvs.first, resrvs.second));
+ const IOAddress& address = resrvs.first->second.getPrefix();
+ ASSERT_EQ("2001:db8::1", address.toText());
+
+ // Try to add it to the host data source.
+ ASSERT_NO_THROW(memptr_->add(host));
+
+ // Try to get it cached.
+ ConstHostPtr got = HostMgr::instance().get6(host->getIPv6SubnetID(),
+ address);
+ ASSERT_TRUE(got);
+ HostDataSourceUtils::compareHosts(got, host);
+
+ // Verify it was cached.
+ EXPECT_EQ(1, hcptr_->size());
+ EXPECT_EQ(1, hcptr_->inserts_);
+
+ // Remove it from test host data source.
+ EXPECT_TRUE(memptr_->del(host->getIPv6SubnetID(), address));
+
+ // It is cached so we can still get it.
+ got = HostMgr::instance().get6(host->getIPv6SubnetID(), address);
+ ASSERT_TRUE(got);
+ HostDataSourceUtils::compareHosts(got, host);
+
+ // Even by identifier.
+ got = HostMgr::instance().get6(host->getIPv6SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_TRUE(got);
+ HostDataSourceUtils::compareHosts(got, host);
+
+ // Verify cache status.
+ EXPECT_EQ(1, hcptr_->size());
+ EXPECT_EQ(1, hcptr_->inserts_);
+}
+
+// Check negative cache feature for IPv4.
+TEST_F(HostCacheTest, negativeIdentifier4) {
+ // Check we have what we need.
+ ASSERT_TRUE(hcptr_);
+ EXPECT_TRUE(HostMgr::checkCacheBackend());
+ EXPECT_EQ(0, hcptr_->size());
+ EXPECT_EQ(0, hcptr_->adds_);
+ ASSERT_TRUE(memptr_);
+ ASSERT_FALSE(HostMgr::instance().getNegativeCaching());
+
+ // Create a host reservation.
+ // We will not add it anywhere, just will use its values.
+ HostPtr host = HostDataSourceUtils::initializeHost4("192.0.2.1", Host::IDENT_HWADDR);
+ ASSERT_TRUE(host);
+
+ // Try to get it cached.
+ ConstHostPtr got = HostMgr::instance().get4(host->getIPv4SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_FALSE(got);
+ // Again to be sure it was not negatively cached.
+ got = HostMgr::instance().get4Any(host->getIPv4SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_FALSE(got);
+
+ // Enable negative caching.
+ HostMgr::instance().setNegativeCaching(true);
+ ASSERT_TRUE(HostMgr::instance().getNegativeCaching());
+
+ // Try it again. There is no such host, but this time negative cache is enabled,
+ // so this negative response will be added to the cache.
+ got = HostMgr::instance().get4(host->getIPv4SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_FALSE(got);
+ EXPECT_EQ(1, hcptr_->size());
+ EXPECT_EQ(1, hcptr_->adds_);
+ got = HostMgr::instance().get4Any(host->getIPv4SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_TRUE(got);
+ EXPECT_TRUE(got->getNegative());
+
+ // Look at inside the negative cached value.
+ EXPECT_EQ(host->getIPv4SubnetID(), got->getIPv4SubnetID());
+ EXPECT_EQ(host->getIdentifierType(), got->getIdentifierType());
+ ASSERT_EQ(host->getIdentifier().size(), got->getIdentifier().size());
+ EXPECT_EQ(0, memcmp(&host->getIdentifier()[0],
+ &got->getIdentifier()[0],
+ host->getIdentifier().size()));
+ EXPECT_EQ("0.0.0.0", got->getIPv4Reservation().toText());
+
+ // get4() (vs get4Any()) does not return negative cached values.
+ got = HostMgr::instance().get4(host->getIPv4SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ EXPECT_FALSE(got);
+
+ // Verify cache status.
+ EXPECT_EQ(1, hcptr_->size());
+ EXPECT_EQ(1, hcptr_->adds_);
+ EXPECT_EQ(0, hcptr_->inserts_);
+
+ // We can verify other overloads of get4() but the hwaddr/duid is
+ // not implemented by the memory test backend and the negative cache
+ // entry has no IP reservation when inserted by the host manager.
+}
+
+// Check negative cache feature for IPv6.
+TEST_F(HostCacheTest, negativeIdentifier6) {
+ // Check we have what we need.
+ ASSERT_TRUE(hcptr_);
+ EXPECT_TRUE(HostMgr::checkCacheBackend());
+ EXPECT_EQ(0, hcptr_->size());
+ EXPECT_EQ(0, hcptr_->adds_);
+ ASSERT_TRUE(memptr_);
+
+ // Create a host reservation.
+ // We will not add it anywhere, just will use its values.
+ HostPtr host = HostDataSourceUtils::initializeHost6("2001:db8::1",
+ Host::IDENT_DUID,
+ false);
+ ASSERT_TRUE(host); // Make sure the host is generated properly.
+
+ // Do not add it to the host data source.
+
+ // Try to get it cached.
+ ConstHostPtr got = HostMgr::instance().get6(host->getIPv6SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_FALSE(got);
+ // Again to be sure it was not negatively cached.
+ got = HostMgr::instance().get6Any(host->getIPv6SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_FALSE(got);
+
+ // Enable negative caching.
+ HostMgr::instance().setNegativeCaching(true);
+ ASSERT_TRUE(HostMgr::instance().getNegativeCaching());
+
+ // Try it but it will be cached only when get6 (vs get6Any) is called.
+ got = HostMgr::instance().get6Any(host->getIPv6SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_FALSE(got);
+ EXPECT_EQ(0, hcptr_->size());
+ EXPECT_EQ(0, hcptr_->adds_);
+
+ got = HostMgr::instance().get6(host->getIPv6SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_FALSE(got);
+
+ // There is a negative cached value now.
+ EXPECT_EQ(1, hcptr_->size());
+ EXPECT_EQ(1, hcptr_->adds_);
+ got = HostMgr::instance().get6Any(host->getIPv6SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ ASSERT_TRUE(got);
+ EXPECT_TRUE(got->getNegative());
+
+ // Look at inside the negative cached value.
+ EXPECT_EQ(host->getIPv6SubnetID(), got->getIPv6SubnetID());
+ EXPECT_EQ(host->getIdentifierType(), got->getIdentifierType());
+ ASSERT_EQ(host->getIdentifier().size(), got->getIdentifier().size());
+ EXPECT_EQ(0, memcmp(&host->getIdentifier()[0],
+ &got->getIdentifier()[0],
+ host->getIdentifier().size()));
+ EXPECT_FALSE(got->hasIPv6Reservation());
+
+ // get6() (vs get6Any()) does not return negative cached values.
+ got = HostMgr::instance().get6(host->getIPv6SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ EXPECT_FALSE(got);
+
+ // Verify cache status.
+ EXPECT_EQ(1, hcptr_->size());
+ EXPECT_EQ(1, hcptr_->adds_);
+ EXPECT_EQ(0, hcptr_->inserts_);
+
+ // No other tests, cf negativeIdentifier4 end comment.
+}
+
+// Check that negative caching by address is not done for IPv4.
+TEST_F(HostCacheTest, negativeAddress4) {
+ // Check we have what we need.
+ ASSERT_TRUE(hcptr_);
+ EXPECT_TRUE(HostMgr::checkCacheBackend());
+ EXPECT_EQ(0, hcptr_->size());
+ EXPECT_EQ(0, hcptr_->adds_);
+ ASSERT_TRUE(memptr_);
+
+ // Create a host reservation.
+ // We will not add it anywhere, just will use its values.
+ HostPtr host = HostDataSourceUtils::initializeHost4("192.0.2.1",
+ Host::IDENT_HWADDR);
+ ASSERT_TRUE(host); // Make sure the host is generated properly.
+ const IOAddress& address = host->getIPv4Reservation();
+
+ // Do not add it to the host data source.
+
+ // Enable negative caching.
+ HostMgr::instance().setNegativeCaching(true);
+ ASSERT_TRUE(HostMgr::instance().getNegativeCaching());
+
+ // Try to get it in negative cache.
+ ConstHostPtr got = HostMgr::instance().get4(host->getIPv4SubnetID(),
+ address);
+ ASSERT_FALSE(got);
+ EXPECT_EQ(0, hcptr_->size());
+ EXPECT_EQ(0, hcptr_->adds_);
+}
+
+// Check that negative caching by address is not done for IPv6.
+TEST_F(HostCacheTest, negativeAddress6) {
+ // Check we have what we need.
+ ASSERT_TRUE(hcptr_);
+ EXPECT_TRUE(HostMgr::checkCacheBackend());
+ EXPECT_EQ(0, hcptr_->size());
+ EXPECT_EQ(0, hcptr_->adds_);
+ ASSERT_TRUE(memptr_);
+
+ // Create a host reservation.
+ // We will not add it anywhere, just will use its values.
+ HostPtr host = HostDataSourceUtils::initializeHost6("2001:db8::1",
+ Host::IDENT_DUID,
+ false);
+ ASSERT_TRUE(host); // Make sure the host is generated properly.
+
+ // Get the address.
+ IPv6ResrvRange resrvs = host->getIPv6Reservations();
+ ASSERT_EQ(1, std::distance(resrvs.first, resrvs.second));
+ const IOAddress& address = resrvs.first->second.getPrefix();
+ ASSERT_EQ("2001:db8::1", address.toText());
+
+ // Do not add it to the host data source.
+
+ // Enable negative caching.
+ HostMgr::instance().setNegativeCaching(true);
+ ASSERT_TRUE(HostMgr::instance().getNegativeCaching());
+
+ // Try to get it in negative cache.
+ ConstHostPtr got = HostMgr::instance().get6(host->getIPv6SubnetID(),
+ address);
+ ASSERT_FALSE(got);
+ EXPECT_EQ(0, hcptr_->size());
+ EXPECT_EQ(0, hcptr_->adds_);
+}
+
+/// @brief Test one backend class.
+///
+/// This class has one entry which is returned to any lookup.
+class TestOneBackend : public BaseHostDataSource {
+public:
+
+ /// Constructor
+ TestOneBackend() : value_() { }
+
+ /// Destructor
+ virtual ~TestOneBackend() { }
+
+ ConstHostCollection getAll(const Host::IdentifierType&, const uint8_t*,
+ const size_t) const {
+ return (getCollection());
+ }
+
+ ConstHostCollection getAll4(const SubnetID&) const {
+ return (getCollection());
+ }
+
+ ConstHostCollection getAll6(const SubnetID&) const {
+ return (getCollection());
+ }
+
+ ConstHostCollection getAllbyHostname(const std::string&) const {
+ return (getCollection());
+ }
+
+ ConstHostCollection getAllbyHostname4(const std::string&,
+ const SubnetID&) const {
+ return (getCollection());
+ }
+
+ ConstHostCollection getAllbyHostname6(const std::string&,
+ const SubnetID&) const {
+ return (getCollection());
+ }
+
+ ConstHostCollection getPage4(const SubnetID&, size_t&, uint64_t,
+ const HostPageSize&) const {
+ return (getCollection());
+ }
+
+ ConstHostCollection getPage6(const SubnetID&, size_t&, uint64_t,
+ const HostPageSize&) const {
+ return (getCollection());
+ }
+
+ ConstHostCollection getPage4(size_t&, uint64_t,
+ const HostPageSize&) const {
+ return (getCollection());
+ }
+
+ ConstHostCollection getPage6(size_t&, uint64_t,
+ const HostPageSize&) const {
+ return (getCollection());
+ }
+
+ ConstHostCollection getAll4(const IOAddress&) const {
+ return (getCollection());
+ }
+
+ ConstHostPtr get4(const SubnetID&, const Host::IdentifierType&,
+ const uint8_t*, const size_t) const {
+ return (getOne());
+ }
+
+ ConstHostPtr get4(const SubnetID&, const IOAddress&) const {
+ return (getOne());
+ }
+
+ ConstHostCollection
+ getAll4(const SubnetID&, const asiolink::IOAddress&) const {
+ return (getCollection());
+ }
+
+ ConstHostPtr get6(const SubnetID&, const Host::IdentifierType&,
+ const uint8_t*, const size_t) const {
+ return (getOne());
+ }
+
+ ConstHostPtr get6(const IOAddress&, const uint8_t) const {
+ return (getOne());
+ }
+
+ ConstHostPtr get6(const SubnetID&, const IOAddress&) const {
+ return (getOne());
+ }
+
+ ConstHostCollection
+ getAll6(const SubnetID&, const IOAddress&) const {
+ return (getCollection());
+ }
+
+ void add(const HostPtr&) {
+ }
+
+ bool del(const SubnetID&, const IOAddress&) {
+ return (false);
+ }
+
+ bool del4(const SubnetID&, const Host::IdentifierType&,
+ const uint8_t*, const size_t) {
+ return (false);
+ }
+
+ bool del6(const SubnetID&, const Host::IdentifierType&,
+ const uint8_t*, const size_t) {
+ return (false);
+ }
+
+ std::string getType() const {
+ return ("one");
+ }
+
+ bool setIPReservationsUnique(const bool) {
+ return (true);
+ }
+
+ /// Specific methods
+
+ /// @brief Set the entry
+ void setValue(const HostPtr& value) {
+ value_ = value;
+ }
+
+protected:
+ /// @brief Return collection
+ ConstHostCollection getCollection() const {
+ ConstHostCollection hosts;
+ hosts.push_back(value_);
+ return (hosts);
+ }
+
+ /// @brief Return one entry
+ ConstHostPtr getOne() const {
+ return(value_);
+ }
+
+ /// #brief The value
+ HostPtr value_;
+};
+
+/// @brief TestOneBackend pointer type
+typedef boost::shared_ptr<TestOneBackend> TestOneBackendPtr;
+
+/// @brief Test no cache class.
+///
+/// This class looks like a cache but throws when insert() is called.
+class TestNoCache : public MemHostDataSource, public CacheHostDataSource {
+public:
+
+ /// Destructor
+ virtual ~TestNoCache() { }
+
+ /// Override add
+ void add(const HostPtr& host) {
+ isc_throw(NotImplemented,
+ "add is not implemented: " << host->toText());
+ }
+
+ /// Insert throws
+ size_t insert(const ConstHostPtr& host, bool overwrite) {
+ isc_throw(NotImplemented,
+ "insert is not implemented: " << host->toText()
+ << " with overwrite: " << overwrite);
+ }
+
+ /// Remove throws
+ bool remove(const HostPtr& host) {
+ isc_throw(NotImplemented,
+ "remove is not implemented: " << host->toText());
+ }
+
+ /// Flush
+ void flush(size_t) {
+ isc_throw(NotImplemented, "flush is not implemented");
+ }
+
+ /// Size
+ size_t size() const {
+ return (0);
+ }
+
+ /// Capacity
+ size_t capacity() const {
+ return (0);
+ }
+
+ /// Type
+ string getType() const {
+ return ("nocache");
+ }
+};
+
+/// @brief TestNoCache pointer type
+typedef boost::shared_ptr<TestNoCache> TestNoCachePtr;
+
+/// @brief Test fixture for testing generic negative cache handling.
+class NegativeCacheTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ NegativeCacheTest() {
+ HostMgr::create();
+
+ // No cache.
+ ncptr_.reset(new TestNoCache());
+ auto nocacheFactory = [this](const DatabaseConnection::ParameterMap&) {
+ return (ncptr_);
+ };
+ HostDataSourceFactory::registerFactory("nocache", nocacheFactory);
+ HostMgr::addBackend("type=nocache");
+
+ // One backend.
+ obptr_.reset(new TestOneBackend());
+ auto oneFactory = [this](const DatabaseConnection::ParameterMap&) {
+ return (obptr_);
+ };
+ HostDataSourceFactory::registerFactory("one", oneFactory);
+ HostMgr::addBackend("type=one");
+ }
+
+ /// @brief Destructor.
+ virtual ~NegativeCacheTest() {
+ HostDataSourceFactory::deregisterFactory("one");
+ HostDataSourceFactory::deregisterFactory("nocache");
+ }
+
+ /// @brief Test one backend.
+ TestOneBackendPtr obptr_;
+
+ /// @brief Test no cache.
+ TestNoCachePtr ncptr_;
+
+ /// Test methods
+
+ /// @brief Set negative caching.
+ void setNegativeCaching() {
+ ASSERT_TRUE(HostMgr::instance().checkCacheBackend());
+ ASSERT_FALSE(HostMgr::instance().getNegativeCaching());
+ HostMgr::instance().setNegativeCaching(true);
+ ASSERT_TRUE(HostMgr::instance().getNegativeCaching());
+ }
+
+ void testGetAll();
+ void testGet4();
+ void testGet6();
+};
+
+/// Verify that getAll* methods ignore negative caching.
+void NegativeCacheTest::testGetAll() {
+ // Create a host reservation.
+ HostPtr host = HostDataSourceUtils::initializeHost4("192.0.2.1",
+ Host::IDENT_HWADDR);
+ ASSERT_TRUE(host);
+ ASSERT_TRUE(host->getHWAddress());
+ ASSERT_EQ("192.0.2.1", host->getIPv4Reservation().toText());
+
+ // Make it a negative cached entry.
+ host->setNegative(true);
+ ASSERT_TRUE(host->getNegative());
+
+ // Set the backend with it.
+ obptr_->setValue(host);
+
+ // Verifies getAll* return a collection with it.
+ ConstHostCollection hosts;
+ ASSERT_NO_THROW(hosts =
+ HostMgr::instance().getAll(host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size()));
+ ASSERT_EQ(1, hosts.size());
+ EXPECT_EQ(host, hosts[0]);
+
+ ASSERT_NO_THROW(hosts =
+ HostMgr::instance().getAll4(host->getIPv4Reservation()));
+ ASSERT_EQ(1, hosts.size());
+ EXPECT_EQ(host, hosts[0]);
+}
+
+/// Verify that get4* methods handle negative caching.
+void NegativeCacheTest::testGet4() {
+ // Create a host reservation.
+ HostPtr host = HostDataSourceUtils::initializeHost4("192.0.2.1",
+ Host::IDENT_HWADDR);
+ ASSERT_TRUE(host);
+ ASSERT_TRUE(host->getHWAddress());
+ ASSERT_EQ("192.0.2.1", host->getIPv4Reservation().toText());
+
+ // Make it a negative cached entry.
+ host->setNegative(true);
+ ASSERT_TRUE(host->getNegative());
+
+ // Set the backend with it.
+ obptr_->setValue(host);
+
+ // Verifies get4 overloads return a null pointer.
+ ConstHostPtr got;
+ ASSERT_NO_THROW(got =
+ HostMgr::instance().get4(host->getIPv4SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size()));
+ EXPECT_FALSE(got);
+
+ ASSERT_NO_THROW(got =
+ HostMgr::instance().get4(host->getIPv4SubnetID(),
+ host->getIPv4Reservation()));
+ EXPECT_FALSE(got);
+
+ // Only getAny returns the negative cached entry.
+ ASSERT_NO_THROW(got =
+ HostMgr::instance().get4Any(host->getIPv4SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size()));
+ EXPECT_TRUE(got);
+ EXPECT_EQ(host, got);
+}
+
+/// Verify that get6* methods handle negative caching.
+void NegativeCacheTest::testGet6() {
+ // Create a host reservation.
+ HostPtr host = HostDataSourceUtils::initializeHost6("2001:db8::1",
+ Host::IDENT_DUID,
+ false);
+ ASSERT_TRUE(host);
+ ASSERT_TRUE(host->getDuid());
+
+ // Get the address.
+ IPv6ResrvRange resrvs = host->getIPv6Reservations();
+ ASSERT_EQ(1, std::distance(resrvs.first, resrvs.second));
+ const IOAddress& address = resrvs.first->second.getPrefix();
+ ASSERT_EQ("2001:db8::1", address.toText());
+
+ // Make it a negative cached entry.
+ host->setNegative(true);
+ ASSERT_TRUE(host->getNegative());
+
+ // Set the backend with it.
+ obptr_->setValue(host);
+
+ // Verifies get6 overloads return a null pointer.
+ ConstHostPtr got;
+ ASSERT_NO_THROW(got =
+ HostMgr::instance().get6(host->getIPv6SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size()));
+ EXPECT_FALSE(got);
+
+ ASSERT_NO_THROW(got = HostMgr::instance().get6(address, 128));
+ EXPECT_FALSE(got);
+
+ ASSERT_NO_THROW(got =
+ HostMgr::instance().get6(host->getIPv6SubnetID(),
+ address));
+ EXPECT_FALSE(got);
+
+ // Only getAny returns the negative cached entry.
+ ASSERT_NO_THROW(got =
+ HostMgr::instance().get6Any(host->getIPv6SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size()));
+ EXPECT_TRUE(got);
+ EXPECT_EQ(host, got);
+}
+
+/// Verify that getAll* methods ignore negative caching.
+TEST_F(NegativeCacheTest, getAll) {
+ testGetAll();
+}
+
+/// Verify that get4* methods handle negative caching.
+TEST_F(NegativeCacheTest, get4) {
+ testGet4();
+}
+
+/// Verify that get6* methods handle negative caching.
+TEST_F(NegativeCacheTest, get6) {
+ testGet6();
+}
+
+/// Verify that getAll* methods behavior does not change with
+/// host manager negative caching.
+TEST_F(NegativeCacheTest, getAllwithNegativeCaching) {
+ setNegativeCaching();
+ testGetAll();
+}
+
+/// Verify that get4* methods behavior does not change with
+/// host manager negative caching.
+TEST_F(NegativeCacheTest, get4withNegativeCaching) {
+ setNegativeCaching();
+ testGet4();
+}
+
+/// Verify that get6* methods behavior does not change with
+/// host manager negative caching.
+TEST_F(NegativeCacheTest, get6withNegativeCaching) {
+ setNegativeCaching();
+ testGet6();
+}
+
+}; // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/host_data_source_factory_unittest.cc b/src/lib/dhcpsrv/tests/host_data_source_factory_unittest.cc
new file mode 100644
index 0000000..35ef6ee
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/host_data_source_factory_unittest.cc
@@ -0,0 +1,202 @@
+// Copyright (C) 2018-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcpsrv/host_data_source_factory.h>
+#include <dhcpsrv/testutils/memory_host_data_source.h>
+#include <exceptions/exceptions.h>
+
+#include <gtest/gtest.h>
+
+#include <iostream>
+#include <sstream>
+
+using namespace std;
+using namespace isc;
+using namespace isc::db;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+
+namespace {
+
+// @brief Register memFactory
+bool registerFactory() {
+ return (HostDataSourceFactory::registerFactory("mem", memFactory));
+}
+
+// @brief Derive mem1 class
+class Mem1HostDataSource : public MemHostDataSource {
+public:
+ virtual string getType() const {
+ return ("mem1");
+ }
+};
+
+// @brief Factory of mem1
+HostDataSourcePtr
+mem1Factory(const DatabaseConnection::ParameterMap&) {
+ return (HostDataSourcePtr(new Mem1HostDataSource()));
+}
+
+// @brief Register mem1Factory
+bool registerFactory1() {
+ return (HostDataSourceFactory::registerFactory("mem1", mem1Factory));
+}
+
+// @brief Derive mem2 class
+class Mem2HostDataSource : public MemHostDataSource {
+public:
+ virtual string getType() const {
+ return ("mem2");
+ }
+};
+
+// @brief Factory of mem2
+HostDataSourcePtr
+mem2Factory(const DatabaseConnection::ParameterMap&) {
+ return (HostDataSourcePtr(new Mem2HostDataSource()));
+}
+
+// @brief Register mem2Factory
+bool registerFactory2() {
+ return (HostDataSourceFactory::registerFactory("mem2", mem2Factory));
+}
+
+// @brief Factory function returning 0
+HostDataSourcePtr
+factory0(const DatabaseConnection::ParameterMap&) {
+ return (HostDataSourcePtr());
+}
+
+// @brief Test fixture class
+class HostDataSourceFactoryTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ HostDataSourceFactoryTest() = default;
+
+ /// @brief Destructor
+ virtual ~HostDataSourceFactoryTest() = default;
+
+private:
+ // @brief Prepares the class for a test.
+ virtual void SetUp() {
+ }
+
+ // @brief Cleans up after the test.
+ virtual void TearDown() {
+ sources_.clear();
+ HostDataSourceFactory::deregisterFactory("mem");
+ HostDataSourceFactory::deregisterFactory("mem1");
+ HostDataSourceFactory::deregisterFactory("mem2");
+ }
+public:
+ HostDataSourceList sources_;
+};
+
+// Verify a factory can be registered and only once.
+TEST_F(HostDataSourceFactoryTest, registerFactory) {
+ EXPECT_TRUE(registerFactory());
+
+ // Only once
+ EXPECT_FALSE(registerFactory());
+}
+
+// Verify a factory registration can be checked.
+TEST_F(HostDataSourceFactoryTest, registeredFactory) {
+ // Not yet registered
+ EXPECT_FALSE(HostDataSourceFactory::registeredFactory("mem"));
+ EXPECT_FALSE(HostDataSourceFactory::registeredFactory("mem1"));
+
+ // Register mem
+ EXPECT_TRUE(registerFactory());
+
+ // Now mem is registered but not mem1
+ EXPECT_TRUE(HostDataSourceFactory::registeredFactory("mem"));
+ EXPECT_FALSE(HostDataSourceFactory::registeredFactory("mem1"));
+}
+
+// Verify a factory can be registered and deregistered
+TEST_F(HostDataSourceFactoryTest, deregisterFactory) {
+ // Does not exist at the beginning
+ EXPECT_FALSE(HostDataSourceFactory::deregisterFactory("mem"));
+
+ // Register and deregister
+ EXPECT_TRUE(registerFactory());
+ EXPECT_TRUE(HostDataSourceFactory::deregisterFactory("mem"));
+ EXPECT_FALSE(HostDataSourceFactory::registeredFactory("mem"));
+
+ // No longer exists
+ EXPECT_FALSE(HostDataSourceFactory::deregisterFactory("mem"));
+}
+
+// Verify a registered factory can be called
+TEST_F(HostDataSourceFactoryTest, add) {
+ EXPECT_TRUE(registerFactory());
+ EXPECT_NO_THROW(HostDataSourceFactory::add(sources_, "type=mem"));
+ ASSERT_EQ(1, sources_.size());
+ EXPECT_EQ("mem", sources_[0]->getType());
+}
+
+// Verify that type is required
+TEST_F(HostDataSourceFactoryTest, notype) {
+ EXPECT_THROW(HostDataSourceFactory::add(sources_, "tp=mem"),
+ InvalidParameter);
+ EXPECT_THROW(HostDataSourceFactory::add(sources_, "type=mem"),
+ InvalidType);
+}
+
+// Verify that factory must not return NULL
+TEST_F(HostDataSourceFactoryTest, null) {
+ EXPECT_TRUE(HostDataSourceFactory::registerFactory("mem", factory0));
+ EXPECT_THROW(HostDataSourceFactory::add(sources_, "type=mem"),
+ Unexpected);
+}
+
+// Verify del class method
+TEST_F(HostDataSourceFactoryTest, del) {
+ // No sources at the beginning
+ EXPECT_FALSE(HostDataSourceFactory::del(sources_, "mem"));
+
+ // Add mem
+ EXPECT_TRUE(registerFactory());
+ EXPECT_NO_THROW(HostDataSourceFactory::add(sources_, "type=mem"));
+ ASSERT_EQ(1, sources_.size());
+
+ // Delete another
+ EXPECT_FALSE(HostDataSourceFactory::del(sources_, "another"));
+
+ // Delete mem
+ EXPECT_TRUE(HostDataSourceFactory::del(sources_, "mem"));
+
+ // No longer mem in sources
+ EXPECT_FALSE(HostDataSourceFactory::del(sources_, "mem"));
+}
+
+// Verify add and del class method on multiple backends
+TEST_F(HostDataSourceFactoryTest, multiple) {
+ // Add foo twice
+ EXPECT_TRUE(registerFactory1());
+ EXPECT_NO_THROW(HostDataSourceFactory::add(sources_, "type=mem1"));
+ EXPECT_NO_THROW(HostDataSourceFactory::add(sources_, "type=mem1"));
+
+ // Add mem2 once
+ EXPECT_TRUE(registerFactory2());
+ EXPECT_NO_THROW(HostDataSourceFactory::add(sources_, "type=mem2"));
+
+ // Delete them
+ EXPECT_TRUE(HostDataSourceFactory::del(sources_, "mem1"));
+ EXPECT_TRUE(HostDataSourceFactory::del(sources_, "mem2"));
+ // Second instance of mem1
+ EXPECT_TRUE(HostDataSourceFactory::del(sources_, "mem1"));
+
+ // No more sources
+ EXPECT_EQ(0, sources_.size());
+ EXPECT_FALSE(HostDataSourceFactory::del(sources_, "mem1"));
+ EXPECT_FALSE(HostDataSourceFactory::del(sources_, "mem2"));
+}
+
+}; // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/host_mgr_unittest.cc b/src/lib/dhcpsrv/tests/host_mgr_unittest.cc
new file mode 100644
index 0000000..5b19af0
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/host_mgr_unittest.cc
@@ -0,0 +1,166 @@
+// Copyright (C) 2014-2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/host.h>
+#include <dhcpsrv/host_data_source_factory.h>
+#include <dhcpsrv/host_mgr.h>
+#include <dhcpsrv/testutils/generic_host_data_source_unittest.h>
+
+#include <gtest/gtest.h>
+#include <vector>
+
+using namespace isc;
+using namespace isc::db;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::asiolink;
+
+namespace {
+
+// The tests in this file only address the in memory hosts.
+
+// This test verifies that HostMgr returns all reservations for the
+// specified HW address. The reservations are defined in the server's
+// configuration.
+TEST_F(HostMgrTest, getAll) {
+ testGetAll(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that HostMgr returns all reservations for the
+// specified DHCPv4 subnet. The reservations are defined in the server's
+// configuration.
+TEST_F(HostMgrTest, getAll4BySubnet) {
+ testGetAll4BySubnet(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that HostMgr returns all reservations for the specified
+// IPv4 subnet and reserved address. The reservations are specified in the
+// server's configuration.
+TEST_F(HostMgrTest, getAll4BySubnetIP) {
+ testGetAll4BySubnetIP(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that HostMgr returns all reservations for the specified
+// IPv6 subnet and reserved address. The reservations are specified in the
+// server's configuration.
+TEST_F(HostMgrTest, getAll6BySubnetIP) {
+ testGetAll6BySubnetIP(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that HostMgr returns all reservations for the
+// specified DHCPv6 subnet. The reservations are defined in the server's
+// configuration.
+TEST_F(HostMgrTest, getAll6BySubnet) {
+ testGetAll6BySubnet(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that HostMgr returns all reservations for the specified
+// hostname. The reservations are defined in the server's configuration.
+TEST_F(HostMgrTest, getAllbyHostname) {
+ testGetAllbyHostname(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that HostMgr returns all reservations for the specified
+// hostname and DHCPv4 subnet. The reservations are defined in the server's
+// configuration.
+TEST_F(HostMgrTest, getAllbyHostnameSubnet4) {
+ testGetAllbyHostnameSubnet4(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that HostMgr returns all reservations for the specified
+// hostname and DHCPv6 subnet. The reservations are defined in the server's
+// configuration.
+TEST_F(HostMgrTest, getAllbyHostnameSubnet6) {
+ testGetAllbyHostnameSubnet6(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that HostMgr returns all reservations for the specified
+// hostname and DHCPv6 subnet. The reservations are defined in the server's
+// configuration.
+TEST_F(HostMgrTest, getAllbyHostname6) {
+ testGetAllbyHostnameSubnet4(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that HostMgr returns all reservations for the
+// specified DHCPv4 subnet by pages. The reservations are defined in
+// the server's configuration.
+TEST_F(HostMgrTest, getPage4) {
+ testGetPage4(false);
+}
+
+// This test verifies that HostMgr returns all v4 reservations by pages.
+// The reservations are defined in the server's configuration.
+TEST_F(HostMgrTest, getPage4All) {
+ testGetPage4All(false);
+}
+
+// This test verifies that HostMgr returns all reservations for the
+// specified DHCPv6 subnet by pages. The reservations are defined in
+// the server's configuration.
+TEST_F(HostMgrTest, getPage6) {
+ testGetPage6(false);
+}
+
+// This test verifies that HostMgr returns all v6 reservations by pages.
+// The reservations are defined in the server's configuration.
+TEST_F(HostMgrTest, getPage6All) {
+ testGetPage6All(false);
+}
+
+// This test verifies that it is possible to gather all reservations for the
+// specified IPv4 address from the HostMgr. The reservations are specified in
+// the server's configuration.
+TEST_F(HostMgrTest, getAll4) {
+ testGetAll4(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that it is possible to retrieve a reservation for the
+// particular host using HostMgr. The reservation is specified in the server's
+// configuration.
+TEST_F(HostMgrTest, get4) {
+ testGet4(*getCfgHosts());
+}
+
+// This test verifies handling of negative caching by get4/get4Any.
+TEST_F(HostMgrTest, get4Any) {
+ testGet4Any();
+}
+
+// This test verifies that it is possible to retrieve IPv6 reservations for
+// the particular host using HostMgr. The reservation is specified in the
+// server's configuration.
+TEST_F(HostMgrTest, get6) {
+ testGet6(*getCfgHosts());
+}
+
+// This test verifies handling of negative caching by get4/get4Any.
+TEST_F(HostMgrTest, get6Any) {
+ testGet6Any();
+}
+
+// This test verifies that it is possible to retrieve the reservation of the
+// particular IPv6 prefix using HostMgr.
+TEST_F(HostMgrTest, get6ByPrefix) {
+ testGet6ByPrefix(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that without a host data source an exception is thrown.
+TEST_F(HostMgrTest, addNoDataSource) {
+ // Remove all configuration.
+ CfgMgr::instance().clear();
+ // Recreate HostMgr instance.
+ HostMgr::create();
+
+ HostPtr host(new Host(hwaddrs_[0]->toText(false), "hw-address",
+ SubnetID(1), SUBNET_ID_UNUSED, IOAddress("192.0.2.5")));
+ EXPECT_THROW(HostMgr::instance().add(host), NoHostDataSourceManager);
+}
+
+} // namespace
diff --git a/src/lib/dhcpsrv/tests/host_reservation_parser_unittest.cc b/src/lib/dhcpsrv/tests/host_reservation_parser_unittest.cc
new file mode 100644
index 0000000..4545902
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/host_reservation_parser_unittest.cc
@@ -0,0 +1,1468 @@
+// Copyright (C) 2014-2018,2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
+#include <dhcp/option_int.h>
+#include <dhcp/option4_addrlst.h>
+#include <dhcp/option6_addrlst.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/cfg_hosts_util.h>
+#include <dhcpsrv/host.h>
+#include <dhcpsrv/parsers/dhcp_parsers.h>
+#include <dhcpsrv/parsers/host_reservation_parser.h>
+#include <dhcpsrv/testutils/config_result_check.h>
+#include <testutils/test_to_element.h>
+#include <boost/pointer_cast.hpp>
+#include <boost/algorithm/string.hpp>
+#include <gtest/gtest.h>
+#include <iterator>
+#include <sstream>
+#include <string>
+#include <vector>
+
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::test;
+
+namespace {
+
+/// @brief Holds a type of the last identifier in @c IdentifierType enum.
+///
+/// This value must be updated when new identifiers are added to the enum.
+// const Host::IdentifierType LAST_IDENTIFIER_TYPE = Host::IDENT_CIRCUIT_ID;
+
+/// @brief Test fixture class for @c HostReservationParser.
+class HostReservationParserTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ HostReservationParserTest() = default;
+
+ /// @brief Destructor
+ virtual ~HostReservationParserTest() = default;
+
+protected:
+ /// @brief Setup for each test.
+ ///
+ /// Clears the configuration in the @c CfgMgr.
+ virtual void SetUp();
+
+ /// @brief Cleans up after each test.
+ ///
+ /// Clears the configuration in the @c CfgMgr.
+ virtual void TearDown();
+
+ /// @brief Checks if the reservation is in the range of reservations.
+ ///
+ /// @param resrv Reservation to be searched for.
+ /// @param range Range of reservations returned by the @c Host object
+ /// in which the reservation will be searched.
+ bool
+ reservationExists(const IPv6Resrv& resrv, const IPv6ResrvRange& range) {
+ for (IPv6ResrvIterator it = range.first; it != range.second;
+ ++it) {
+ if (resrv == it->second) {
+ return (true);
+ }
+ }
+ return (false);
+ }
+
+ /// @brief Retrieves DHCP option from a host.
+ ///
+ /// @param host Reference to a host object for which an option should be
+ /// retrieved.
+ /// @param option_space Option space name.
+ /// @param option_code Code of an option to be retrieved.
+ ///
+ /// @return Pointer to the option retrieved or NULL pointer if option
+ /// hasn't been found.
+ OptionPtr
+ retrieveOption(const Host& host, const std::string& option_space,
+ const uint16_t option_code) const {
+ if ((option_space != DHCP6_OPTION_SPACE) && (option_space != DHCP4_OPTION_SPACE)) {
+ return (OptionPtr());
+ }
+
+ // Retrieve a pointer to the appropriate container depending if we're
+ // interested in DHCPv4 or DHCPv6 options.
+ ConstCfgOptionPtr cfg_option = (option_space == DHCP4_OPTION_SPACE ?
+ host.getCfgOption4() : host.getCfgOption6());
+
+ // Retrieve options.
+ OptionContainerPtr options = cfg_option->getAll(option_space);
+ if (options) {
+ const OptionContainerTypeIndex& idx = options->get<1>();
+ OptionContainerTypeIndex::const_iterator it = idx.find(option_code);
+ if (it != idx.end()) {
+ return (it->option_);
+ }
+ }
+
+ return (OptionPtr());
+ }
+
+ /// @brief This test verifies that it is possible to specify an empty list
+ /// of options for a host.
+ ///
+ /// @tparam ParserType Type of the parser to be tested.
+ template<typename ParserType>
+ void testEmptyOptionList() const {
+ // Create configuration with empty option list. Note that we have to
+ // add reservation for at least one resource because host declarations
+ // without any reservations are rejected. Thus, we have added hostname.
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"hostname\": \"foo.isc.org\","
+ "\"option-data\": [ ]"
+ "}";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostPtr host;
+ ParserType parser;
+ ASSERT_NO_THROW(host = parser.parse(SubnetID(10), config_element));
+
+ ASSERT_TRUE(host);
+
+ // There should be no options assigned to a host.
+ EXPECT_TRUE(host->getCfgOption4()->empty());
+ EXPECT_TRUE(host->getCfgOption6()->empty());
+ }
+
+ /// @brief This test verifies that the parser can parse a DHCPv4
+ /// reservation configuration including a specific identifier.
+ ///
+ /// @param identifier_name Identifier name.
+ /// @param identifier_type Identifier type.
+ void testIdentifier4(const std::string& identifier_name,
+ const std::string& identifier_value,
+ const Host::IdentifierType& /*expected_identifier_type*/,
+ const std::vector<uint8_t>& /*expected_identifier*/) const {
+ std::ostringstream config;
+ config << "{ \"" << identifier_name << "\": \"" << identifier_value
+ << "\","
+ << "\"ip-address\": \"192.0.2.112\","
+ << "\"hostname\": \"\" }";
+
+ ElementPtr config_element = Element::fromJSON(config.str());
+
+ HostPtr host;
+ HostReservationParser4 parser;
+ ASSERT_NO_THROW(host = parser.parse(SubnetID(10), config_element));
+ ASSERT_TRUE(host);
+
+ EXPECT_EQ(10, host->getIPv4SubnetID());
+ EXPECT_EQ(SUBNET_ID_UNUSED, host->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.112", host->getIPv4Reservation().toText());
+ EXPECT_TRUE(host->getHostname().empty());
+ }
+
+ /// @brief This test verifies that the parser returns an error when
+ /// configuration is invalid.
+ ///
+ /// @param config JSON configuration to be tested.
+ /// @tparam ParserType Type of the parser class to use.
+ template<typename ParserType>
+ void testInvalidConfig(const std::string& config) const {
+ ElementPtr config_element = Element::fromJSON(config);
+ HostPtr host;
+ ParserType parser;
+ EXPECT_THROW({
+ host = parser.parse(SubnetID(10), config_element);
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
+ }, isc::Exception);
+ }
+
+ /// @brief HW Address object used by tests.
+ HWAddrPtr hwaddr_;
+
+ /// @brief DUID object used by tests.
+ DuidPtr duid_;
+
+ /// @brief Vector holding circuit id used by tests.
+ std::vector<uint8_t> circuit_id_;
+
+ /// @brief Vector holding client id used by tests.
+ std::vector<uint8_t> client_id_;
+};
+
+void
+HostReservationParserTest::SetUp() {
+ CfgMgr::instance().clear();
+ // Initialize HW address used by tests.
+ const uint8_t hwaddr_data[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 };
+ hwaddr_ = HWAddrPtr(new HWAddr(hwaddr_data, sizeof(hwaddr_data),
+ HTYPE_ETHER));
+
+ // Initialize DUID used by tests.
+ const uint8_t duid_data[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0A };
+ duid_ = DuidPtr(new DUID(duid_data, sizeof(duid_data)));
+
+ const std::string circuit_id_str = "howdy";
+ circuit_id_.assign(circuit_id_str.begin(), circuit_id_str.end());
+
+ client_id_.push_back(0x01); // Client identifier type.
+ // Often client id comprises HW address.
+ client_id_.insert(client_id_.end(), hwaddr_->hwaddr_.begin(),
+ hwaddr_->hwaddr_.end());
+}
+
+void
+HostReservationParserTest::TearDown() {
+ CfgMgr::instance().clear();
+}
+
+/// @brief class of subnet_id reservations
+class CfgHostsSubnet : public CfgToElement {
+public:
+ /// @brief constructor
+ CfgHostsSubnet(ConstCfgHostsPtr hosts, SubnetID id)
+ : hosts_(hosts), id_(id) { }
+
+ /// @brief unparse method
+ ElementPtr toElement() const;
+
+private:
+ /// @brief the host reservation configuration
+ ConstCfgHostsPtr hosts_;
+
+ /// @brief the subnet ID
+ SubnetID id_;
+};
+
+ElementPtr
+CfgHostsSubnet::toElement() const {
+ CfgHostsList list;
+ try {
+ list.internalize(hosts_->toElement());
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "CfgHostsSubnet::toElement: " << ex.what();
+ }
+ ElementPtr result = boost::const_pointer_cast<Element>(list.get(id_));
+
+ // Strip
+ for (size_t i = 0; i < result->size(); ++i) {
+ ElementPtr resv = result->getNonConst(i);
+ ConstElementPtr ip_address = resv->get("ip-address");
+ if (ip_address && (ip_address->stringValue() == "0.0.0.0")) {
+ resv->remove("ip-address");
+ }
+ ConstElementPtr ip_addresses = resv->get("ip-addresses");
+ if (ip_addresses && ip_addresses->empty()) {
+ resv->remove("ip-addresses");
+ }
+ ConstElementPtr prefixes = resv->get("prefixes");
+ if (prefixes && prefixes->empty()) {
+ resv->remove("prefixes");
+ }
+ ConstElementPtr hostname = resv->get("hostname");
+ if (hostname && hostname->stringValue().empty()) {
+ resv->remove("hostname");
+ }
+ ConstElementPtr next_server = resv->get("next-server");
+ if (next_server && (next_server->stringValue() == "0.0.0.0")) {
+ resv->remove("next-server");
+ }
+ ConstElementPtr server_hostname = resv->get("server-hostname");
+ if (server_hostname && server_hostname->stringValue().empty()) {
+ resv->remove("server-hostname");
+ }
+ ConstElementPtr boot_file_name = resv->get("boot-file-name");
+ if (boot_file_name && boot_file_name->stringValue().empty()) {
+ resv->remove("boot-file-name");
+ }
+ ConstElementPtr client_classes = resv->get("client-classes");
+ if (client_classes && client_classes->empty()) {
+ resv->remove("client-classes");
+ }
+ ConstElementPtr option_data = resv->get("option-data");
+ if (option_data && option_data->empty()) {
+ resv->remove("option-data");
+ }
+ }
+ return (result);
+}
+
+// This test verifies that the parser can parse the reservation entry for
+// which hw-address is a host identifier.
+TEST_F(HostReservationParserTest, dhcp4HWaddr) {
+ testIdentifier4("hw-address", "1:2:3:4:5:6", Host::IDENT_HWADDR,
+ hwaddr_->hwaddr_);
+}
+
+// This test verifies that the parser can parse the reservation entry for
+// which DUID is a host identifier.
+TEST_F(HostReservationParserTest, dhcp4DUID) {
+ testIdentifier4("duid", "01:02:03:04:05:06:07:08:09:0A",
+ Host::IDENT_DUID, duid_->getDuid());
+}
+
+// This test verifies that the parser can parse the reservation entry for
+// which DUID specified as a string of hexadecimal digits with '0x' prefix
+// is a host identifier
+TEST_F(HostReservationParserTest, dhcp4DUIDWithPrefix) {
+ testIdentifier4("duid", "0x0102030405060708090A",
+ Host::IDENT_DUID, duid_->getDuid());
+}
+
+// This test verifies that the parser can parse a reservation entry for
+// which circuit-id is an identifier. The circuit-id is specified as
+// a string in quotes.
+TEST_F(HostReservationParserTest, dhcp4CircuitIdStringInQuotes) {
+ testIdentifier4("circuit-id", "'howdy'", Host::IDENT_CIRCUIT_ID,
+ circuit_id_);
+}
+
+// This test verifies that the parser can parse a reservation entry for
+// which circuit-id is an identifier. The circuit-id is specified in
+// hexadecimal format.
+TEST_F(HostReservationParserTest, dhcp4CircuitIdHex) {
+ testIdentifier4("circuit-id", "686F776479", Host::IDENT_CIRCUIT_ID,
+ circuit_id_);
+}
+
+// This test verifies that the parser can parse a reservation entry for
+// which circuit-id is an identifier. The circuit-id is specified in
+// hexadecimal format with a '0x' prefix.
+TEST_F(HostReservationParserTest, dhcp4CircuitIdHexWithPrefix) {
+ testIdentifier4("circuit-id", "0x686F776479", Host::IDENT_CIRCUIT_ID,
+ circuit_id_);
+}
+
+// This test verifies that the parser can parse a reservation entry for
+// which client-id is an identifier. The client-id is specified in
+// hexadecimal format.
+TEST_F(HostReservationParserTest, dhcp4ClientIdHex) {
+ testIdentifier4("client-id", "01010203040506", Host::IDENT_CLIENT_ID,
+ client_id_);
+}
+
+// This test verifies that the parser can parse a reservation entry for
+// which client-id is an identifier. The client-id is specified in
+// hexadecimal format with a '0x' prefix.
+TEST_F(HostReservationParserTest, dhcp4ClientIdHexWithPrefix) {
+ testIdentifier4("client-id", "0x01010203040506", Host::IDENT_CLIENT_ID,
+ client_id_);
+}
+
+// This test verifies that the parser can parse the reservation entry
+// when IPv4 address is specified, but hostname is not.
+TEST_F(HostReservationParserTest, dhcp4NoHostname) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0a\","
+ "\"ip-address\": \"192.0.2.10\" }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostPtr host;
+ HostReservationParser4 parser;
+ ASSERT_NO_THROW(host = parser.parse(SubnetID(10), config_element));
+
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_NO_THROW(cfg_hosts->add(host));
+
+ HostCollection hosts;
+ ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_DUID,
+ &duid_->getDuid()[0],
+ duid_->getDuid().size()));
+ ASSERT_EQ(1, hosts.size());
+
+ EXPECT_EQ(10, hosts[0]->getIPv4SubnetID());
+ EXPECT_EQ(SUBNET_ID_UNUSED, hosts[0]->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.10", hosts[0]->getIPv4Reservation().toText());
+ EXPECT_TRUE(hosts[0]->getHostname().empty());
+
+ // lower duid value
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>("[" + config + "]", cfg_subnet);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet6(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet6);
+
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet1(cfg_hosts, SubnetID(1));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet1);
+}
+
+// This test verifies that it is possible to specify DHCPv4 client classes
+// within the host reservation.
+TEST_F(HostReservationParserTest, dhcp4ClientClasses) {
+ std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+ "\"client-classes\": [ \"foo\", \"bar\" ] }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostPtr host;
+ HostReservationParser4 parser;
+ ASSERT_NO_THROW(host = parser.parse(SubnetID(10), config_element));
+ ASSERT_TRUE(host);
+
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_NO_THROW(cfg_hosts->add(host));
+
+ HostCollection hosts;
+ ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_HWADDR,
+ &hwaddr_->hwaddr_[0],
+ hwaddr_->hwaddr_.size()));
+ ASSERT_EQ(1, hosts.size());
+
+ const ClientClasses& classes = hosts[0]->getClientClasses4();
+ ASSERT_EQ(2, classes.size());
+ EXPECT_TRUE(classes.contains("foo"));
+ EXPECT_TRUE(classes.contains("bar"));
+
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>("[" + config + "]", cfg_subnet);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet6(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet6);
+
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet1(cfg_hosts, SubnetID(1));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet1);
+}
+
+// This test verifies that the parser can parse reservation entry
+// containing next-server, server-hostname and boot-file-name values for
+// DHCPv4 message fields.
+TEST_F(HostReservationParserTest, dhcp4MessageFields) {
+ std::string config = "{ \"hw-address\": \"1:2:3:4:5:6\","
+ "\"next-server\": \"192.0.2.11\","
+ "\"server-hostname\": \"some-name.example.org\","
+ "\"boot-file-name\": \"/tmp/some-file.efi\" }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostPtr host;
+ HostReservationParser4 parser;
+ ASSERT_NO_THROW(host = parser.parse(SubnetID(10), config_element));
+ ASSERT_TRUE(host);
+
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_NO_THROW(cfg_hosts->add(host));
+
+ HostCollection hosts;
+ ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_HWADDR,
+ &hwaddr_->hwaddr_[0],
+ hwaddr_->hwaddr_.size()));
+
+ ASSERT_EQ(1, hosts.size());
+
+ EXPECT_EQ(10, hosts[0]->getIPv4SubnetID());
+ EXPECT_EQ("192.0.2.11", hosts[0]->getNextServer().toText());
+ EXPECT_EQ("some-name.example.org", hosts[0]->getServerHostname());
+ EXPECT_EQ("/tmp/some-file.efi", hosts[0]->getBootFileName());
+
+ // canonize hw-address
+ config_element->set("hw-address",
+ Element::create(std::string("01:02:03:04:05:06")));
+ ElementPtr expected = Element::createList();
+ expected->add(config_element);
+
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>(expected, cfg_subnet);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet6(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet6);
+
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet1(cfg_hosts, SubnetID(1));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet1);
+}
+
+// This test verifies that the invalid value of the next server is rejected.
+TEST_F(HostReservationParserTest, invalidNextServer) {
+ // Invalid IPv4 address.
+ std::string config = "{ \"hw-address\": \"1:2:3:4:5:6\","
+ "\"next-server\": \"192.0.2.foo\" }";
+ testInvalidConfig<HostReservationParser4>(config);
+
+ // Broadcast address.
+ config = "{ \"hw-address\": \"1:2:3:4:5:6\","
+ "\"next-server\": \"255.255.255.255\" }";
+ testInvalidConfig<HostReservationParser4>(config);
+
+ // IPv6 address.
+ config = "{ \"hw-address\": \"1:2:3:4:5:6\","
+ "\"next-server\": \"2001:db8:1::1\" }";
+ testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the invalid server hostname is rejected.
+TEST_F(HostReservationParserTest, invalidServerHostname) {
+ std::ostringstream config;
+ config << "{ \"hw-address\": \"1:2:3:4:5:6\","
+ "\"server-hostname\": \"";
+ config << std::string(64, 'a');
+ config << "\" }";
+ testInvalidConfig<HostReservationParser4>(config.str());
+}
+
+// This test verifies that the invalid boot file name is rejected.
+TEST_F(HostReservationParserTest, invalidBootFileName) {
+ std::ostringstream config;
+ config << "{ \"hw-address\": \"1:2:3:4:5:6\","
+ "\"boot-file-name\": \"";
+ config << std::string(128, 'a');
+ config << "\" }";
+ testInvalidConfig<HostReservationParser4>(config.str());
+}
+
+// This test verifies that the configuration parser for host reservations
+// throws an exception when IPv6 address is specified for IPv4 address
+// reservation.
+TEST_F(HostReservationParserTest, dhcp4IPv6Address) {
+ std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+ "\"ip-address\": \"2001:db8:1::1\" }";
+ testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the configuration parser for host reservations
+// throws an exception when no HW address nor DUID is specified.
+TEST_F(HostReservationParserTest, noIdentifier) {
+ std::string config = "{ \"ip-address\": \"192.0.2.112\","
+ "\"hostname\": \"\" }";
+ testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the configuration parser for host reservations
+// throws an exception when neither ip address nor hostname is specified.
+TEST_F(HostReservationParserTest, noResource) {
+ std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\" }";
+ testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the parser can parse the reservation entry
+// when IP address is not specified, but hostname is specified.
+TEST_F(HostReservationParserTest, noIPAddress) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"hostname\": \"foo.example.com\" }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostPtr host;
+ HostReservationParser4 parser;
+ ASSERT_NO_THROW(host = parser.parse(SubnetID(10), config_element));
+ ASSERT_TRUE(host);
+
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_NO_THROW(cfg_hosts->add(host));
+
+ HostCollection hosts;
+ ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_DUID,
+ &duid_->getDuid()[0],
+ duid_->getDuid().size()));
+ ASSERT_EQ(1, hosts.size());
+
+ EXPECT_EQ(10, hosts[0]->getIPv4SubnetID());
+ EXPECT_EQ(SUBNET_ID_UNUSED, hosts[0]->getIPv6SubnetID());
+ EXPECT_EQ("0.0.0.0", hosts[0]->getIPv4Reservation().toText());
+ EXPECT_EQ("foo.example.com", hosts[0]->getHostname());
+
+ // lower duid value
+ boost::algorithm::to_lower(config);
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>("[" + config + "]", cfg_subnet);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet6(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet6);
+
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet1(cfg_hosts, SubnetID(1));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet1);
+}
+
+// This test verifies that the configuration parser for host reservations
+// throws an exception when hostname is empty, and IP address is not
+// specified.
+TEST_F(HostReservationParserTest, emptyHostname) {
+ std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+ "\"hostname\": \"\" }";
+ testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the configuration parser for host reservations
+// throws an exception when invalid IP address is specified.
+TEST_F(HostReservationParserTest, malformedAddress) {
+ std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+ "\"ip-address\": \"192.0.2.bogus\" }";
+ testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the configuration parser for host reservations
+// throws an exception when zero IP address is specified.
+TEST_F(HostReservationParserTest, zeroAddress) {
+ std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+ "\"ip-address\": \"0.0.0.0\" }";
+ testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the configuration parser for host reservations
+// throws an exception when broadcast IP address is specified.
+TEST_F(HostReservationParserTest, bcastAddress) {
+ std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+ "\"ip-address\": \"255.255.255.255\" }";
+ testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the configuration parser for host reservations
+// throws an exception when invalid next server address is specified.
+TEST_F(HostReservationParserTest, malformedNextServer) {
+ std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+ "\"next-server\": \"192.0.2.bogus\" }";
+ testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the configuration parser for host reservations
+// throws an exception when zero next server address is specified.
+TEST_F(HostReservationParserTest, zeroNextServer) {
+ std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+ "\"next-server\": \"0.0.0.0\" }";
+ testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the configuration parser for host reservations
+// throws an exception when broadcast next server address is specified.
+TEST_F(HostReservationParserTest, bcastNextServer) {
+ std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+ "\"next-server\": \"255.255.255.255\" }";
+ testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the configuration parser for host reservations
+// throws an exception when unsupported parameter is specified.
+TEST_F(HostReservationParserTest, invalidParameterName) {
+ // The "ip-addresses" parameter name is incorrect for the DHCPv4
+ // case - it is only valid for DHCPv6 case. Trying to set this
+ // parameter should result in error.
+ std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+ "\"hostname\": \"foo.bar.isc.org\","
+ "\"ip-addresses\": \"2001:db8:1::1\" }";
+ testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the parser can parse the IPv6 reservation entry for
+// which hw-address is a host identifier.
+TEST_F(HostReservationParserTest, dhcp6HWaddr) {
+ std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+ "\"ip-addresses\": [ \"2001:db8:1::1\", \"2001:db8:1::2\" ],"
+ "\"prefixes\": [ \"2001:db8:2000:0101::/64\", "
+ "\"2001:db8:2000:0102::/64\" ],"
+ "\"hostname\": \"foo.example.com\" }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostPtr host;
+ HostReservationParser6 parser;
+ ASSERT_NO_THROW(host = parser.parse(SubnetID(10), config_element));
+ ASSERT_TRUE(host);
+
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_NO_THROW(cfg_hosts->add(host));
+
+ HostCollection hosts;
+ ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_HWADDR,
+ &hwaddr_->hwaddr_[0],
+ hwaddr_->hwaddr_.size()));
+ ASSERT_EQ(1, hosts.size());
+
+ EXPECT_EQ(SUBNET_ID_UNUSED, hosts[0]->getIPv4SubnetID());
+ EXPECT_EQ(10, hosts[0]->getIPv6SubnetID());
+ EXPECT_EQ("foo.example.com", hosts[0]->getHostname());
+
+ IPv6ResrvRange addresses = hosts[0]->
+ getIPv6Reservations(IPv6Resrv::TYPE_NA);
+ ASSERT_EQ(2, std::distance(addresses.first, addresses.second));
+
+ EXPECT_TRUE(reservationExists(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::1")),
+ addresses));
+ EXPECT_TRUE(reservationExists(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::2")),
+ addresses));
+
+ IPv6ResrvRange prefixes = hosts[0]->getIPv6Reservations(IPv6Resrv::TYPE_PD);
+ ASSERT_EQ(2, std::distance(prefixes.first, prefixes.second));
+ EXPECT_TRUE(reservationExists(IPv6Resrv(IPv6Resrv::TYPE_PD,
+ IOAddress("2001:db8:2000:0101::"),
+ 64),
+ prefixes));
+ EXPECT_TRUE(reservationExists(IPv6Resrv(IPv6Resrv::TYPE_PD,
+ IOAddress("2001:db8:2000:0102::"),
+ 64),
+ prefixes));
+
+ // canonize prefixes
+ config_element->set("prefixes",
+ Element::fromJSON("[ \"2001:db8:2000:101::/64\", "
+ "\"2001:db8:2000:102::/64\" ]"));
+ ElementPtr expected = Element::createList();
+ expected->add(config_element);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>(expected, cfg_subnet);
+
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet4(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet4);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet1(cfg_hosts, SubnetID(1));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet1);
+}
+
+// This test verifies that the parser can parse the IPv6 reservation entry for
+// which DUID is a host identifier.
+TEST_F(HostReservationParserTest, dhcp6DUID) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"ip-addresses\": [ \"2001:db8:1::100\", \"2001:db8:1::200\" ],"
+ "\"prefixes\": [ ],"
+ "\"hostname\": \"foo.example.com\" }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostPtr host;
+ HostReservationParser6 parser;
+ ASSERT_NO_THROW(host = parser.parse(SubnetID(12), config_element));
+ ASSERT_TRUE(host);
+
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_NO_THROW(cfg_hosts->add(host));
+
+ HostCollection hosts;
+ ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_DUID,
+ &duid_->getDuid()[0],
+ duid_->getDuid().size()));
+ ASSERT_EQ(1, hosts.size());
+
+ EXPECT_EQ(SUBNET_ID_UNUSED, hosts[0]->getIPv4SubnetID());
+ EXPECT_EQ(12, hosts[0]->getIPv6SubnetID());
+ EXPECT_EQ("foo.example.com", hosts[0]->getHostname());
+
+ IPv6ResrvRange addresses = hosts[0]->
+ getIPv6Reservations(IPv6Resrv::TYPE_NA);
+ ASSERT_EQ(2, std::distance(addresses.first, addresses.second));
+
+ EXPECT_TRUE(reservationExists(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::100")),
+ addresses));
+ EXPECT_TRUE(reservationExists(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::200")),
+ addresses));
+
+ IPv6ResrvRange prefixes = hosts[0]->getIPv6Reservations(IPv6Resrv::TYPE_PD);
+ ASSERT_EQ(0, std::distance(prefixes.first, prefixes.second));
+
+ // remove prefixes and lower duid value
+ config_element->remove("prefixes");
+ config = prettyPrint(config_element);
+ boost::algorithm::to_lower(config);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet(cfg_hosts, SubnetID(12));
+ runToElementTest<CfgHostsSubnet>("[" + config + "]", cfg_subnet);
+
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet4(cfg_hosts, SubnetID(12));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet4);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet1(cfg_hosts, SubnetID(1));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet1);
+}
+
+// This test verifies that host reservation parser for DHCPv6 rejects
+// "circuit-id" as a host identifier.
+TEST_F(HostReservationParserTest, dhcp6CircuitId) {
+ // Use DHCPv4 specific identifier 'circuit-id' with DHCPv6 parser.
+ std::string config = "{ \"circuit-id\": \"'howdy'\","
+ "\"ip-addresses\": [ \"2001:db8:1::100\", \"2001:db8:1::200\" ],"
+ "\"prefixes\": [ ],"
+ "\"hostname\": \"foo.example.com\" }";
+ testInvalidConfig<HostReservationParser6>(config);
+}
+
+// This test verifies that host reservation parser for DHCPv6 rejects
+// "client-id" as a host identifier.
+TEST_F(HostReservationParserTest, dhcp6ClientId) {
+ // Use DHCPv4 specific identifier 'client-id' with DHCPv6 parser.
+ std::string config = "{ \"client-id\": \"01010203040506\","
+ "\"ip-addresses\": [ \"2001:db8:1::100\", \"2001:db8:1::200\" ],"
+ "\"prefixes\": [ ],"
+ "\"hostname\": \"foo.example.com\" }";
+ testInvalidConfig<HostReservationParser6>(config);
+}
+
+// This test verifies that the parser can parse the IPv6 reservation entry
+// which lacks hostname parameter.
+TEST_F(HostReservationParserTest, dhcp6NoHostname) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"ip-addresses\": [ \"2001:db8:1::100\", \"2001:db8:1::200\" ],"
+ "\"prefixes\": [ ] }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostPtr host;
+ HostReservationParser6 parser;
+ ASSERT_NO_THROW(host = parser.parse(SubnetID(12), config_element));
+ ASSERT_TRUE(host);
+
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_NO_THROW(cfg_hosts->add(host));
+
+ HostCollection hosts;
+ ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_DUID,
+ &duid_->getDuid()[0],
+ duid_->getDuid().size()));
+ ASSERT_EQ(1, hosts.size());
+
+ EXPECT_EQ(SUBNET_ID_UNUSED, hosts[0]->getIPv4SubnetID());
+ EXPECT_EQ(12, hosts[0]->getIPv6SubnetID());
+ EXPECT_TRUE(hosts[0]->getHostname().empty());
+
+ IPv6ResrvRange addresses = hosts[0]->
+ getIPv6Reservations(IPv6Resrv::TYPE_NA);
+ ASSERT_EQ(2, std::distance(addresses.first, addresses.second));
+
+ EXPECT_TRUE(reservationExists(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::100")),
+ addresses));
+ EXPECT_TRUE(reservationExists(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::200")),
+ addresses));
+
+ IPv6ResrvRange prefixes = hosts[0]->getIPv6Reservations(IPv6Resrv::TYPE_PD);
+ ASSERT_EQ(0, std::distance(prefixes.first, prefixes.second));
+
+ // remove prefixes and lower duid value
+ config_element->remove("prefixes");
+ config = prettyPrint(config_element);
+ boost::algorithm::to_lower(config);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet(cfg_hosts, SubnetID(12));
+ runToElementTest<CfgHostsSubnet>("[" + config + "]", cfg_subnet);
+
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet4(cfg_hosts, SubnetID(12));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet4);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet1(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet1);
+}
+
+// This test verifies that it is possible to specify DHCPv4 client classes
+// within the host reservation.
+TEST_F(HostReservationParserTest, dhcp6ClientClasses) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"client-classes\": [ \"foo\", \"bar\" ] }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostPtr host;
+ HostReservationParser6 parser;
+ ASSERT_NO_THROW(host = parser.parse(SubnetID(10), config_element));
+ ASSERT_TRUE(host);
+
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_NO_THROW(cfg_hosts->add(host));
+
+ HostCollection hosts;
+ ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_DUID,
+ &duid_->getDuid()[0],
+ duid_->getDuid().size()));
+ ASSERT_EQ(1, hosts.size());
+
+ const ClientClasses& classes = hosts[0]->getClientClasses6();
+ ASSERT_EQ(2, classes.size());
+ EXPECT_TRUE(classes.contains("foo"));
+ EXPECT_TRUE(classes.contains("bar"));
+
+ // lower duid value
+ boost::algorithm::to_lower(config);
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>("[" + config + "]", cfg_subnet);
+
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet4(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet4);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet1(cfg_hosts, SubnetID(1));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet1);
+}
+
+// This test verifies that the configuration parser throws an exception
+// when IPv4 address is specified for IPv6 reservation.
+TEST_F(HostReservationParserTest, dhcp6IPv4Address) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"ip-addresses\": [ \"192.0.2.3\", \"2001:db8:1::200\" ],"
+ "\"prefixes\": [ ] }";
+ testInvalidConfig<HostReservationParser6>(config);
+}
+
+// This test verifies that the configuration parser throws an exception
+// when empty address has been specified.
+TEST_F(HostReservationParserTest, dhcp6NullAddress) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"ip-addresses\": [ \"\" ],"
+ "\"prefixes\": [ ] }";
+ testInvalidConfig<HostReservationParser6>(config);
+}
+
+// This test verifies that the configuration parser throws an exception
+// when invalid prefix length is specified.
+TEST_F(HostReservationParserTest, dhcp6InvalidPrefixLength) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"prefixes\": [ \"2001:db8:1::/abc\" ] }";
+ testInvalidConfig<HostReservationParser6>(config);
+}
+
+// This test verifies that the configuration parser throws an exception
+// when empty prefix is specified.
+TEST_F(HostReservationParserTest, dhcp6NullPrefix) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"prefixes\": [ \"/64\" ] }";
+ testInvalidConfig<HostReservationParser6>(config);
+}
+
+// This test verifies that the configuration parser throws an exception
+// when only slash is specified for the prefix..
+TEST_F(HostReservationParserTest, dhcp6NullPrefix2) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"prefixes\": [ \"/\" ] }";
+ testInvalidConfig<HostReservationParser6>(config);
+}
+
+// This test verifies that the configuration parser throws an exception
+// when slash is missing for the prefix..
+TEST_F(HostReservationParserTest, dhcp6NullPrefix3) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"prefixes\": [ \"2001:db8:2000:0101::\" ] }";
+ testInvalidConfig<HostReservationParser6>(config);
+}
+
+// This test verifies that the configuration parser throws an exception
+// when slash is followed by nothing for the prefix..
+TEST_F(HostReservationParserTest, dhcp6NullPrefix4) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"prefixes\": [ \"2001:db8:2000:0101::/\" ] }";
+ testInvalidConfig<HostReservationParser6>(config);
+}
+
+// This test verifies that the configuration parser throws an exception
+// when slash is not followed by a number for the prefix..
+TEST_F(HostReservationParserTest, dhcp6NullPrefix5) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"prefixes\": [ \"2001:db8:2000:0101::/foo\" ] }";
+ testInvalidConfig<HostReservationParser6>(config);
+}
+
+// This test verifies that the configuration parser throws an exception
+// when the same address is reserved twice.
+TEST_F(HostReservationParserTest, dhcp6DuplicatedAddress) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"ip-addresses\": [ \"2001:db8:1::1\", \"2001:db8:1::1\" ] }";
+ testInvalidConfig<HostReservationParser6>(config);
+}
+
+// This test verifies that the configuration parser throws an exception
+// when the same prefix is reserved twice.
+TEST_F(HostReservationParserTest, dhcp6DuplicatedPrefix) {
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"prefixes\": [ \"2001:db8:0101::/64\", \"2001:db8:0101::/64\" ] }";
+ testInvalidConfig<HostReservationParser6>(config);
+}
+
+// This test verifies that the configuration parser for host reservations
+// throws an exception when unsupported parameter is specified.
+TEST_F(HostReservationParserTest, dhcp6invalidParameterName) {
+ // The "ip-address" parameter name is incorrect for the DHCPv6
+ // case - it is only valid for DHCPv4 case. Trying to set this
+ // parameter should result in error.
+ std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+ "\"hostname\": \"foo.bar.isc.org\","
+ "\"ip-address\": \"192.0.2.3\" }";
+ testInvalidConfig<HostReservationParser6>(config);
+}
+
+// This test verifies that it is possible to specify DHCPv4 options for
+// a host.
+TEST_F(HostReservationParserTest, options4) {
+ // Create configuration with three options for a host.
+ std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+ "\"option-data\": ["
+ "{"
+ "\"name\": \"name-servers\","
+ "\"data\": \"172.16.15.10, 172.16.15.20\""
+ "},"
+ "{"
+ "\"name\": \"log-servers\","
+ "\"code\": 7,"
+ "\"csv-format\": true,"
+ "\"space\": \"dhcp4\","
+ "\"data\": \"172.16.15.23\","
+ "\"always-send\": false"
+ "},"
+ "{"
+ "\"name\": \"default-ip-ttl\","
+ "\"data\": \"64\""
+ "} ]"
+ "}";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostPtr host;
+ HostReservationParser4 parser;
+ ASSERT_NO_THROW(host = parser.parse(SubnetID(10), config_element));
+ ASSERT_TRUE(host);
+
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_NO_THROW(cfg_hosts->add(host));
+
+ HostCollection hosts;
+ ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_HWADDR,
+ &hwaddr_->hwaddr_[0],
+ hwaddr_->hwaddr_.size()));
+ ASSERT_EQ(1, hosts.size());
+
+ // Retrieve and sanity check name servers.
+ Option4AddrLstPtr opt_dns = boost::dynamic_pointer_cast<
+ Option4AddrLst>(retrieveOption(*hosts[0], DHCP4_OPTION_SPACE, DHO_NAME_SERVERS));
+ ASSERT_TRUE(opt_dns);
+ Option4AddrLst::AddressContainer dns_addrs = opt_dns->getAddresses();
+ ASSERT_EQ(2, dns_addrs.size());
+ EXPECT_EQ("172.16.15.10", dns_addrs[0].toText());
+ EXPECT_EQ("172.16.15.20", dns_addrs[1].toText());
+
+ // Retrieve and sanity check log servers.
+ Option4AddrLstPtr opt_log = boost::dynamic_pointer_cast<
+ Option4AddrLst>(retrieveOption(*hosts[0], DHCP4_OPTION_SPACE, DHO_LOG_SERVERS));
+ ASSERT_TRUE(opt_log);
+ Option4AddrLst::AddressContainer log_addrs = opt_log->getAddresses();
+ ASSERT_EQ(1, log_addrs.size());
+ EXPECT_EQ("172.16.15.23", log_addrs[0].toText());
+
+ // Retrieve and sanity check default IP TTL.
+ OptionUint8Ptr opt_ttl = boost::dynamic_pointer_cast<
+ OptionUint8>(retrieveOption(*hosts[0], DHCP4_OPTION_SPACE, DHO_DEFAULT_IP_TTL));
+ ASSERT_TRUE(opt_ttl);
+ EXPECT_EQ(64, opt_ttl->getValue());
+
+ // Canonize the config
+ ElementPtr option = config_element->get("option-data")->getNonConst(0);
+ option->set("code", Element::create(DHO_NAME_SERVERS));
+ option->set("space", Element::create(std::string(DHCP4_OPTION_SPACE)));
+ option->set("csv-format", Element::create(true));
+ option->set("always-send", Element::create(false));
+ option = config_element->get("option-data")->getNonConst(1);
+ option = config_element->get("option-data")->getNonConst(2);
+ option->set("code", Element::create(DHO_DEFAULT_IP_TTL));
+ option->set("space", Element::create(std::string(DHCP4_OPTION_SPACE)));
+ option->set("csv-format", Element::create(true));
+ option->set("always-send", Element::create(false));
+ ElementPtr expected = Element::createList();
+ expected->add(config_element);
+
+ // Try to unparse it.
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>(expected, cfg_subnet);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet6(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet6);
+
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet1(cfg_hosts, SubnetID(1));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet1);
+}
+
+// This test verifies that it is possible to specify DHCPv6 options for
+// a host.
+TEST_F(HostReservationParserTest, options6) {
+ // Create configuration with three options for a host.
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"option-data\": ["
+ "{"
+ "\"name\": \"dns-servers\","
+ "\"data\": \"2001:db8:1::1, 2001:db8:1::2\""
+ "},"
+ "{"
+ "\"name\": \"nis-servers\","
+ "\"code\": 27,"
+ "\"csv-format\": true,"
+ "\"space\": \"dhcp6\","
+ "\"data\": \"2001:db8:1::1204\","
+ "\"always-send\": true"
+ "},"
+ "{"
+ "\"name\": \"preference\","
+ "\"data\": \"11\""
+ "} ]"
+ "}";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostPtr host;
+ HostReservationParser6 parser;
+ ASSERT_NO_THROW(host = parser.parse(SubnetID(10), config_element));
+ ASSERT_TRUE(host);
+
+ // One host should have been added to the configuration.
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+ ASSERT_NO_THROW(cfg_hosts->add(host));
+
+ HostCollection hosts;
+ ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_DUID,
+ &duid_->getDuid()[0],
+ duid_->getDuid().size()));
+ ASSERT_EQ(1, hosts.size());
+
+ // Retrieve and sanity check DNS servers option.
+ Option6AddrLstPtr opt_dns = boost::dynamic_pointer_cast<
+ Option6AddrLst>(retrieveOption(*hosts[0], DHCP6_OPTION_SPACE, D6O_NAME_SERVERS));
+ ASSERT_TRUE(opt_dns);
+ Option6AddrLst::AddressContainer dns_addrs = opt_dns->getAddresses();
+ ASSERT_EQ(2, dns_addrs.size());
+ EXPECT_EQ("2001:db8:1::1", dns_addrs[0].toText());
+ EXPECT_EQ("2001:db8:1::2", dns_addrs[1].toText());
+
+ // Retrieve and sanity check NIS servers option.
+ Option6AddrLstPtr opt_nis = boost::dynamic_pointer_cast<
+ Option6AddrLst>(retrieveOption(*hosts[0], DHCP6_OPTION_SPACE, D6O_NIS_SERVERS));
+ ASSERT_TRUE(opt_nis);
+ Option6AddrLst::AddressContainer nis_addrs = opt_nis->getAddresses();
+ ASSERT_EQ(1, nis_addrs.size());
+ EXPECT_EQ("2001:db8:1::1204", nis_addrs[0].toText());
+
+ // Retrieve and sanity check preference option.
+ OptionUint8Ptr opt_prf = boost::dynamic_pointer_cast<
+ OptionUint8>(retrieveOption(*hosts[0], DHCP6_OPTION_SPACE, D6O_PREFERENCE));
+ ASSERT_TRUE(opt_prf);
+ EXPECT_EQ(11, opt_prf->getValue());
+
+ // Canonize the config
+ ElementPtr option = config_element->get("option-data")->getNonConst(0);
+ option->set("code", Element::create(D6O_NAME_SERVERS));
+ option->set("space", Element::create(std::string(DHCP6_OPTION_SPACE)));
+ option->set("csv-format", Element::create(true));
+ option->set("always-send", Element::create(false));
+ option = config_element->get("option-data")->getNonConst(1);
+ option = config_element->get("option-data")->getNonConst(2);
+ option->set("code", Element::create(D6O_PREFERENCE));
+ option->set("space", Element::create(std::string(DHCP6_OPTION_SPACE)));
+ option->set("csv-format", Element::create(true));
+ option->set("always-send", Element::create(false));
+ config = prettyPrint(config_element);
+ boost::algorithm::to_lower(config);
+
+ // Try to unparse it.
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>("[" + config + "]", cfg_subnet);
+
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet4(cfg_hosts, SubnetID(10));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet4);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet1(cfg_hosts, SubnetID(1));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet1);
+}
+
+// This test verifies that it is possible to specify an empty list of
+// DHCPv4 options for a host declaration.
+TEST_F(HostReservationParserTest, options4Empty) {
+ testEmptyOptionList<HostReservationParser4>();
+}
+
+// This test verifies that it is possible to specify an empty list of
+// DHCPv6 options for a host declaration.
+TEST_F(HostReservationParserTest, options6Empty) {
+ testEmptyOptionList<HostReservationParser6>();
+}
+
+// This test checks that specifying DHCPv6 options for the DHCPv4 host
+// reservation parser is not allowed.
+TEST_F(HostReservationParserTest, options4InvalidOptionSpace) {
+ // Create configuration specifying DHCPv6 option for a DHCPv4 host
+ // reservation parser.
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"option-data\": ["
+ "{"
+ "\"name\": \"dns-servers\","
+ "\"space\": \"dhcp6\","
+ "\"data\": \"2001:db8:1::1, 2001:db8:1::2\""
+ "} ]"
+ "}";
+
+ testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test checks that specifying DHCPv4 options for the DHCPv6 host
+// reservation parser is not allowed.
+TEST_F(HostReservationParserTest, options6InvalidOptionSpace) {
+ // Create configuration specifying DHCPv4 option for a DHCPv6 host
+ // reservation parser.
+ std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ "\"option-data\": ["
+ "{"
+ "\"name\": \"name-servers\","
+ "\"space\": \"dhcp4\","
+ "\"data\": \"172.16.15.10, 172.16.15.20\""
+ "} ]"
+ "}";
+
+ testInvalidConfig<HostReservationParser6>(config);
+}
+
+// This test verifies that host identifiers for DHCPv4 are mutually exclusive.
+TEST_F(HostReservationParserTest, mutuallyExclusiveIdentifiers4) {
+ std::vector<std::string> identifiers;
+ identifiers.push_back("hw-address");
+ identifiers.push_back("duid");
+ identifiers.push_back("circuit-id");
+
+ for (unsigned int i = 0; i < identifiers.size(); ++i) {
+ // j points to an index of the next identifier. If it
+ // overflows, we set it to 0.
+ unsigned int j = (i + 1) % (identifiers.size());
+ Host::IdentifierType first = static_cast<Host::IdentifierType>(i);
+ Host::IdentifierType second = static_cast<Host::IdentifierType>(j);
+
+ SCOPED_TRACE("Using identifiers " + Host::getIdentifierName(first)
+ + " and " + Host::getIdentifierName(second));
+
+ // Create configuration with two different identifiers.
+ std::ostringstream config;
+ config << "{ \"" << Host::getIdentifierName(first) << "\": \"121314151617\","
+ "\"" << Host::getIdentifierName(second) << "\": \"0A0B0C0D0E0F\","
+ "\"ip-address\": \"192.0.2.3\" }";
+ testInvalidConfig<HostReservationParser4>(config.str());
+ }
+}
+
+// This test verifies that host identifiers for DHCPv6 are mutually exclusive.
+TEST_F(HostReservationParserTest, mutuallyExclusiveIdentifiers6) {
+ std::vector<std::string> identifiers;
+ identifiers.push_back("hw-address");
+ identifiers.push_back("duid");
+
+ for (unsigned int i = 0; i < identifiers.size(); ++i) {
+ // j points to an index of the next identifier. If it
+ // overflows, we set it to 0.
+ unsigned int j = (i + 1) % (identifiers.size());
+
+ SCOPED_TRACE("Using identifiers " + identifiers[i] + " and "
+ + identifiers[j]);
+
+ // Create configuration with two different identifiers.
+ std::ostringstream config;
+ config << "{ \"" << identifiers[i] << "\": \"121314151617\","
+ "\"" << identifiers[j] << "\": \"0A0B0C0D0E0F\","
+ "\"ip-addresses\": \"2001:db8:1::1\" }";
+ testInvalidConfig<HostReservationParser6>(config.str());
+ }
+}
+
+/// @brief Test fixture class for @ref HostReservationIdsParser.
+class HostReservationIdsParserTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Clears current configuration.
+ HostReservationIdsParserTest() {
+ CfgMgr::instance().clear();
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Clears current configuration.
+ virtual ~HostReservationIdsParserTest() {
+ CfgMgr::instance().clear();
+ }
+
+ /// @brief Test verifies that invalid configuration causes an error.
+ ///
+ /// @param config Configuration string.
+ /// @tparam ParserType @ref HostReservationIdsParser4 or
+ /// @ref HostReservationIdsParser6
+ template<typename ParserType>
+ void testInvalidConfig(const std::string& config) const {
+ ElementPtr config_element = Element::fromJSON(config);
+ ParserType parser;
+ EXPECT_THROW(parser.parse(config_element), DhcpConfigError);
+ }
+
+};
+
+// Test that list of supported DHCPv4 identifiers list is correctly
+// parsed.
+TEST_F(HostReservationIdsParserTest, dhcp4Identifiers) {
+ std::string config =
+ "[ \"circuit-id\", \"duid\", \"hw-address\", \"client-id\" ]";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostReservationIdsParser4 parser;
+ ASSERT_NO_THROW(parser.parse(config_element));
+
+ ConstCfgHostOperationsPtr cfg = CfgMgr::instance().getStagingCfg()->
+ getCfgHostOperations4();
+ const CfgHostOperations::IdentifierTypes& ids = cfg->getIdentifierTypes();
+ ASSERT_EQ(4, ids.size());
+
+ CfgHostOperations::IdentifierTypes::const_iterator id = ids.begin();
+ EXPECT_EQ(*id++, Host::IDENT_CIRCUIT_ID);
+ EXPECT_EQ(*id++, Host::IDENT_DUID);
+ EXPECT_EQ(*id++, Host::IDENT_HWADDR);
+ EXPECT_EQ(*id++, Host::IDENT_CLIENT_ID);
+
+ runToElementTest<CfgHostOperations>(config, *cfg);
+}
+
+// Test that list of supported DHCPv6 identifiers list is correctly
+// parsed.
+TEST_F(HostReservationIdsParserTest, dhcp6Identifiers) {
+ std::string config = "[ \"duid\", \"hw-address\" ]";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostReservationIdsParser6 parser;
+ ASSERT_NO_THROW(parser.parse(config_element));
+
+ ConstCfgHostOperationsPtr cfg = CfgMgr::instance().getStagingCfg()->
+ getCfgHostOperations6();
+ const CfgHostOperations::IdentifierTypes& ids = cfg->getIdentifierTypes();
+ ASSERT_EQ(2, ids.size());
+
+ CfgHostOperations::IdentifierTypes::const_iterator id = ids.begin();
+ EXPECT_EQ(*id++, Host::IDENT_DUID);
+ EXPECT_EQ(*id++, Host::IDENT_HWADDR);
+
+ runToElementTest<CfgHostOperations>(config, *cfg);
+}
+
+// Test that invalid DHCPv4 identifier causes error.
+TEST_F(HostReservationIdsParserTest, dhcp4InvalidIdentifier) {
+ // Create configuration including unsupported identifier.
+ std::string config = "[ \"unsupported-id\" ]";
+ testInvalidConfig<HostReservationIdsParser4>(config);
+}
+
+// Test that invalid DHCPv6 identifier causes error.
+TEST_F(HostReservationIdsParserTest, dhcp6InvalidIdentifier) {
+ // Create configuration including unsupported identifier for DHCPv6.
+ // The circuit-id is only supported in DHCPv4.
+ std::string config = "[ \"circuit-id\" ]";
+ testInvalidConfig<HostReservationIdsParser6>(config);
+}
+
+// Check that all supported identifiers are used when 'auto' keyword
+// is specified for DHCPv4 case.
+TEST_F(HostReservationIdsParserTest, dhcp4AutoIdentifiers) {
+ std::string config = "[ \"auto\" ]";
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostReservationIdsParser4 parser;
+ ASSERT_NO_THROW(parser.parse(config_element));
+
+ ConstCfgHostOperationsPtr cfg = CfgMgr::instance().getStagingCfg()->
+ getCfgHostOperations4();
+ const CfgHostOperations::IdentifierTypes& ids = cfg->getIdentifierTypes();
+ ASSERT_EQ(5, ids.size());
+
+ CfgHostOperations::IdentifierTypes::const_iterator id = ids.begin();
+ EXPECT_EQ(*id++, Host::IDENT_HWADDR);
+ EXPECT_EQ(*id++, Host::IDENT_DUID);
+ EXPECT_EQ(*id++, Host::IDENT_CIRCUIT_ID);
+ EXPECT_EQ(*id++, Host::IDENT_CLIENT_ID);
+ EXPECT_EQ(*id++, Host::IDENT_FLEX);
+}
+
+// This test verifies that use of "auto" together with an explicit
+// identifier causes an error. "auto" is placed before the explicit
+// identifier.
+TEST_F(HostReservationIdsParserTest, dhcp4AutoBeforeIdentifier) {
+ std::string config = "[ \"auto\", \"duid\" ]";
+ testInvalidConfig<HostReservationIdsParser4>(config);
+}
+
+// This test verifies that use of "auto" together with an explicit
+// identifier causes an error. "auto" is placed after the explicit
+// identifier.
+TEST_F(HostReservationIdsParserTest, dhcp4AutoAfterIdentifier) {
+ std::string config = "[ \"duid\", \"auto\" ]";
+ testInvalidConfig<HostReservationIdsParser4>(config);
+}
+
+// Test that empty list of identifier types is not allowed.
+TEST_F(HostReservationIdsParserTest, dhcp4EmptyList) {
+ std::string config = "[ ]";
+ testInvalidConfig<HostReservationIdsParser4>(config);
+}
+
+// Check that all supported identifiers are used when 'auto' keyword
+// is specified for DHCPv6 case.
+TEST_F(HostReservationIdsParserTest, dhcp6AutoIdentifiers) {
+ std::string config = "[ \"auto\" ]";
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostReservationIdsParser6 parser;
+ ASSERT_NO_THROW(parser.parse(config_element));
+
+ ConstCfgHostOperationsPtr cfg = CfgMgr::instance().getStagingCfg()->
+ getCfgHostOperations6();
+ const CfgHostOperations::IdentifierTypes& ids = cfg->getIdentifierTypes();
+ ASSERT_EQ(3, ids.size());
+
+ CfgHostOperations::IdentifierTypes::const_iterator id = ids.begin();
+ EXPECT_EQ(*id++, Host::IDENT_HWADDR);
+ EXPECT_EQ(*id++, Host::IDENT_DUID);
+ EXPECT_EQ(*id++, Host::IDENT_FLEX);
+}
+
+// This test verifies that use of "auto" together with an explicit
+// identifier causes an error. "auto" is placed before the explicit
+// identifier.
+TEST_F(HostReservationIdsParserTest, dhcp6AutoBeforeIdentifier) {
+ std::string config = "[ \"auto\", \"duid\" ]";
+ testInvalidConfig<HostReservationIdsParser6>(config);
+}
+
+// This test verifies that use of "auto" together with an explicit
+// identifier causes an error. "auto" is placed after the explicit
+// identifier.
+TEST_F(HostReservationIdsParserTest, dhcp6AutoAfterIdentifier) {
+ std::string config = "[ \"duid\", \"auto\" ]";
+ testInvalidConfig<HostReservationIdsParser6>(config);
+}
+
+// Test that empty list of identifier types is not allowed.
+TEST_F(HostReservationIdsParserTest, dhcp6EmptyList) {
+ std::string config = "[ ]";
+ testInvalidConfig<HostReservationIdsParser6>(config);
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/host_reservations_list_parser_unittest.cc b/src/lib/dhcpsrv/tests/host_reservations_list_parser_unittest.cc
new file mode 100644
index 0000000..c10adde
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/host_reservations_list_parser_unittest.cc
@@ -0,0 +1,388 @@
+// Copyright (C) 2014-2018,2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <cc/data.h>
+#include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/cfg_hosts.h>
+#include <dhcpsrv/cfg_hosts_util.h>
+#include <dhcpsrv/host.h>
+#include <dhcpsrv/subnet_id.h>
+#include <dhcpsrv/parsers/dhcp_parsers.h>
+#include <dhcpsrv/parsers/host_reservation_parser.h>
+#include <dhcpsrv/parsers/host_reservations_list_parser.h>
+#include <testutils/test_to_element.h>
+#include <boost/algorithm/string.hpp>
+#include <gtest/gtest.h>
+#include <sstream>
+#include <string>
+#include <vector>
+
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::test;
+
+namespace {
+
+/// @brief Test fixture class for @c HostReservationsListParser.
+class HostReservationsListParserTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ HostReservationsListParserTest() = default;
+
+ /// @brief Destructor
+ virtual ~HostReservationsListParserTest() = default;
+
+protected:
+ /// @brief Setup for each test.
+ ///
+ /// Clears the configuration in the @c CfgMgr. It also initializes
+ /// hwaddr_ and duid_ class members.
+ virtual void SetUp();
+
+ /// @brief Clean up after each test.
+ ///
+ /// Clears the configuration in the @c CfgMgr.
+ virtual void TearDown();
+
+ /// @brief HW Address object used by tests.
+ HWAddrPtr hwaddr_;
+
+ /// @brief DUID object used by tests.
+ DuidPtr duid_;
+};
+
+void
+HostReservationsListParserTest::SetUp() {
+ CfgMgr::instance().clear();
+ // Initialize HW address used by tests.
+ const uint8_t hwaddr_data[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 };
+ hwaddr_ = HWAddrPtr(new HWAddr(hwaddr_data, sizeof(hwaddr_data),
+ HTYPE_ETHER));
+
+ // Initialize DUID used by tests.
+ const uint8_t duid_data[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0A };
+ duid_ = DuidPtr(new DUID(duid_data, sizeof(duid_data)));
+}
+
+void
+HostReservationsListParserTest::TearDown() {
+ CfgMgr::instance().clear();
+}
+
+/// @brief class of subnet_id reservations
+class CfgHostsSubnet : public CfgToElement {
+public:
+ /// @brief constructor
+ CfgHostsSubnet(ConstCfgHostsPtr hosts, SubnetID id)
+ : hosts_(hosts), id_(id) { }
+
+ /// @brief unparse method
+ ElementPtr toElement() const;
+
+private:
+ /// @brief the host reservation configuration
+ ConstCfgHostsPtr hosts_;
+
+ /// @brief the subnet ID
+ SubnetID id_;
+};
+
+ElementPtr
+CfgHostsSubnet::toElement() const {
+ CfgHostsList list;
+ try {
+ list.internalize(hosts_->toElement());
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "CfgHostsSubnet::toElement: " << ex.what();
+ }
+ ElementPtr result = boost::const_pointer_cast<Element>(list.get(id_));
+
+ // Strip
+ for (size_t i = 0; i < result->size(); ++i) {
+ ElementPtr resv = result->getNonConst(i);
+ ConstElementPtr ip_address = resv->get("ip-address");
+ if (ip_address && (ip_address->stringValue() == "0.0.0.0")) {
+ resv->remove("ip-address");
+ }
+ ConstElementPtr ip_addresses = resv->get("ip-addresses");
+ if (ip_addresses && ip_addresses->empty()) {
+ resv->remove("ip-addresses");
+ }
+ ConstElementPtr prefixes = resv->get("prefixes");
+ if (prefixes && prefixes->empty()) {
+ resv->remove("prefixes");
+ }
+ ConstElementPtr hostname = resv->get("hostname");
+ if (hostname && hostname->stringValue().empty()) {
+ resv->remove("hostname");
+ }
+ ConstElementPtr next_server = resv->get("next-server");
+ if (next_server && (next_server->stringValue() == "0.0.0.0")) {
+ resv->remove("next-server");
+ }
+ ConstElementPtr server_hostname = resv->get("server-hostname");
+ if (server_hostname && server_hostname->stringValue().empty()) {
+ resv->remove("server-hostname");
+ }
+ ConstElementPtr boot_file_name = resv->get("boot-file-name");
+ if (boot_file_name && boot_file_name->stringValue().empty()) {
+ resv->remove("boot-file-name");
+ }
+ ConstElementPtr client_classes = resv->get("client-classes");
+ if (client_classes && client_classes->empty()) {
+ resv->remove("client-classes");
+ }
+ ConstElementPtr option_data = resv->get("option-data");
+ if (option_data && option_data->empty()) {
+ resv->remove("option-data");
+ }
+ }
+ return (result);
+}
+
+// This test verifies that the parser for the list of the host reservations
+// parses IPv4 reservations correctly.
+TEST_F(HostReservationsListParserTest, ipv4Reservations) {
+ CfgMgr::instance().setFamily(AF_INET);
+ // hexadecimal in lower case for toElement()
+ std::string config =
+ "[ "
+ " { "
+ " \"hw-address\": \"01:02:03:04:05:06\","
+ " \"ip-address\": \"192.0.2.134\","
+ " \"hostname\": \"foo.example.com\""
+ " }, "
+ " { "
+ " \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ " \"ip-address\": \"192.0.2.110\","
+ " \"hostname\": \"bar.example.com\""
+ " } "
+ "]";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ HostCollection hosts;
+ HostReservationsListParser<HostReservationParser4> parser;
+ ASSERT_NO_THROW(parser.parse(SubnetID(1), config_element, hosts));
+
+ for (auto h = hosts.begin(); h != hosts.end(); ++h) {
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(*h);
+ }
+
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+
+ // Get the first reservation for the host identified by the HW address.
+ ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_HWADDR,
+ &hwaddr_->hwaddr_[0],
+ hwaddr_->hwaddr_.size()));
+ ASSERT_EQ(1, hosts.size());
+
+ EXPECT_EQ(1, hosts[0]->getIPv4SubnetID());
+ EXPECT_EQ(SUBNET_ID_UNUSED, hosts[0]->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.134", hosts[0]->getIPv4Reservation().toText());
+ EXPECT_EQ("foo.example.com", hosts[0]->getHostname());
+
+ // Get the second reservation for the host identified by the DUID.
+ ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_DUID,
+ &duid_->getDuid()[0],
+ duid_->getDuid().size()));
+ ASSERT_EQ(1, hosts.size());
+
+ EXPECT_EQ(1, hosts[0]->getIPv4SubnetID());
+ EXPECT_EQ(SUBNET_ID_UNUSED, hosts[0]->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.110", hosts[0]->getIPv4Reservation().toText());
+ EXPECT_EQ("bar.example.com", hosts[0]->getHostname());
+
+ // Get back the config from cfg_hosts
+ boost::algorithm::to_lower(config);
+ CfgHostsSubnet cfg_subnet(cfg_hosts, SubnetID(1));
+ runToElementTest<CfgHostsSubnet>(config, cfg_subnet);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet6(cfg_hosts, SubnetID(1));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet6);
+
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet1(cfg_hosts, SUBNET_ID_UNUSED);
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet1);
+}
+
+// This test verifies that an attempt to add two reservations with the
+// same identifier value will return an error.
+TEST_F(HostReservationsListParserTest, duplicatedIdentifierValue4) {
+ std::vector<std::string> identifiers;
+ identifiers.push_back("hw-address");
+ identifiers.push_back("duid");
+ identifiers.push_back("circuit-id");
+ identifiers.push_back("flex-id");
+
+ for (unsigned int i = 0; i < identifiers.size(); ++i) {
+ SCOPED_TRACE("Using identifier " + identifiers[i]);
+
+ std::ostringstream config;
+ config <<
+ "[ "
+ " { "
+ " \"" << identifiers[i] << "\": \"010203040506\","
+ " \"ip-address\": \"192.0.2.134\","
+ " \"hostname\": \"foo.example.com\""
+ " }, "
+ " { "
+ " \"" << identifiers[i] << "\": \"010203040506\","
+ " \"ip-address\": \"192.0.2.110\","
+ " \"hostname\": \"bar.example.com\""
+ " } "
+ "]";
+
+ ElementPtr config_element = Element::fromJSON(config.str());
+
+ HostCollection hosts;
+ HostReservationsListParser<HostReservationParser4> parser;
+ EXPECT_THROW({
+ parser.parse(SubnetID(1), config_element, hosts);
+ for (auto h = hosts.begin(); h != hosts.end(); ++h) {
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(*h);
+ }
+ }, DuplicateHost);
+ // The code threw exception, because the second insertion failed.
+ // Nevertheless, the first host insertion succeeded, so the next
+ // time we try to insert them, we will get ReservedAddress exception,
+ // rather than DuplicateHost. Therefore we need to remove the
+ // first host that's still there.
+ CfgMgr::instance().clear();
+ }
+}
+
+// This test verifies that the parser for the list of the host reservations
+// parses IPv6 reservations correctly.
+TEST_F(HostReservationsListParserTest, ipv6Reservations) {
+ // hexadecimal in lower case for toElement()
+ std::string config =
+ "[ "
+ " { \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+ " \"ip-addresses\": [ ],"
+ " \"prefixes\": [ \"2001:db8:1:2::/80\" ],"
+ " \"hostname\": \"foo.example.com\" "
+ " }, "
+ " { \"hw-address\": \"01:02:03:04:05:06\","
+ " \"ip-addresses\": [ \"2001:db8:1::123\" ],"
+ " \"hostname\": \"bar.example.com\" "
+ " } "
+ "]";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse configuration.
+ HostCollection hosts;
+ HostReservationsListParser<HostReservationParser6> parser;
+ ASSERT_NO_THROW(parser.parse(SubnetID(2), config_element, hosts));
+
+ for (auto h = hosts.begin(); h != hosts.end(); ++h) {
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(*h);
+ }
+
+ CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+
+ // Get the reservation for the host identified by the HW address.
+ ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_HWADDR,
+ &hwaddr_->hwaddr_[0],
+ hwaddr_->hwaddr_.size()));
+ ASSERT_EQ(1, hosts.size());
+
+ // Make sure it belongs to a valid subnet.
+ EXPECT_EQ(SUBNET_ID_UNUSED, hosts[0]->getIPv4SubnetID());
+ EXPECT_EQ(2, hosts[0]->getIPv6SubnetID());
+
+ // Get the reserved addresses for the host. There should be exactly one
+ // address reserved for this host.
+ IPv6ResrvRange prefixes =
+ hosts[0]->getIPv6Reservations(IPv6Resrv::TYPE_NA);
+ ASSERT_EQ(1, std::distance(prefixes.first, prefixes.second));
+
+ EXPECT_EQ(IPv6Resrv::TYPE_NA, prefixes.first->second.getType());
+ EXPECT_EQ("2001:db8:1::123", prefixes.first->second.getPrefix().toText());
+ EXPECT_EQ(128, prefixes.first->second.getPrefixLen());
+
+ // Validate the second reservation.
+ ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_DUID,
+ &duid_->getDuid()[0],
+ duid_->getDuid().size()));
+ ASSERT_EQ(1, hosts.size());
+
+ EXPECT_EQ(SUBNET_ID_UNUSED, hosts[0]->getIPv4SubnetID());
+ EXPECT_EQ(2, hosts[0]->getIPv6SubnetID());
+
+ // This reservation was for a prefix, instead of an IPv6 address.
+ prefixes = hosts[0]->getIPv6Reservations(IPv6Resrv::TYPE_PD);
+ ASSERT_EQ(1, std::distance(prefixes.first, prefixes.second));
+
+ EXPECT_EQ(IPv6Resrv::TYPE_PD, prefixes.first->second.getType());
+ EXPECT_EQ("2001:db8:1:2::", prefixes.first->second.getPrefix().toText());
+ EXPECT_EQ(80, prefixes.first->second.getPrefixLen());
+
+ // Get back the config from cfg_hosts
+ ElementPtr resv = config_element->getNonConst(0);
+ resv->remove("ip-addresses");
+ config = prettyPrint(config_element);
+ boost::algorithm::to_lower(config);
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet(cfg_hosts, SubnetID(2));
+ runToElementTest<CfgHostsSubnet>(config, cfg_subnet);
+
+ CfgMgr::instance().setFamily(AF_INET);
+ CfgHostsSubnet cfg_subnet4(cfg_hosts, SubnetID(2));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet4);
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ CfgHostsSubnet cfg_subnet1(cfg_hosts, SubnetID(1));
+ runToElementTest<CfgHostsSubnet>("[ ]", cfg_subnet1);
+}
+
+// This test verifies that an attempt to add two reservations with the
+// same identifier value will return an error.
+TEST_F(HostReservationsListParserTest, duplicatedIdentifierValue6) {
+ std::vector<std::string> identifiers;
+ identifiers.push_back("hw-address");
+ identifiers.push_back("duid");
+ identifiers.push_back("flex-id");
+
+ for (unsigned int i = 0; i < identifiers.size(); ++i) {
+ SCOPED_TRACE("Using identifier " + identifiers[i]);
+
+ std::ostringstream config;
+ config <<
+ "[ "
+ " { "
+ " \"" << identifiers[i] << "\": \"010203040506\","
+ " \"ip-addresses\": [ \"2001:db8:1::123\" ],"
+ " \"hostname\": \"foo.example.com\""
+ " }, "
+ " { "
+ " \"" << identifiers[i] << "\": \"010203040506\","
+ " \"ip-addresses\": [ \"2001:db8:1::123\" ],"
+ " \"hostname\": \"bar.example.com\""
+ " } "
+ "]";
+
+ ElementPtr config_element = Element::fromJSON(config.str());
+
+ HostCollection hosts;
+ HostReservationsListParser<HostReservationParser6> parser;
+ EXPECT_THROW({
+ parser.parse(SubnetID(1), config_element, hosts);
+ for (auto h = hosts.begin(); h != hosts.end(); ++h) {
+ CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(*h);
+ }
+ }, DuplicateHost);
+ }
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/host_unittest.cc b/src/lib/dhcpsrv/tests/host_unittest.cc
new file mode 100644
index 0000000..5bafd8b
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/host_unittest.cc
@@ -0,0 +1,1310 @@
+// Copyright (C) 2014-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcpsrv/host.h>
+#include <dhcp/option_space.h>
+#include <util/encode/hex.h>
+#include <util/range_utilities.h>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+#include <cstdlib>
+#include <unordered_set>
+#include <sstream>
+#include <boost/functional/hash.hpp>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::asiolink;
+using namespace isc::data;
+
+namespace {
+
+/// @brief Holds a type of the last identifier in @c IdentifierType enum.
+///
+/// This value must be updated when new identifiers are added to the enum.
+const Host::IdentifierType LAST_IDENTIFIER_TYPE = Host::IDENT_CLIENT_ID;
+
+// This test verifies that it is possible to create IPv6 address
+// reservation.
+TEST(IPv6ResrvTest, constructorAddress) {
+ IPv6Resrv resrv(IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::cafe"));
+ EXPECT_EQ("2001:db8:1::cafe", resrv.getPrefix().toText());
+ EXPECT_EQ(128, resrv.getPrefixLen());
+ EXPECT_EQ(IPv6Resrv::TYPE_NA, resrv.getType());
+}
+
+// This test verifies that it is possible to create IPv6 prefix
+// reservation.
+TEST(IPv6ResrvTest, constructorPrefix) {
+ IPv6Resrv resrv(IPv6Resrv::TYPE_PD, IOAddress("2001:db8:1::"), 64);
+ EXPECT_EQ("2001:db8:1::", resrv.getPrefix().toText());
+ EXPECT_EQ(64, resrv.getPrefixLen());
+ EXPECT_EQ(IPv6Resrv::TYPE_PD, resrv.getType());
+}
+
+// This test verifies that the toText() function prints correctly.
+TEST(IPv6ResrvTest, toText) {
+ IPv6Resrv resrv_prefix(IPv6Resrv::TYPE_PD, IOAddress("2001:db8:1::"), 64);
+ EXPECT_EQ("2001:db8:1::/64", resrv_prefix.toText());
+
+ IPv6Resrv resrv_address(IPv6Resrv::TYPE_NA, IOAddress("2001:db8:111::23"));
+ EXPECT_EQ("2001:db8:111::23", resrv_address.toText());
+}
+
+// This test verifies that invalid prefix is rejected.
+TEST(IPv6ResrvTest, constructorInvalidPrefix) {
+ // IPv4 address is invalid for IPv6 reservation.
+ EXPECT_THROW(IPv6Resrv(IPv6Resrv::TYPE_NA, IOAddress("10.0.0.1"), 128),
+ isc::BadValue);
+ // Multicast address is invalid for IPv6 reservation.
+ EXPECT_THROW(IPv6Resrv(IPv6Resrv::TYPE_NA, IOAddress("ff02:1::2"), 128),
+ isc::BadValue);
+}
+
+// This test verifies that invalid prefix length is rejected.
+TEST(IPv6ResrvTest, constructiorInvalidPrefixLength) {
+ ASSERT_NO_THROW(IPv6Resrv(IPv6Resrv::TYPE_PD, IOAddress("2001:db8:1::"),
+ 128));
+ EXPECT_THROW(IPv6Resrv(IPv6Resrv::TYPE_PD, IOAddress("2001:db8:1::"), 129),
+ isc::BadValue);
+ EXPECT_THROW(IPv6Resrv(IPv6Resrv::TYPE_PD, IOAddress("2001:db8:1::"), 244),
+ isc::BadValue);
+ EXPECT_THROW(IPv6Resrv(IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::"), 64),
+ isc::BadValue);
+}
+
+// This test verifies that it is possible to modify prefix and its
+// length in an existing reservation.
+TEST(IPv6ResrvTest, setPrefix) {
+ // Create a reservation using an address and prefix length 128.
+ IPv6Resrv resrv(IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1"));
+ ASSERT_EQ("2001:db8:1::1", resrv.getPrefix().toText());
+ ASSERT_EQ(128, resrv.getPrefixLen());
+ ASSERT_EQ(IPv6Resrv::TYPE_NA, resrv.getType());
+
+ // Modify the reservation to use a prefix having a length of 48.
+ ASSERT_NO_THROW(resrv.set(IPv6Resrv::TYPE_PD, IOAddress("2001:db8::"), 48));
+ EXPECT_EQ("2001:db8::", resrv.getPrefix().toText());
+ EXPECT_EQ(48, resrv.getPrefixLen());
+ EXPECT_EQ(IPv6Resrv::TYPE_PD, resrv.getType());
+
+ // IPv4 address is invalid for IPv6 reservation.
+ EXPECT_THROW(resrv.set(IPv6Resrv::TYPE_NA, IOAddress("10.0.0.1"), 128),
+ isc::BadValue);
+ // IPv6 multicast address is invalid for IPv6 reservation.
+ EXPECT_THROW(resrv.set(IPv6Resrv::TYPE_NA, IOAddress("ff02::1:2"), 128),
+ isc::BadValue);
+ // Prefix length greater than 128 is invalid.
+ EXPECT_THROW(resrv.set(IPv6Resrv::TYPE_PD, IOAddress("2001:db8:1::"), 129),
+ isc::BadValue);
+}
+
+// This test checks that the equality operators work fine.
+TEST(IPv6ResrvTest, equal) {
+ EXPECT_TRUE(IPv6Resrv(IPv6Resrv::TYPE_PD, IOAddress("2001:db8::"), 64) ==
+ IPv6Resrv(IPv6Resrv::TYPE_PD, IOAddress("2001:db8::"), 64));
+
+ EXPECT_FALSE(IPv6Resrv(IPv6Resrv::TYPE_PD, IOAddress("2001:db8::"), 64) !=
+ IPv6Resrv(IPv6Resrv::TYPE_PD, IOAddress("2001:db8::"), 64));
+
+ EXPECT_TRUE(IPv6Resrv(IPv6Resrv::TYPE_NA, IOAddress("2001:db8::1")) ==
+ IPv6Resrv(IPv6Resrv::TYPE_NA, IOAddress("2001:db8::1")));
+ EXPECT_FALSE(IPv6Resrv(IPv6Resrv::TYPE_NA, IOAddress("2001:db8::1")) !=
+ IPv6Resrv(IPv6Resrv::TYPE_NA, IOAddress("2001:db8::1")));
+
+ EXPECT_FALSE(IPv6Resrv(IPv6Resrv::TYPE_NA, IOAddress("2001:db8::1")) ==
+ IPv6Resrv(IPv6Resrv::TYPE_NA, IOAddress("2001:db8::2")));
+ EXPECT_TRUE(IPv6Resrv(IPv6Resrv::TYPE_NA, IOAddress("2001:db8::1")) !=
+ IPv6Resrv(IPv6Resrv::TYPE_NA, IOAddress("2001:db8::2")));
+
+ EXPECT_FALSE(IPv6Resrv(IPv6Resrv::TYPE_PD, IOAddress("2001:db8::"), 64) ==
+ IPv6Resrv(IPv6Resrv::TYPE_PD, IOAddress("2001:db8::"), 48));
+ EXPECT_TRUE(IPv6Resrv(IPv6Resrv::TYPE_PD, IOAddress("2001:db8::"), 64) !=
+ IPv6Resrv(IPv6Resrv::TYPE_PD, IOAddress("2001:db8::"), 48));
+
+ EXPECT_FALSE(IPv6Resrv(IPv6Resrv::TYPE_NA, IOAddress("2001:db8::1"), 128) ==
+ IPv6Resrv(IPv6Resrv::TYPE_PD, IOAddress("2001:db8::1"), 128));
+ EXPECT_TRUE(IPv6Resrv(IPv6Resrv::TYPE_NA, IOAddress("2001:db8::1"), 128) !=
+ IPv6Resrv(IPv6Resrv::TYPE_PD, IOAddress("2001:db8::1"), 128));
+}
+
+/// @brief Test fixture class for @c Host.
+class HostTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Re-initializes random number generator.
+ HostTest() {
+ srand(1);
+ }
+
+ /// @brief Checks if the reservation is in the range of reservations.
+ ///
+ /// @param resrv Reservation to be searched for.
+ /// @param range Range of reservations returned by the @c Host object
+ /// in which the reservation will be searched.
+ ///
+ /// @return true if reservation exists, false otherwise.
+ bool
+ reservationExists(const IPv6Resrv& resrv, const IPv6ResrvRange& range) {
+ for (IPv6ResrvIterator it = range.first; it != range.second;
+ ++it) {
+ if (resrv == it->second) {
+ return (true);
+ }
+ }
+ return (false);
+ }
+
+ /// @brief Returns upper bound of the supported identifier types.
+ ///
+ /// Some unit tests verify the @c Host class behavior for all
+ /// supported identifier types. The unit test needs to iterate
+ /// over all supported identifier types and thus it must be
+ /// aware of the upper bound of the @c Host::IdentifierType
+ /// enum. The upper bound is the numeric representation of the
+ /// last identifier type plus 1.
+ unsigned int
+ identifierTypeUpperBound() const {
+ return (static_cast<unsigned int>(LAST_IDENTIFIER_TYPE) + 1);
+ }
+};
+
+// This test verifies that correct identifier name is returned for
+// a given identifier name and that an error is reported for an
+// unsupported identifier name.
+TEST_F(HostTest, getIdentifier) {
+ EXPECT_EQ(Host::IDENT_HWADDR, Host::getIdentifierType("hw-address"));
+ EXPECT_EQ(Host::IDENT_DUID, Host::getIdentifierType("duid"));
+ EXPECT_EQ(Host::IDENT_CIRCUIT_ID, Host::getIdentifierType("circuit-id"));
+ EXPECT_EQ(Host::IDENT_CLIENT_ID, Host::getIdentifierType("client-id"));
+ EXPECT_EQ(Host::IDENT_FLEX, Host::getIdentifierType("flex-id"));
+
+ EXPECT_THROW(Host::getIdentifierType("unsupported"), isc::BadValue);
+}
+
+// This test verifies that it is possible to create a Host object
+// using hardware address in the textual format.
+TEST_F(HostTest, createFromHWAddrString) {
+ HostPtr host;
+ ASSERT_NO_THROW(host.reset(new Host("01:02:03:04:05:06", "hw-address",
+ SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"),
+ "somehost.example.org",
+ std::string(), std::string(),
+ IOAddress("192.0.0.2"),
+ "server-hostname.example.org",
+ "bootfile.efi", AuthKey("12345678"))));
+ // The HW address should be set to non-null.
+ HWAddrPtr hwaddr = host->getHWAddress();
+ ASSERT_TRUE(hwaddr);
+
+ EXPECT_EQ("hwtype=1 01:02:03:04:05:06", hwaddr->toText());
+
+ // DUID should be null if hardware address is in use.
+ EXPECT_FALSE(host->getDuid());
+ EXPECT_EQ(1, host->getIPv4SubnetID());
+ EXPECT_EQ(2, host->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.3", host->getIPv4Reservation().toText());
+ EXPECT_EQ("somehost.example.org", host->getHostname());
+ EXPECT_EQ("192.0.0.2", host->getNextServer().toText());
+ EXPECT_EQ("server-hostname.example.org", host->getServerHostname());
+ EXPECT_EQ("bootfile.efi", host->getBootFileName());
+ EXPECT_EQ("12345678", host->getKey().toText());
+ EXPECT_FALSE(host->getContext());
+
+ // Use invalid identifier name
+ EXPECT_THROW(Host("01:02:03:04:05:06", "bogus", SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"), "somehost.example.org"),
+ isc::BadValue);
+
+ // Use invalid HW address.
+ EXPECT_THROW(Host("01:0203040506", "hw-address", SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"), "somehost.example.org"),
+ isc::BadValue);
+}
+
+// This test verifies that it is possible to create Host object using
+// a DUID in the textual format.
+TEST_F(HostTest, createFromDUIDString) {
+ HostPtr host;
+ ASSERT_NO_THROW(host.reset(new Host("a1:b2:c3:d4:e5:06", "duid",
+ SubnetID(10), SubnetID(20),
+ IOAddress("192.0.2.5"),
+ "me.example.org")));
+
+ // DUID should be set to non-null value.
+ DuidPtr duid = host->getDuid();
+ ASSERT_TRUE(duid);
+
+ EXPECT_EQ("a1:b2:c3:d4:e5:06", duid->toText());
+
+ // Hardware address must be null if DUID is in use.
+ EXPECT_FALSE(host->getHWAddress());
+
+ EXPECT_EQ(10, host->getIPv4SubnetID());
+ EXPECT_EQ(20, host->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.5", host->getIPv4Reservation().toText());
+ EXPECT_EQ("me.example.org", host->getHostname());
+ EXPECT_FALSE(host->getContext());
+
+ // Use invalid DUID.
+ EXPECT_THROW(Host("bogus", "duid", SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"), "somehost.example.org"),
+ isc::BadValue);
+
+ // Empty DUID is also not allowed.
+ EXPECT_THROW(Host("", "duid", SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"), "somehost.example.org"),
+ isc::BadValue);
+}
+
+// This test verifies that it is possible to create Host object using
+// hardware address in the binary format.
+TEST_F(HostTest, createFromHWAddrBinary) {
+ HostPtr host;
+ // Prepare the hardware address in binary format.
+ const uint8_t hwaddr_data[] = {
+ 0xaa, 0xab, 0xca, 0xda, 0xbb, 0xee
+ };
+ ASSERT_NO_THROW(host.reset(new Host(hwaddr_data,
+ sizeof(hwaddr_data),
+ Host::IDENT_HWADDR,
+ SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"),
+ "somehost.example.org",
+ std::string(), std::string(),
+ IOAddress("192.0.0.2"),
+ "server-hostname.example.org",
+ "bootfile.efi", AuthKey("0abc1234"))));
+
+ // Hardware address should be non-null.
+ HWAddrPtr hwaddr = host->getHWAddress();
+ ASSERT_TRUE(hwaddr);
+
+ EXPECT_EQ("hwtype=1 aa:ab:ca:da:bb:ee", hwaddr->toText());
+
+ // DUID should be null if hardware address is in use.
+ EXPECT_FALSE(host->getDuid());
+ EXPECT_EQ(1, host->getIPv4SubnetID());
+ EXPECT_EQ(2, host->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.3", host->getIPv4Reservation().toText());
+ EXPECT_EQ("somehost.example.org", host->getHostname());
+ EXPECT_EQ("192.0.0.2", host->getNextServer().toText());
+ EXPECT_EQ("server-hostname.example.org", host->getServerHostname());
+ EXPECT_EQ("bootfile.efi", host->getBootFileName());
+ EXPECT_EQ("0ABC1234", host->getKey().toText());
+ EXPECT_FALSE(host->getContext());
+}
+
+// This test verifies that it is possible to create a Host object using
+// DUID in the binary format.
+TEST_F(HostTest, createFromDuidBinary) {
+ HostPtr host;
+ // Prepare DUID binary.
+ const uint8_t duid_data[] = {
+ 1, 2, 3, 4, 5, 6
+ };
+ ASSERT_NO_THROW(host.reset(new Host(duid_data,
+ sizeof(duid_data),
+ Host::IDENT_DUID,
+ SubnetID(10), SubnetID(20),
+ IOAddress("192.0.2.5"),
+ "me.example.org")));
+ // DUID should be non null.
+ DuidPtr duid = host->getDuid();
+ ASSERT_TRUE(duid);
+
+ EXPECT_EQ("01:02:03:04:05:06", duid->toText());
+
+ // Hardware address should be null if DUID is in use.
+ EXPECT_FALSE(host->getHWAddress());
+ EXPECT_EQ(10, host->getIPv4SubnetID());
+ EXPECT_EQ(20, host->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.5", host->getIPv4Reservation().toText());
+ EXPECT_EQ("me.example.org", host->getHostname());
+ EXPECT_FALSE(host->getContext());
+}
+
+// This test verifies that it is possible create Host instance using all
+// supported identifiers in a binary format.
+TEST_F(HostTest, createFromIdentifierBinary) {
+ HostPtr host;
+ // Iterate over all supported identifier types.
+ for (unsigned int i = 0; i < identifierTypeUpperBound(); ++i) {
+ const Host::IdentifierType type = static_cast<Host::IdentifierType>(i);
+ // Create identifier of variable length and fill with random values.
+ std::vector<uint8_t> identifier(random() % 14 + 6);
+ util::fillRandom(identifier.begin(), identifier.end());
+
+ // Try to create a Host instance using this identifier.
+ ASSERT_NO_THROW(host.reset(new Host(&identifier[0], identifier.size(),
+ type, SubnetID(10), SubnetID(20),
+ IOAddress("192.0.2.5"),
+ "me.example.org")));
+
+ // Retrieve identifier from Host instance and check if it is correct.
+ const std::vector<uint8_t>& identifier_returned = host->getIdentifier();
+ EXPECT_TRUE(identifier_returned == identifier);
+ EXPECT_EQ(type, host->getIdentifierType());
+
+ EXPECT_EQ(10, host->getIPv4SubnetID());
+ EXPECT_EQ(20, host->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.5", host->getIPv4Reservation().toText());
+ EXPECT_EQ("me.example.org", host->getHostname());
+ EXPECT_FALSE(host->getContext());
+ }
+}
+
+// This test verifies that it is possible to create Host instance using
+// all supported identifiers in hexadecimal format.
+TEST_F(HostTest, createFromIdentifierHex) {
+ HostPtr host;
+ // Iterate over all supported identifiers.
+ for (unsigned int i = 0; i < identifierTypeUpperBound(); ++i) {
+ const Host::IdentifierType type = static_cast<Host::IdentifierType>(i);
+ // Create identifier of a variable length.
+ std::vector<uint8_t> identifier(random() % 14 + 6);
+ util::fillRandom(identifier.begin(), identifier.end());
+
+ // HW address is a special case, because it must contain colons
+ // between consecutive octets.
+ HWAddrPtr hwaddr;
+ if (type == Host::IDENT_HWADDR) {
+ hwaddr.reset(new HWAddr(identifier, HTYPE_ETHER));
+ }
+
+ // Convert identifier to hexadecimal representation.
+ const std::string identifier_hex = (hwaddr ?
+ hwaddr->toText(false) :
+ util::encode::encodeHex(identifier));
+ const std::string identifier_name = Host::getIdentifierName(type);
+
+ // Try to create Host instance.
+ ASSERT_NO_THROW(host.reset(new Host(identifier_hex, identifier_name,
+ SubnetID(10), SubnetID(20),
+ IOAddress("192.0.2.5"),
+ "me.example.org")))
+ << "test failed for " << identifier_name << "="
+ << identifier_hex;
+
+ // Retrieve the identifier from the Host instance and verify if it
+ // is correct.
+ const std::vector<uint8_t>& identifier_returned = host->getIdentifier();
+ EXPECT_TRUE(identifier_returned == identifier);
+ EXPECT_EQ(type, host->getIdentifierType());
+
+ EXPECT_EQ(10, host->getIPv4SubnetID());
+ EXPECT_EQ(20, host->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.5", host->getIPv4Reservation().toText());
+ EXPECT_EQ("me.example.org", host->getHostname());
+ EXPECT_FALSE(host->getContext());
+ }
+}
+
+// This test verifies that it is possible to create Host instance using
+// identifiers specified as text in quotes.
+TEST_F(HostTest, createFromIdentifierString) {
+ HostPtr host;
+ // It is not allowed to specify HW address or DUID as a string in quotes.
+ for (unsigned int i = 2; i < identifierTypeUpperBound(); ++i) {
+ const Host::IdentifierType type = static_cast<Host::IdentifierType>(i);
+ const std::string identifier_name = Host::getIdentifierName(type);
+
+ // Construct unique identifier for a host. This is a string
+ // consisting of a word "identifier", hyphen and the name of
+ // the identifier, e.g. "identifier-hw-address".
+ std::ostringstream identifier_without_quotes;
+ identifier_without_quotes << "identifier-" << identifier_name;
+
+ // Insert quotes to the identifier to indicate to the Host
+ // constructor that it is encoded as a text.
+ std::ostringstream identifier;
+ identifier << "'" << identifier_without_quotes.str() << "'";
+
+ ASSERT_NO_THROW(host.reset(new Host(identifier.str(), identifier_name,
+ SubnetID(10), SubnetID(20),
+ IOAddress("192.0.2.5"),
+ "me.example.org")))
+ << "test failed for " << identifier_name << "="
+ << identifier.str();
+
+ // Get the identifier from the Host and convert it back to the string
+ // format, so as it can be compared with the identifier used during
+ // Host object construction.
+ const std::vector<uint8_t>& identifier_returned = host->getIdentifier();
+ const std::string identifier_returned_str(identifier_returned.begin(),
+ identifier_returned.end());
+ // Exclude quotes in comparison. Quotes should have been removed.
+ EXPECT_EQ(identifier_without_quotes.str(), identifier_returned_str);
+ EXPECT_EQ(type, host->getIdentifierType());
+
+ EXPECT_EQ(10, host->getIPv4SubnetID());
+ EXPECT_EQ(20, host->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.5", host->getIPv4Reservation().toText());
+ EXPECT_EQ("me.example.org", host->getHostname());
+ EXPECT_FALSE(host->getContext());
+ }
+}
+
+// This test verifies that it is possible to override a host identifier
+// using setIdentifier method with an identifier specified in
+// hexadecimal format.
+TEST_F(HostTest, setIdentifierHex) {
+ HostPtr host;
+ // Iterate over all supported identifiers.
+ for (unsigned int i = 0; i < identifierTypeUpperBound(); ++i) {
+
+ // In order to test that setIdentifier replaces an existing
+ // identifier we have to initialize Host with a different
+ // identifier first. We pick the next identifier after the
+ // one we want to set. If 'i' points to the last one, we
+ // use the first one.
+ unsigned int j = (i + 1) % identifierTypeUpperBound();
+
+ Host::IdentifierType type = static_cast<Host::IdentifierType>(j);
+ // Create identifier of a variable length.
+ std::vector<uint8_t> identifier(random() % 14 + 6);
+ util::fillRandom(identifier.begin(), identifier.end());
+
+ // HW address is a special case, because it must contain colons
+ // between consecutive octets.
+ HWAddrPtr hwaddr;
+ if (type == Host::IDENT_HWADDR) {
+ hwaddr.reset(new HWAddr(identifier, HTYPE_ETHER));
+ }
+
+ // Convert identifier to hexadecimal representation.
+ std::string identifier_hex = (hwaddr ?
+ hwaddr->toText(false) :
+ util::encode::encodeHex(identifier));
+ std::string identifier_name = Host::getIdentifierName(type);
+
+ // Try to create Host instance.
+ ASSERT_NO_THROW(host.reset(new Host(identifier_hex, identifier_name,
+ SubnetID(10), SubnetID(20),
+ IOAddress("192.0.2.5"),
+ "me.example.org")))
+ << "test failed for " << identifier_name << "="
+ << identifier_hex;
+
+ // Retrieve the identifier from the Host instance and verify if it
+ // is correct.
+ std::vector<uint8_t> identifier_returned = host->getIdentifier();
+ EXPECT_TRUE(identifier_returned == identifier);
+ EXPECT_EQ(type, host->getIdentifierType());
+
+ EXPECT_EQ(10, host->getIPv4SubnetID());
+ EXPECT_EQ(20, host->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.5", host->getIPv4Reservation().toText());
+ EXPECT_EQ("me.example.org", host->getHostname());
+ EXPECT_FALSE(host->getContext());
+
+ // Now use another identifier.
+ type = static_cast<Host::IdentifierType>(i);
+ // Create identifier of a variable length.
+ identifier.resize(random() % 14 + 6);
+ util::fillRandom(identifier.begin(), identifier.end());
+
+ hwaddr.reset();
+ if (type == Host::IDENT_HWADDR) {
+ hwaddr.reset(new HWAddr(identifier, HTYPE_ETHER));
+ }
+
+ // Convert identifier to hexadecimal representation.
+ identifier_hex = (hwaddr ? hwaddr->toText(false) :
+ util::encode::encodeHex(identifier));
+ identifier_name = Host::getIdentifierName(type);
+
+ // Try to replace identifier for a host.
+ ASSERT_NO_THROW(host->setIdentifier(identifier_hex, identifier_name))
+ << "test failed for " << identifier_name << "="
+ << identifier_hex;
+
+ // Retrieve the identifier from the Host instance and verify if it
+ // is correct.
+ identifier_returned = host->getIdentifier();
+ EXPECT_TRUE(identifier_returned == identifier);
+ EXPECT_EQ(type, host->getIdentifierType());
+
+ EXPECT_EQ(10, host->getIPv4SubnetID());
+ EXPECT_EQ(20, host->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.5", host->getIPv4Reservation().toText());
+ EXPECT_EQ("me.example.org", host->getHostname());
+ EXPECT_FALSE(host->getContext());
+ }
+}
+
+// This test verifies that it is possible to override a host identifier
+// using setIdentifier method with an identifier specified in binary
+// format.
+TEST_F(HostTest, setIdentifierBinary) {
+ HostPtr host;
+ // Iterate over all supported identifier types.
+ for (unsigned int i = 0; i < identifierTypeUpperBound(); ++i) {
+
+ // In order to test that setIdentifier replaces an existing
+ // identifier we have to initialize Host with a different
+ // identifier first. We pick the next identifier after the
+ // one we want to set. If 'i' points to the last one, we
+ // use the first one.
+ unsigned int j = (i + 1) % identifierTypeUpperBound();
+
+ Host::IdentifierType type = static_cast<Host::IdentifierType>(j);
+ // Create identifier of variable length and fill with random values.
+ std::vector<uint8_t> identifier(random() % 14 + 6);
+ util::fillRandom(identifier.begin(), identifier.end());
+
+ // Try to create a Host instance using this identifier.
+ ASSERT_NO_THROW(host.reset(new Host(&identifier[0], identifier.size(),
+ type, SubnetID(10), SubnetID(20),
+ IOAddress("192.0.2.5"),
+ "me.example.org")));
+
+ // Retrieve identifier from Host instance and check if it is correct.
+ std::vector<uint8_t> identifier_returned = host->getIdentifier();
+ EXPECT_TRUE(identifier_returned == identifier);
+ EXPECT_EQ(type, host->getIdentifierType());
+
+ EXPECT_EQ(10, host->getIPv4SubnetID());
+ EXPECT_EQ(20, host->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.5", host->getIPv4Reservation().toText());
+ EXPECT_EQ("me.example.org", host->getHostname());
+ EXPECT_FALSE(host->getContext());
+
+ type = static_cast<Host::IdentifierType>(i);
+ // Create identifier of variable length and fill with random values.
+ identifier.resize(random() % 14 + 6);
+ util::fillRandom(identifier.begin(), identifier.end());
+
+ // Try to set new identifier.
+ ASSERT_NO_THROW(host->setIdentifier(&identifier[0], identifier.size(),
+ type));
+
+ // Retrieve identifier from Host instance and check if it is correct.
+ identifier_returned = host->getIdentifier();
+ EXPECT_TRUE(identifier_returned == identifier);
+ EXPECT_EQ(type, host->getIdentifierType());
+
+ EXPECT_EQ(10, host->getIPv4SubnetID());
+ EXPECT_EQ(20, host->getIPv6SubnetID());
+ EXPECT_EQ("192.0.2.5", host->getIPv4Reservation().toText());
+ EXPECT_EQ("me.example.org", host->getHostname());
+ EXPECT_FALSE(host->getContext());
+ }
+}
+
+// This test verifies that the IPv6 reservations of a different type can
+// be added for the host.
+TEST_F(HostTest, addReservations) {
+ HostPtr host;
+ ASSERT_NO_THROW(host.reset(new Host("01:02:03:04:05:06", "hw-address",
+ SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"))));
+
+ EXPECT_FALSE(host->hasIPv6Reservation());
+
+ // Add 4 reservations: 2 for NAs, 2 for PDs
+ ASSERT_NO_THROW(
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::cafe")));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_PD,
+ IOAddress("2001:db8:1:1::"), 64));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_PD,
+ IOAddress("2001:db8:1:2::"), 64));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::1")));
+ );
+
+ EXPECT_TRUE(host->hasIPv6Reservation());
+
+ // Check that reservations exist.
+ EXPECT_TRUE(host->hasReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::cafe"))));
+ EXPECT_TRUE(host->hasReservation(IPv6Resrv(IPv6Resrv::TYPE_PD,
+ IOAddress("2001:db8:1:1::"),
+ 64)));
+ EXPECT_TRUE(host->hasReservation(IPv6Resrv(IPv6Resrv::TYPE_PD,
+ IOAddress("2001:db8:1:2::"),
+ 64)));
+ EXPECT_TRUE(host->hasReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::1"))));
+
+ // Get only NA reservations.
+ IPv6ResrvRange addresses = host->getIPv6Reservations(IPv6Resrv::TYPE_NA);
+ ASSERT_EQ(2, std::distance(addresses.first, addresses.second));
+ EXPECT_TRUE(reservationExists(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::cafe")),
+ addresses));
+ EXPECT_TRUE(reservationExists(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::1")),
+ addresses));
+
+
+ // Get only PD reservations.
+ IPv6ResrvRange prefixes = host->getIPv6Reservations(IPv6Resrv::TYPE_PD);
+ ASSERT_EQ(2, std::distance(prefixes.first, prefixes.second));
+ EXPECT_TRUE(reservationExists(IPv6Resrv(IPv6Resrv::TYPE_PD,
+ IOAddress("2001:db8:1:1::"), 64),
+ prefixes));
+ EXPECT_TRUE(reservationExists(IPv6Resrv(IPv6Resrv::TYPE_PD,
+ IOAddress("2001:db8:1:2::"), 64),
+ prefixes));
+}
+
+// This test checks that various modifiers may be used to replace the current
+// values of the Host class.
+TEST_F(HostTest, setValues) {
+ HostPtr host;
+ ASSERT_NO_THROW(host.reset(new Host("01:02:03:04:05:06", "hw-address",
+ SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"),
+ "some-host.eXAMple.org")));
+
+ ASSERT_EQ(1, host->getIPv4SubnetID());
+ ASSERT_EQ(2, host->getIPv6SubnetID());
+ ASSERT_EQ("192.0.2.3", host->getIPv4Reservation().toText());
+ ASSERT_EQ("some-host.eXAMple.org", host->getHostname());
+ ASSERT_EQ("some-host.example.org", host->getLowerHostname());
+ ASSERT_FALSE(host->getContext());
+ ASSERT_FALSE(host->getNegative());
+
+ host->setIPv4SubnetID(SubnetID(123));
+ host->setIPv6SubnetID(SubnetID(234));
+ host->setIPv4Reservation(IOAddress("10.0.0.1"));
+ host->setHostname("other-host.eXAMple.org");
+ host->setNextServer(IOAddress("192.0.2.2"));
+ host->setServerHostname("server-hostname.example.org");
+ host->setBootFileName("bootfile.efi");
+ const std::vector<uint8_t>& random_value(AuthKey::getRandomKeyString());
+ host->setKey(AuthKey(random_value));
+ std::string user_context = "{ \"foo\": \"bar\" }";
+ host->setContext(Element::fromJSON(user_context));
+ host->setNegative(true);
+
+ EXPECT_EQ(123, host->getIPv4SubnetID());
+ EXPECT_EQ(234, host->getIPv6SubnetID());
+ EXPECT_EQ("10.0.0.1", host->getIPv4Reservation().toText());
+ EXPECT_EQ("other-host.eXAMple.org", host->getHostname());
+ ASSERT_EQ("other-host.example.org", host->getLowerHostname());
+ EXPECT_EQ("192.0.2.2", host->getNextServer().toText());
+ EXPECT_EQ("server-hostname.example.org", host->getServerHostname());
+ EXPECT_EQ("bootfile.efi", host->getBootFileName());
+ EXPECT_EQ(random_value, host->getKey().getAuthKey());
+ ASSERT_TRUE(host->getContext());
+ EXPECT_EQ(user_context, host->getContext()->str());
+ EXPECT_TRUE(host->getNegative());
+
+ // Remove IPv4 reservation.
+ host->removeIPv4Reservation();
+ EXPECT_EQ(IOAddress::IPV4_ZERO_ADDRESS(), host->getIPv4Reservation());
+
+ // An IPv6 address can't be used for IPv4 reservations.
+ EXPECT_THROW(host->setIPv4Reservation(IOAddress("2001:db8:1::1")),
+ isc::BadValue);
+ // Zero address can't be set, the removeIPv4Reservation should be
+ // used instead.
+ EXPECT_THROW(host->setIPv4Reservation(IOAddress::IPV4_ZERO_ADDRESS()),
+ isc::BadValue);
+ // Broadcast address can't be set.
+ EXPECT_THROW(host->setIPv4Reservation(IOAddress::IPV4_BCAST_ADDRESS()),
+ isc::BadValue);
+
+ // Broadcast and IPv6 are invalid addresses for next server.
+ EXPECT_THROW(host->setNextServer(asiolink::IOAddress::IPV4_BCAST_ADDRESS()),
+ isc::BadValue);
+ EXPECT_THROW(host->setNextServer(IOAddress("2001:db8:1::1")),
+ isc::BadValue);
+}
+
+// Test that Host constructors initialize client classes from string.
+TEST_F(HostTest, clientClassesFromConstructor) {
+ HostPtr host;
+ // Prepare the hardware address in binary format.
+ const uint8_t hwaddr_data[] = {
+ 0xaa, 0xab, 0xca, 0xda, 0xbb, 0xee
+ };
+
+ // Try the "from binary" constructor.
+ ASSERT_NO_THROW(host.reset(new Host(hwaddr_data,
+ sizeof(hwaddr_data),
+ Host::IDENT_HWADDR,
+ SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"),
+ "somehost.example.org",
+ "alpha, , beta",
+ "gamma")));
+
+ EXPECT_TRUE(host->getClientClasses4().contains("alpha"));
+ EXPECT_TRUE(host->getClientClasses4().contains("beta"));
+ EXPECT_FALSE(host->getClientClasses4().contains("gamma"));
+ EXPECT_TRUE(host->getClientClasses6().contains("gamma"));
+ EXPECT_FALSE(host->getClientClasses6().contains("alpha"));
+ EXPECT_FALSE(host->getClientClasses6().contains("beta"));
+
+ // Try the "from string" constructor.
+ ASSERT_NO_THROW(host.reset(new Host("01:02:03:04:05:06", "hw-address",
+ SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"),
+ "somehost.example.org",
+ "alpha, beta, gamma",
+ "beta, gamma")));
+
+ EXPECT_TRUE(host->getClientClasses4().contains("alpha"));
+ EXPECT_TRUE(host->getClientClasses4().contains("beta"));
+ EXPECT_TRUE(host->getClientClasses4().contains("gamma"));
+ EXPECT_FALSE(host->getClientClasses6().contains("alpha"));
+ EXPECT_TRUE(host->getClientClasses6().contains("beta"));
+ EXPECT_TRUE(host->getClientClasses6().contains("gamma"));
+}
+
+// Test that new client classes can be added for the Host.
+TEST_F(HostTest, addClientClasses) {
+ HostPtr host;
+ ASSERT_NO_THROW(host.reset(new Host("01:02:03:04:05:06", "hw-address",
+ SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"))));
+
+ EXPECT_FALSE(host->getClientClasses4().contains("foo"));
+ EXPECT_FALSE(host->getClientClasses6().contains("foo"));
+ EXPECT_FALSE(host->getClientClasses4().contains("bar"));
+ EXPECT_FALSE(host->getClientClasses6().contains("bar"));
+
+ host->addClientClass4("foo");
+ host->addClientClass6("bar");
+ EXPECT_TRUE(host->getClientClasses4().contains("foo"));
+ EXPECT_FALSE(host->getClientClasses6().contains("foo"));
+ EXPECT_FALSE(host->getClientClasses4().contains("bar"));
+ EXPECT_TRUE(host->getClientClasses6().contains("bar"));
+
+ host->addClientClass4("bar");
+ host->addClientClass6("foo");
+ EXPECT_TRUE(host->getClientClasses4().contains("foo"));
+ EXPECT_TRUE(host->getClientClasses6().contains("foo"));
+ EXPECT_TRUE(host->getClientClasses4().contains("bar"));
+ EXPECT_TRUE(host->getClientClasses6().contains("bar"));
+}
+
+// This test checks that it is possible to add DHCPv4 options for a host.
+TEST_F(HostTest, addOptions4) {
+ Host host("01:02:03:04:05:06", "hw-address", SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"));
+
+ // Differentiate options by their codes (100-109)
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V4, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(host.getCfgOption4()->add(option, false, DHCP4_OPTION_SPACE));
+ }
+
+ // Add 7 options to another option space. The option codes partially overlap
+ // with option codes that we have added to dhcp4 option space.
+ for (uint16_t code = 105; code < 112; ++code) {
+ OptionPtr option(new Option(Option::V4, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(host.getCfgOption4()->add(option, false, "isc"));
+ }
+
+ // Get options from the Subnet and check if all 10 are there.
+ OptionContainerPtr options = host.getCfgOption4()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_TRUE(options);
+ ASSERT_EQ(10, options->size());
+
+ // It should be possible to retrieve DHCPv6 options but the container
+ // should be empty.
+ OptionContainerPtr options6 = host.getCfgOption6()->getAll(DHCP6_OPTION_SPACE);
+ ASSERT_TRUE(options6);
+ EXPECT_TRUE(options6->empty());
+
+ // Also make sure that for dhcp4 option space no DHCPv6 options are
+ // returned. This is to check that containers for DHCPv4 and DHCPv6
+ // options do not share information.
+ options6 = host.getCfgOption6()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_TRUE(options6);
+ EXPECT_TRUE(options6->empty());
+
+ // Validate codes of options added to dhcp4 option space.
+ uint16_t expected_code = 100;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ options = host.getCfgOption4()->getAll("isc");
+ ASSERT_TRUE(options);
+ ASSERT_EQ(7, options->size());
+
+ // Validate codes of options added to isc option space.
+ expected_code = 105;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ // Try to get options from a non-existing option space.
+ options = host.getCfgOption4()->getAll("abcd");
+ ASSERT_TRUE(options);
+ EXPECT_TRUE(options->empty());
+}
+
+// This test checks that it is possible to add DHCPv6 options for a host.
+TEST_F(HostTest, addOptions6) {
+ Host host("01:02:03:04:05:06", "hw-address", SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"));
+
+ // Differentiate options by their codes (100-109)
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(host.getCfgOption6()->add(option, false, DHCP6_OPTION_SPACE));
+ }
+
+ // Add 7 options to another option space. The option codes partially overlap
+ // with option codes that we have added to dhcp6 option space.
+ for (uint16_t code = 105; code < 112; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(host.getCfgOption6()->add(option, false, "isc"));
+ }
+
+ // Get options from the Subnet and check if all 10 are there.
+ OptionContainerPtr options = host.getCfgOption6()->getAll(DHCP6_OPTION_SPACE);
+ ASSERT_TRUE(options);
+ ASSERT_EQ(10, options->size());
+
+ // It should be possible to retrieve DHCPv4 options but the container
+ // should be empty.
+ OptionContainerPtr options4 = host.getCfgOption4()->getAll(DHCP4_OPTION_SPACE);
+ ASSERT_TRUE(options4);
+ EXPECT_TRUE(options4->empty());
+
+ // Also make sure that for dhcp6 option space no DHCPv4 options are
+ // returned. This is to check that containers for DHCPv4 and DHCPv6
+ // options do not share information.
+ options4 = host.getCfgOption4()->getAll(DHCP6_OPTION_SPACE);
+ ASSERT_TRUE(options4);
+ EXPECT_TRUE(options4->empty());
+
+ // Validate codes of options added to dhcp6 option space.
+ uint16_t expected_code = 100;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ options = host.getCfgOption6()->getAll("isc");
+ ASSERT_TRUE(options);
+ ASSERT_EQ(7, options->size());
+
+ // Validate codes of options added to isc option space.
+ expected_code = 105;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ // Try to get options from a non-existing option space.
+ options = host.getCfgOption6()->getAll("abcd");
+ ASSERT_TRUE(options);
+ EXPECT_TRUE(options->empty());
+}
+
+// This test verifies that it is possible to retrieve a textual
+// representation of the host identifier.
+TEST_F(HostTest, getIdentifierAsText) {
+ // HW address
+ Host host1("01:02:03:04:05:06", "hw-address",
+ SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"));
+ EXPECT_EQ("hwaddr=010203040506", host1.getIdentifierAsText());
+
+ // DUID
+ Host host2("0a:0b:0c:0d:0e:0f:ab:cd:ef", "duid",
+ SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"));
+ EXPECT_EQ("duid=0A0B0C0D0E0FABCDEF",
+ host2.getIdentifierAsText());
+
+ // Circuit id.
+ Host host3("'marcin's-home'", "circuit-id",
+ SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"));
+ EXPECT_EQ("circuit-id=6D617263696E27732D686F6D65",
+ host3.getIdentifierAsText());
+}
+
+// This test verifies that conversion of the identifier type to a
+// name works correctly.
+TEST_F(HostTest, getIdentifierName) {
+ EXPECT_EQ("hw-address", Host::getIdentifierName(Host::IDENT_HWADDR));
+
+}
+
+// This test checks that Host object is correctly described in the
+// textual format using the toText method.
+TEST_F(HostTest, toText) {
+ HostPtr host;
+ ASSERT_NO_THROW(host.reset(new Host("01:02:03:04:05:06", "hw-address",
+ SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"),
+ "myhost.example.com")));
+
+ // Add 4 reservations: 2 for NAs, 2 for PDs.
+ ASSERT_NO_THROW(
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::cafe")));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_PD,
+ IOAddress("2001:db8:1:1::"), 64));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_PD,
+ IOAddress("2001:db8:1:2::"), 64));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::1")));
+ );
+
+ // Add invisible user context
+ std::string user_context = "{ \"foo\": \"bar\" }";
+ host->setContext(Element::fromJSON(user_context));
+
+ // Make sure that the output is correct,
+ EXPECT_EQ("hwaddr=010203040506 ipv4_subnet_id=1 ipv6_subnet_id=2"
+ " hostname=myhost.example.com"
+ " ipv4_reservation=192.0.2.3"
+ " siaddr=(no)"
+ " sname=(empty)"
+ " file=(empty)"
+ " key=(empty)"
+ " ipv6_reservation0=2001:db8:1::cafe"
+ " ipv6_reservation1=2001:db8:1::1"
+ " ipv6_reservation2=2001:db8:1:1::/64"
+ " ipv6_reservation3=2001:db8:1:2::/64",
+ host->toText());
+
+ // Reset some of the data and make sure that the output is affected.
+ host->setHostname("");
+ host->removeIPv4Reservation();
+ host->setIPv4SubnetID(SUBNET_ID_UNUSED);
+ host->setNegative(true);
+
+ EXPECT_EQ("hwaddr=010203040506 ipv6_subnet_id=2"
+ " hostname=(empty) ipv4_reservation=(no)"
+ " siaddr=(no)"
+ " sname=(empty)"
+ " file=(empty)"
+ " key=(empty)"
+ " ipv6_reservation0=2001:db8:1::cafe"
+ " ipv6_reservation1=2001:db8:1::1"
+ " ipv6_reservation2=2001:db8:1:1::/64"
+ " ipv6_reservation3=2001:db8:1:2::/64"
+ " negative cached",
+ host->toText());
+
+ // Create host identified by DUID, instead of HWADDR, with a very
+ // basic configuration.
+ ASSERT_NO_THROW(host.reset(new Host("11:12:13:14:15", "duid",
+ SUBNET_ID_UNUSED, SUBNET_ID_UNUSED,
+ IOAddress::IPV4_ZERO_ADDRESS(),
+ "myhost")));
+
+ EXPECT_EQ("duid=1112131415 hostname=myhost ipv4_reservation=(no)"
+ " siaddr=(no)"
+ " sname=(empty)"
+ " file=(empty)"
+ " key=(empty)"
+ " ipv6_reservations=(none)", host->toText());
+
+ // Add some classes.
+ host->addClientClass4("modem");
+ host->addClientClass4("router");
+
+ EXPECT_EQ("duid=1112131415 hostname=myhost ipv4_reservation=(no)"
+ " siaddr=(no)"
+ " sname=(empty)"
+ " file=(empty)"
+ " key=(empty)"
+ " ipv6_reservations=(none)"
+ " dhcp4_class0=modem dhcp4_class1=router",
+ host->toText());
+
+ host->addClientClass6("hub");
+ host->addClientClass6("device");
+
+ // Note that now classes are in insert order.
+ EXPECT_EQ("duid=1112131415 hostname=myhost ipv4_reservation=(no)"
+ " siaddr=(no)"
+ " sname=(empty)"
+ " file=(empty)"
+ " key=(empty)"
+ " ipv6_reservations=(none)"
+ " dhcp4_class0=modem dhcp4_class1=router"
+ " dhcp6_class0=hub dhcp6_class1=device",
+ host->toText());
+
+ // Create global host identified by DUID, with a very basic configuration.
+ ASSERT_NO_THROW(host.reset(new Host("11:12:13:14:15", "duid",
+ SUBNET_ID_GLOBAL, SUBNET_ID_GLOBAL,
+ IOAddress::IPV4_ZERO_ADDRESS(),
+ "myhost")));
+
+ EXPECT_EQ("duid=1112131415 ipv4_subnet_id=0 ipv6_subnet_id=0 "
+ "hostname=myhost ipv4_reservation=(no)"
+ " siaddr=(no)"
+ " sname=(empty)"
+ " file=(empty)"
+ " key=(empty)"
+ " ipv6_reservations=(none)", host->toText());
+
+}
+
+// This test checks that Host object is correctly unparsed,
+TEST_F(HostTest, unparse) {
+ boost::scoped_ptr<Host> host;
+ ASSERT_NO_THROW(host.reset(new Host("01:02:03:04:05:06", "hw-address",
+ SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"),
+ "myhost.example.com")));
+
+ // Add 4 reservations: 2 for NAs, 2 for PDs.
+ ASSERT_NO_THROW(
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::cafe")));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_PD,
+ IOAddress("2001:db8:1:1::"), 64));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_PD,
+ IOAddress("2001:db8:1:2::"), 64));
+ host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+ IOAddress("2001:db8:1::1")));
+ );
+
+ // Add user context
+ std::string user_context = "{ \"comment\": \"a host reservation\" }";
+ host->setContext(Element::fromJSON(user_context));
+
+ // Make sure that the output is correct,
+ EXPECT_EQ("{ "
+ "\"boot-file-name\": \"\", "
+ "\"client-classes\": [ ], "
+ "\"hostname\": \"myhost.example.com\", "
+ "\"hw-address\": \"01:02:03:04:05:06\", "
+ "\"ip-address\": \"192.0.2.3\", "
+ "\"next-server\": \"0.0.0.0\", "
+ "\"option-data\": [ ], "
+ "\"server-hostname\": \"\", "
+ "\"user-context\": { \"comment\": \"a host reservation\" } "
+ "}",
+ host->toElement4()->str());
+
+ EXPECT_EQ("{ "
+ "\"client-classes\": [ ], "
+ "\"hostname\": \"myhost.example.com\", "
+ "\"hw-address\": \"01:02:03:04:05:06\", "
+ "\"ip-addresses\": [ \"2001:db8:1::cafe\", \"2001:db8:1::1\" ], "
+ "\"option-data\": [ ], "
+ "\"prefixes\": [ \"2001:db8:1:1::/64\", \"2001:db8:1:2::/64\" ], "
+ "\"user-context\": { \"comment\": \"a host reservation\" } "
+ "}",
+ host->toElement6()->str());
+
+ // Reset some of the data and make sure that the output is affected.
+ host->setHostname("");
+ host->removeIPv4Reservation();
+ host->setIPv4SubnetID(SUBNET_ID_UNUSED);
+
+ EXPECT_EQ("{ "
+ "\"boot-file-name\": \"\", "
+ "\"client-classes\": [ ], "
+ "\"hostname\": \"\", "
+ "\"hw-address\": \"01:02:03:04:05:06\", "
+ "\"next-server\": \"0.0.0.0\", "
+ "\"option-data\": [ ], "
+ "\"server-hostname\": \"\", "
+ "\"user-context\": { \"comment\": \"a host reservation\" } "
+ "}",
+ host->toElement4()->str());
+
+ EXPECT_EQ("{ "
+ "\"client-classes\": [ ], "
+ "\"hostname\": \"\", "
+ "\"hw-address\": \"01:02:03:04:05:06\", "
+ "\"ip-addresses\": [ \"2001:db8:1::cafe\", \"2001:db8:1::1\" ], "
+ "\"option-data\": [ ], "
+ "\"prefixes\": [ \"2001:db8:1:1::/64\", \"2001:db8:1:2::/64\" ], "
+ "\"user-context\": { \"comment\": \"a host reservation\" } "
+ "}",
+ host->toElement6()->str());
+
+ // Create host identified by DUID, instead of HWADDR, with a very
+ // basic configuration.
+ ASSERT_NO_THROW(host.reset(new Host("11:12:13:14:15", "duid",
+ SUBNET_ID_UNUSED, SUBNET_ID_UNUSED,
+ IOAddress::IPV4_ZERO_ADDRESS(),
+ "myhost")));
+
+ EXPECT_EQ("{ "
+ "\"boot-file-name\": \"\", "
+ "\"client-classes\": [ ], "
+ "\"duid\": \"11:12:13:14:15\", "
+ "\"hostname\": \"myhost\", "
+ "\"next-server\": \"0.0.0.0\", "
+ "\"option-data\": [ ], "
+ "\"server-hostname\": \"\" "
+ "}",
+ host->toElement4()->str());
+
+ EXPECT_EQ("{ "
+ "\"client-classes\": [ ], "
+ "\"duid\": \"11:12:13:14:15\", "
+ "\"hostname\": \"myhost\", "
+ "\"ip-addresses\": [ ], "
+ "\"option-data\": [ ], "
+ "\"prefixes\": [ ] "
+ "}",
+ host->toElement6()->str());
+
+ // Add some classes.
+ host->addClientClass4("modem");
+ host->addClientClass4("router");
+ // Set invisible negative cache.
+ host->setNegative(true);
+
+ EXPECT_EQ("{ "
+ "\"boot-file-name\": \"\", "
+ "\"client-classes\": [ \"modem\", \"router\" ], "
+ "\"duid\": \"11:12:13:14:15\", "
+ "\"hostname\": \"myhost\", "
+ "\"next-server\": \"0.0.0.0\", "
+ "\"option-data\": [ ], "
+ "\"server-hostname\": \"\" "
+ "}",
+ host->toElement4()->str());
+
+ EXPECT_EQ("{ "
+ "\"client-classes\": [ ], "
+ "\"duid\": \"11:12:13:14:15\", "
+ "\"hostname\": \"myhost\", "
+ "\"ip-addresses\": [ ], "
+ "\"option-data\": [ ], "
+ "\"prefixes\": [ ] "
+ "}",
+ host->toElement6()->str());
+
+ // Now the classes are in defined order (vs. alphabetical order).
+ host->addClientClass6("hub");
+ host->addClientClass6("device");
+
+ EXPECT_EQ("{ "
+ "\"boot-file-name\": \"\", "
+ "\"client-classes\": [ \"modem\", \"router\" ], "
+ "\"duid\": \"11:12:13:14:15\", "
+ "\"hostname\": \"myhost\", "
+ "\"next-server\": \"0.0.0.0\", "
+ "\"option-data\": [ ], "
+ "\"server-hostname\": \"\" "
+ "}",
+ host->toElement4()->str());
+
+ EXPECT_EQ("{ "
+ "\"client-classes\": [ \"hub\", \"device\" ], "
+ "\"duid\": \"11:12:13:14:15\", "
+ "\"hostname\": \"myhost\", "
+ "\"ip-addresses\": [ ], "
+ "\"option-data\": [ ], "
+ "\"prefixes\": [ ] "
+ "}",
+ host->toElement6()->str());
+}
+
+// Test verifies if the host can store HostId properly.
+TEST_F(HostTest, hostId) {
+ HostPtr host;
+ ASSERT_NO_THROW(host.reset(new Host("01:02:03:04:05:06", "hw-address",
+ SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"),
+ "myhost.example.com")));
+ EXPECT_EQ(0, host->getHostId());
+
+ EXPECT_NO_THROW(host->setHostId(12345));
+
+ EXPECT_EQ(12345, host->getHostId());
+}
+
+// Test verifies if we can modify the host keys.
+TEST_F(HostTest, keys) {
+ HostPtr host;
+ ASSERT_NO_THROW(host.reset(new Host("01:02:03:04:05:06", "hw-address",
+ SubnetID(1), SubnetID(2),
+ IOAddress("192.0.2.3"),
+ "myhost.example.com")));
+ // Key must be empty
+ EXPECT_EQ(0, host->getKey().getAuthKey().size());
+ EXPECT_EQ("", host->getKey().toText());
+
+ // now set to random value
+ const std::vector<uint8_t>& random_key(AuthKey::getRandomKeyString());
+ host->setKey(AuthKey(random_key));
+ EXPECT_EQ(random_key, host->getKey().getAuthKey());
+}
+
+// Test verifies if getRandomKeyString can generate 1000 keys which are random
+TEST_F(HostTest, randomKeys) {
+ // use hashtable and set size to 1000
+ std::unordered_set<std::vector<uint8_t>,
+ boost::hash<std::vector<uint8_t>>> keys;
+
+ int dup_element = 0;
+ const uint16_t max_iter = 1000;
+ uint16_t iter_num = 0;
+ size_t max_hash_size = 1000;
+
+ keys.reserve(max_hash_size);
+
+ for (iter_num = 0; iter_num < max_iter; iter_num++) {
+ std::vector<uint8_t> key = AuthKey::getRandomKeyString();
+ if (keys.count(key)) {
+ dup_element++;
+ break;
+ }
+
+ keys.insert(key);
+ }
+
+ EXPECT_EQ(0, dup_element);
+}
+
+// Test performs basic functionality test of the AuthKey class
+TEST(AuthKeyTest, basicTest) {
+ // Call the constructor with default argument
+ // Default constructor should generate random string of 16 bytes
+ AuthKey defaultKey;
+ ASSERT_EQ(16, defaultKey.getAuthKey().size());
+
+ AuthKey longKey("0123456789abcdef1122334455667788");
+ ASSERT_EQ(16, longKey.getAuthKey().size());
+
+ // Check the setters for valid and invalid string
+ std::string key16ByteStr = "000102030405060708090A0B0C0D0E0F";
+ std::vector<uint8_t> key16ByteBin = {
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf
+ };
+ std::string key18ByteStr = "0123456789abcdefgh";
+
+ AuthKey defaultTestKey;
+
+ defaultTestKey.setAuthKey(key16ByteStr);
+ ASSERT_EQ(16, defaultTestKey.getAuthKey().size());
+ ASSERT_EQ(key16ByteStr, defaultTestKey.toText());
+ ASSERT_EQ(key16ByteBin, defaultTestKey.getAuthKey());
+
+ ASSERT_THROW(defaultTestKey.setAuthKey(key18ByteStr), BadValue);
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/ifaces_config_parser_unittest.cc b/src/lib/dhcpsrv/tests/ifaces_config_parser_unittest.cc
new file mode 100644
index 0000000..9e65462
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/ifaces_config_parser_unittest.cc
@@ -0,0 +1,392 @@
+// Copyright (C) 2015-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <cc/data.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/parsers/ifaces_config_parser.h>
+#include <testutils/test_to_element.h>
+#include <gtest/gtest.h>
+
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::test;
+
+namespace {
+
+/// @brief Test fixture class for @c IfacesConfigParser
+class IfacesConfigParserTest : public ::testing::Test {
+protected:
+
+ /// @brief Setup for each test.
+ ///
+ /// Clears the configuration in the @c CfgMgr.
+ virtual void SetUp();
+
+ /// @brief Cleans up after each test.
+ ///
+ /// Clears the configuration in the @c CfgMgr.
+ virtual void TearDown();
+
+};
+
+void
+IfacesConfigParserTest::SetUp() {
+ CfgMgr::instance().clear();
+ IfaceMgr::instance().setTestMode(true);
+}
+
+void
+IfacesConfigParserTest::TearDown() {
+ CfgMgr::instance().clear();
+ IfaceMgr::instance().setTestMode(false);
+ IfaceMgr::instance().clearIfaces();
+ IfaceMgr::instance().closeSockets();
+ IfaceMgr::instance().detectIfaces();
+}
+
+// This test checks that the parser correctly parses the list of interfaces
+// on which the server should listen.
+TEST_F(IfacesConfigParserTest, interfaces) {
+ // Creates fake interfaces with fake addresses.
+ IfaceMgrTestConfig test_config(true);
+
+ // Configuration with one interface.
+ std::string config =
+ "{ \"interfaces\": [ \"eth0\" ], \"re-detect\": false }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse the configuration.
+ IfacesConfigParser parser(AF_INET, false);
+ CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ ASSERT_TRUE(cfg_iface);
+ ASSERT_NO_THROW(parser.parse(cfg_iface, config_element));
+
+ // Check it can be unparsed.
+ runToElementTest<CfgIface>(config, *cfg_iface);
+
+ // Open sockets according to the parsed configuration.
+ SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
+ ASSERT_TRUE(cfg);
+ ASSERT_NO_THROW(cfg->getCfgIface()->openSockets(AF_INET, 10000));
+
+ // Only eth0 should have an open socket.
+ EXPECT_TRUE(test_config.socketOpen("eth0", AF_INET));
+ EXPECT_FALSE(test_config.socketOpen("eth1", AF_INET));
+
+ // Reset configuration.
+ cfg->getCfgIface()->closeSockets();
+ CfgMgr::instance().clear();
+
+ // Try similar configuration but this time add a wildcard interface
+ // to see if sockets will open on all interfaces.
+ config = "{ \"interfaces\": [ \"eth0\", \"*\" ], \"re-detect\": false }";
+ config_element = Element::fromJSON(config);
+
+ cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ ASSERT_TRUE(cfg_iface);
+ ASSERT_NO_THROW(parser.parse(cfg_iface, config_element));
+
+ runToElementTest<CfgIface>(config, *cfg_iface);
+
+ cfg = CfgMgr::instance().getStagingCfg();
+ ASSERT_NO_THROW(cfg->getCfgIface()->openSockets(AF_INET, 10000));
+
+ EXPECT_TRUE(test_config.socketOpen("eth0", AF_INET));
+ EXPECT_TRUE(test_config.socketOpen("eth1", AF_INET));
+}
+
+// This test checks that the parser does not re-detect interfaces in test mode.
+TEST_F(IfacesConfigParserTest, testMode) {
+ // Creates fake interfaces with fake addresses.
+ IfaceMgrTestConfig test_config(true);
+
+ // Configuration with wildcard..
+ std::string config =
+ "{ \"interfaces\": [ \"*\" ], \"re-detect\": true }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse the configuration in test mode.
+ IfacesConfigParser parser(AF_INET, true);
+ CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ ASSERT_TRUE(cfg_iface);
+ ASSERT_NO_THROW(parser.parse(cfg_iface, config_element));
+
+ // Verify we still have the eth1961 interface.
+ EXPECT_TRUE(IfaceMgr::instance().getIface("eth1961"));
+
+ // Reparse in not test mode.
+ IfacesConfigParser parser2(AF_INET, false);
+ CfgMgr::instance().clear();
+ cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ ASSERT_TRUE(cfg_iface);
+ ASSERT_NO_THROW(parser2.parse(cfg_iface, config_element));
+
+ // The eth1961 interface no longer exists.
+ EXPECT_FALSE(IfaceMgr::instance().getIface("eth1961"));
+}
+
+// This test checks that the parsed structure can be converted back to Element
+// tree.
+TEST_F(IfacesConfigParserTest, toElement) {
+ // Creates fake interfaces with fake addresses.
+ IfaceMgrTestConfig test_config(true);
+
+ // Configuration with one interface.
+ std::string config =
+ "{ \"user-context\": { \"foo\": \"bar\" }, "
+ " \"interfaces\": [ \"eth0\" ], "
+ " \"dhcp-socket-type\": \"udp\","
+ " \"outbound-interface\": \"use-routing\", "
+ " \"re-detect\": false }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse the configuration.
+ IfacesConfigParser parser(AF_INET, false);
+ CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ ASSERT_TRUE(cfg_iface);
+ ASSERT_NO_THROW(parser.parse(cfg_iface, config_element));
+
+ // Check it can be unparsed.
+ runToElementTest<CfgIface>(config, *cfg_iface);
+}
+
+
+// This test verifies that it is possible to select the raw socket
+// use in the configuration for interfaces.
+TEST_F(IfacesConfigParserTest, socketTypeRaw) {
+ // Create the reference configuration, which we will compare
+ // the parsed configuration to.
+ CfgIface cfg_ref;
+
+ // Configuration with a raw socket selected.
+ std::string config = "{ ""\"interfaces\": [ ],"
+ " \"dhcp-socket-type\": \"raw\","
+ " \"re-detect\": false }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse the configuration.
+ IfacesConfigParser parser(AF_INET, false);
+ CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ ASSERT_NO_THROW(parser.parse(cfg_iface, config_element));
+
+ // Compare the resulting configuration with a reference
+ // configuration using the raw socket.
+ SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
+ ASSERT_TRUE(cfg);
+ cfg_ref.useSocketType(AF_INET, CfgIface::SOCKET_RAW);
+ EXPECT_TRUE(*cfg->getCfgIface() == cfg_ref);
+}
+
+// This test verifies that it is possible to select the datagram socket
+// use in the configuration for interfaces.
+TEST_F(IfacesConfigParserTest, socketTypeDatagram) {
+ // Create the reference configuration, which we will compare
+ // the parsed configuration to.
+ CfgIface cfg_ref;
+
+ // Configuration with a datagram socket selected.
+ std::string config = "{ \"interfaces\": [ ],"
+ " \"dhcp-socket-type\": \"udp\","
+ " \"re-detect\": false }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse the configuration.
+ IfacesConfigParser parser(AF_INET, false);
+ CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ ASSERT_TRUE(cfg_iface);
+ ASSERT_NO_THROW(parser.parse(cfg_iface, config_element));
+
+ // Check it can be unparsed.
+ runToElementTest<CfgIface>(config, *cfg_iface);
+
+ // Compare the resulting configuration with a reference
+ // configuration using the raw socket.
+ SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
+ ASSERT_TRUE(cfg);
+ cfg_ref.useSocketType(AF_INET, CfgIface::SOCKET_UDP);
+ ASSERT_TRUE(cfg->getCfgIface());
+ EXPECT_TRUE(*cfg->getCfgIface() == cfg_ref);
+}
+
+// Test that the configuration rejects the invalid socket type.
+TEST_F(IfacesConfigParserTest, socketTypeInvalid) {
+ // For DHCPv4 we only accept the raw socket or datagram socket.
+ IfacesConfigParser parser4(AF_INET, false);
+ CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ std::string config = "{ \"interfaces\": [ ],"
+ "\"dhcp-socket-type\": \"default\","
+ " \"re-detect\": false }";
+ ElementPtr config_element = Element::fromJSON(config);
+ ASSERT_THROW(parser4.parse(cfg_iface, config_element), DhcpConfigError);
+
+ // For DHCPv6 we don't accept any socket type.
+ IfacesConfigParser parser6(AF_INET6, false);
+ config = "{ \"interfaces\": [ ],"
+ " \"dhcp-socket-type\": \"udp\","
+ " \"re-detect\": false }";
+ config_element = Element::fromJSON(config);
+ ASSERT_THROW(parser6.parse(cfg_iface, config_element), DhcpConfigError);
+}
+
+// Tests that outbound-interface is parsed properly.
+TEST_F(IfacesConfigParserTest, outboundInterface) {
+ // For DHCPv4 we accept 'use-routing' or 'same-as-inbound'.
+ IfacesConfigParser parser4(AF_INET, false);
+
+ // For DHCPv6 we don't accept this at all.
+ IfacesConfigParser parser6(AF_INET6, false);
+
+ CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+
+ // The default should be to use the same as client's query packet.
+ EXPECT_EQ(CfgIface::SAME_AS_INBOUND, cfg_iface->getOutboundIface());
+
+ // Value 1: use-routing
+ std::string config = "{ \"interfaces\": [ ],"
+ "\"outbound-interface\": \"use-routing\","
+ " \"re-detect\": false }";
+ ElementPtr config_element = Element::fromJSON(config);
+ ASSERT_NO_THROW(parser4.parse(cfg_iface, config_element));
+ EXPECT_EQ(CfgIface::USE_ROUTING, cfg_iface->getOutboundIface());
+ EXPECT_THROW(parser6.parse(cfg_iface, config_element), DhcpConfigError);
+
+ // Value 2: same-as-inbound
+ config = "{ \"interfaces\": [ ],"
+ "\"outbound-interface\": \"same-as-inbound\","
+ " \"re-detect\": false }";
+ config_element = Element::fromJSON(config);
+ ASSERT_NO_THROW(parser4.parse(cfg_iface, config_element));
+ EXPECT_EQ(CfgIface::SAME_AS_INBOUND, cfg_iface->getOutboundIface());
+ EXPECT_THROW(parser6.parse(cfg_iface, config_element), DhcpConfigError);
+
+ // Other values are not supported.
+ config = "{ \"interfaces\": [ ],"
+ "\"outbound-interface\": \"default\","
+ " \"re-detect\": false }";
+ config_element = Element::fromJSON(config);
+ EXPECT_THROW(parser4.parse(cfg_iface, config_element), DhcpConfigError);
+ EXPECT_THROW(parser6.parse(cfg_iface, config_element), DhcpConfigError);
+}
+
+// Tests that service-sockets-require-all is parsed properly.
+TEST_F(IfacesConfigParserTest, serviceSocketRequireAll) {
+ // Create the reference configuration, which we will compare
+ // the parsed configuration to.
+ CfgIface cfg_ref;
+
+ // Configuration with a require all sockets to open selected.
+ std::string config = "{ \"interfaces\": [ ],"
+ " \"re-detect\": false,"
+ " \"service-sockets-require-all\": true }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse the configuration.
+ IfacesConfigParser parser(AF_INET, false);
+ CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ ASSERT_TRUE(cfg_iface);
+ ASSERT_NO_THROW(parser.parse(cfg_iface, config_element));
+ EXPECT_TRUE(cfg_iface->getServiceSocketsRequireAll());
+
+ // Check it can be unparsed.
+ runToElementTest<CfgIface>(config, *cfg_iface);
+}
+
+// Tests that service-sockets-max-retries is parsed properly.
+TEST_F(IfacesConfigParserTest, serviceSocketMaxRetries) {
+ // Create the reference configuration, which we will compare
+ // the parsed configuration to.
+ CfgIface cfg_ref;
+
+ // Configuration with a non-zero retries selected.
+ std::string config = "{ \"interfaces\": [ ],"
+ " \"re-detect\": false,"
+ " \"service-sockets-max-retries\": 42 }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse the configuration.
+ IfacesConfigParser parser(AF_INET, false);
+ CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ ASSERT_TRUE(cfg_iface);
+ ASSERT_NO_THROW(parser.parse(cfg_iface, config_element));
+ EXPECT_FALSE(cfg_iface->getServiceSocketsRequireAll());
+
+ // Configuration should contain a number of retries and a wait time.
+ std::string expected_config = "{ \"interfaces\": [ ],"
+ " \"re-detect\": false,"
+ " \"service-sockets-retry-wait-time\": 5000,"
+ " \"service-sockets-max-retries\": 42 }";
+
+ // Check it can be unparsed.
+ runToElementTest<CfgIface>(expected_config, *cfg_iface);
+}
+
+// Tests that service-sockets-retry-wait-time is parsed properly if
+// service-sockets-max-retries is provided.
+TEST_F(IfacesConfigParserTest, serviceSocketRetryWaitTime) {
+ // Create the reference configuration, which we will compare
+ // the parsed configuration to.
+ CfgIface cfg_ref;
+
+ // Configuration with a non-zero number of retries and a non-default wait time.
+ std::string config = "{ \"interfaces\": [ ],"
+ " \"re-detect\": false,"
+ " \"service-sockets-retry-wait-time\": 4224,"
+ " \"service-sockets-max-retries\": 42 }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse the configuration.
+ IfacesConfigParser parser(AF_INET, false);
+ CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ ASSERT_TRUE(cfg_iface);
+ ASSERT_NO_THROW(parser.parse(cfg_iface, config_element));
+ EXPECT_FALSE(cfg_iface->getServiceSocketsRequireAll());
+
+ // Check it can be unparsed.
+ runToElementTest<CfgIface>(config, *cfg_iface);
+}
+
+// Tests that service-sockets-retry-wait-time is ignored if
+// service-sockets-max-retries is not provided.
+TEST_F(IfacesConfigParserTest, serviceSocketRetryWaitTimeWithoutMaxRetries) {
+ // Create the reference configuration, which we will compare
+ // the parsed configuration to.
+ CfgIface cfg_ref;
+
+ // Configuration with zero (default) retries and a non-default wait time.
+ std::string config = "{ \"interfaces\": [ ],"
+ " \"re-detect\": false,"
+ " \"service-sockets-retry-wait-time\": 4224 }";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse the configuration.
+ IfacesConfigParser parser(AF_INET, false);
+ CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+ ASSERT_TRUE(cfg_iface);
+ ASSERT_NO_THROW(parser.parse(cfg_iface, config_element));
+ EXPECT_FALSE(cfg_iface->getServiceSocketsRequireAll());
+
+ // Retry wait time is not applicable; it is skipped.
+ std::string expected_config = "{ \"interfaces\": [ ],"
+ " \"re-detect\": false }";
+
+ // Check it can be unparsed.
+ runToElementTest<CfgIface>(expected_config, *cfg_iface);
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/ip_range_permutation_unittest.cc b/src/lib/dhcpsrv/tests/ip_range_permutation_unittest.cc
new file mode 100644
index 0000000..25a45f4
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/ip_range_permutation_unittest.cc
@@ -0,0 +1,140 @@
+// Copyright (C) 2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcpsrv/ip_range_permutation.h>
+
+#include <gtest/gtest.h>
+
+#include <set>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+
+namespace {
+
+// This test verifies that the object can be successfully constructed for
+// both IPv4 and IPv6 address range.
+TEST(IPRangePermutationTest, constructor) {
+ ASSERT_NO_THROW({
+ AddressRange range(IOAddress("192.0.2.10"), IOAddress("192.0.2.100"));
+ IPRangePermutation perm(range);
+ });
+ ASSERT_NO_THROW({
+ AddressRange range(IOAddress("3000::"), IOAddress("3000::10"));
+ IPRangePermutation perm(range);
+ });
+}
+
+// This test verifies that a permutation of IPv4 address range can
+// be generated.
+TEST(IPRangePermutationTest, ipv4) {
+ // Create address range with 91 addresses.
+ AddressRange range(IOAddress("192.0.2.10"), IOAddress("192.0.2.100"));
+ IPRangePermutation perm(range);
+
+ // This set will record unique IP addresses generated.
+ std::set<IOAddress> addrs;
+ bool done = false;
+
+ // Call the next() function 95 times. The first 91 calls should return non-zero
+ // IP addresses.
+ for (auto i = 0; i < 95; ++i) {
+ auto next = perm.next(done);
+ if (!next.isV4Zero()) {
+ // Make sure the returned address is within the range.
+ EXPECT_LE(range.start_, next);
+ EXPECT_LE(next, range.end_);
+ }
+ // If we went over all addresses in the range, the flags indicating that
+ // the permutation is exhausted should be set to true.
+ if (i >= 90) {
+ EXPECT_TRUE(done);
+ EXPECT_TRUE(perm.exhausted());
+ } else {
+ // We're not done yet, so these flag should still be false.
+ EXPECT_FALSE(done);
+ EXPECT_FALSE(perm.exhausted());
+ }
+ // Insert the address returned to the set.
+ addrs.insert(next);
+ }
+
+ // We should have recorded 92 unique addresses, including the zero address.
+ EXPECT_EQ(92, addrs.size());
+ EXPECT_TRUE(addrs.begin()->isV4Zero());
+}
+
+// This test verifies that a permutation of IPv6 address range can
+// be generated.
+TEST(IPRangePermutationTest, ipv6) {
+ AddressRange range(IOAddress("2001:db8:1::1:fea0"),
+ IOAddress("2001:db8:1::2:abcd"));
+ IPRangePermutation perm(range);
+
+ std::set<IOAddress> addrs;
+ bool done = false;
+ for (auto i = 0; i < 44335; ++i) {
+ auto next = perm.next(done);
+ if (!next.isV6Zero()) {
+ // Make sure that the address is within the range.
+ EXPECT_LE(range.start_, next);
+ EXPECT_LE(next, range.end_);
+ }
+ // If we went over all addresses in the range, the flags indicating that
+ // the permutation is exhausted should be set to true.
+ if (i >= 44333) {
+ EXPECT_TRUE(done);
+ EXPECT_TRUE(perm.exhausted());
+ } else {
+ // We're not done yet, so these flag should still be false.
+ EXPECT_FALSE(done);
+ EXPECT_FALSE(perm.exhausted());
+ }
+ // Insert the address returned to the set.
+ addrs.insert(next);
+ }
+ // We should have recorded 44335 unique addresses, including the zero address.
+ EXPECT_EQ(44335, addrs.size());
+ EXPECT_TRUE(addrs.begin()->isV6Zero());
+}
+
+// This test verifies that a permutation of delegated prefixes can be
+// generated.
+TEST(IPRangePermutationTest, pd) {
+ PrefixRange range(IOAddress("3000::"), 112, 120);
+ IPRangePermutation perm(range);
+
+ std::set<IOAddress> addrs;
+ bool done = false;
+ for (auto i = 0; i < 257; ++i) {
+ auto next = perm.next(done);
+ if (!next.isV6Zero()) {
+ // Make sure the prefix is within the range.
+ EXPECT_LE(range.start_, next);
+ EXPECT_LE(next, range.end_);
+ }
+ // If we went over all delegated prefixes in the range, the flags indicating
+ // that the permutation is exhausted should be set to true.
+ if (i >= 255) {
+ EXPECT_TRUE(done);
+ EXPECT_TRUE(perm.exhausted());
+ } else {
+ // We're not done yet, so these flag should still be false.
+ EXPECT_FALSE(done);
+ EXPECT_FALSE(perm.exhausted());
+ }
+ // Insert the prefix returned to the set.
+ addrs.insert(next);
+ }
+
+ // We should have recorded 257 unique addresses, including the zero address.
+ EXPECT_EQ(257, addrs.size());
+ EXPECT_TRUE(addrs.begin()->isV6Zero());
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/ip_range_unittest.cc b/src/lib/dhcpsrv/tests/ip_range_unittest.cc
new file mode 100644
index 0000000..754ab6f
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/ip_range_unittest.cc
@@ -0,0 +1,63 @@
+// Copyright (C) 2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcpsrv/ip_range.h>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+
+namespace {
+
+// This test verifies that an exception is thrown upon an attempt to
+// create an address range from invalid values and that no exception
+// is thrown when the values are correct.
+TEST(AddressRangeTest, constructor) {
+ // The start address must be lower or equal the end address.
+ EXPECT_THROW(AddressRange(IOAddress("192.0.2.100"), IOAddress("192.0.2.99")),
+ BadValue);
+ // The start and end address must be of the same family.
+ EXPECT_THROW(AddressRange(IOAddress("192.0.2.100"), IOAddress("2001:db8:1::1")),
+ BadValue);
+ // It is allowed to create address range with a single IP address.
+ EXPECT_NO_THROW(AddressRange(IOAddress("192.0.2.100"), IOAddress("192.0.2.100")));
+}
+
+// This test verifies successful construction of the prefix range.
+TEST(PrefixRangeTest, constructor) {
+ boost::scoped_ptr<PrefixRange> range;
+ ASSERT_NO_THROW(range.reset(new PrefixRange(IOAddress("2001:db8:1::"), 64, 96)));
+ EXPECT_EQ("2001:db8:1::", range->start_.toText());
+ EXPECT_EQ("2001:db8:1:0:ffff:ffff::", range->end_.toText());
+
+ ASSERT_NO_THROW(range.reset(new PrefixRange(IOAddress("2001:db8:1:2::"), 80, 120)));
+ EXPECT_EQ("2001:db8:1:2::", range->start_.toText());
+ EXPECT_EQ("2001:db8:1:2:0:ffff:ffff:ff00", range->end_.toText());
+
+ ASSERT_NO_THROW(range.reset(new PrefixRange(IOAddress("2001:db8:1:2::"), 80, 127)));
+ EXPECT_EQ("2001:db8:1:2::", range->start_.toText());
+ EXPECT_EQ("2001:db8:1:2:0:ffff:ffff:fffe", range->end_.toText());
+}
+
+// This test verifies that exception is thrown upon an attempt to
+// create a prefix range from invalid values.
+TEST(PrefixRangeTest, constructorWithInvalidValues) {
+ boost::scoped_ptr<PrefixRange> range;
+ // It must be IPv6 prefix.
+ EXPECT_THROW(PrefixRange(IOAddress("192.0.2.0"), 8, 16), BadValue);
+ // Delegated length must not be lower than prefix length.
+ EXPECT_THROW(PrefixRange(IOAddress("2001:db8:1::"), 96, 64), BadValue);
+ // Lengths must not exceed 128.
+ EXPECT_THROW(PrefixRange(IOAddress("2001:db8:1::"), 200, 204), BadValue);
+ // End must not be lower than start.
+ EXPECT_THROW(PrefixRange(IOAddress("2001:db8:1:1::6:0"), IOAddress("2001:db8:1::5:0"), 112),
+ BadValue);
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/lease_file_loader_unittest.cc b/src/lib/dhcpsrv/tests/lease_file_loader_unittest.cc
new file mode 100644
index 0000000..ee25e16
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/lease_file_loader_unittest.cc
@@ -0,0 +1,881 @@
+// Copyright (C) 2015-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcpsrv/csv_lease_file4.h>
+#include <dhcpsrv/csv_lease_file6.h>
+#include <dhcpsrv/memfile_lease_storage.h>
+#include <dhcpsrv/lease_file_loader.h>
+#include <dhcpsrv/testutils/lease_file_io.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/cfg_consistency.h>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+#include <sstream>
+#include <string>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+
+namespace {
+
+/// @brief Test fixture class for @c LeaseFileLoader class.
+class LeaseFileLoaderTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Initializes filename used for unit tests and creates an
+ /// IO object to be used to write to this file.
+ LeaseFileLoaderTest();
+
+ /// @brief Destructor
+ ///
+ /// Removes any configuration that may have been added in CfgMgr.
+ virtual ~LeaseFileLoaderTest();
+
+ /// @brief Prepends the absolute path to the file specified
+ /// as an argument.
+ ///
+ /// @param filename Name of the file.
+ /// @return Absolute path to the test file.
+ static std::string absolutePath(const std::string& filename);
+
+ /// @brief Retrieves the lease from the storage using an IP address.
+ ///
+ /// This method returns the pointer to the @c Lease4 or @c Lease6
+ /// object representing the lease in the container. This is used to
+ /// check if the lease was parsed in the lease file and added to the
+ /// container by the @c LeaseFileLoader::load method.
+ ///
+ /// @param address A string representation of the leased address.
+ /// @param storage A reference to the container in which the lease
+ /// is to be searched.
+ /// @tparam LeasePtrType Type of the returned object: @c Lease4Ptr
+ /// @c Lease6Ptr.
+ /// @tparam LeaseStorage Type of the container: @c Lease4Container
+ /// @c Lease6Container.
+ ///
+ /// @return A pointer to the lease or NULL if no lease found.
+ template<typename LeasePtrType, typename LeaseStorage>
+ static LeasePtrType getLease(const std::string& address, const LeaseStorage& storage) {
+ typedef typename LeaseStorage::template nth_index<0>::type SearchIndex;
+ // Both Lease4Storage and Lease6Storage use index 0 to retrieve the
+ // lease using an IP address.
+ const SearchIndex& idx = storage.template get<0>();
+ typename SearchIndex::iterator lease = idx.find(IOAddress(address));
+ // Lease found. Return it.
+ if (lease != idx.end()) {
+ return (*lease);
+ }
+ // No lease found.
+ return (LeasePtrType());
+ }
+
+ /// @brief Tests the write function.
+ ///
+ /// This method writes the leases from the storage container to the lease file
+ /// then compares the output to the string provided in the arguments to verify
+ /// the write was correct. The order of the leases in the output will depend
+ /// on the order in which the container provides the leases.
+ ///
+ /// @param lease_file A reference to the file to write to
+ /// @param storage A reference to the container to be written to the file
+ /// @param compare The string to compare to what was read from the file
+ ///
+ /// @tparam LeaseObjectType A @c Lease4 or @c Lease6.
+ /// @tparam LeaseFileType A @c CSVLeaseFile4 or @c CSVLeaseFile6.
+ /// @tparam StorageType A @c Lease4Storage or @c Lease6Storage.
+ ///
+ template<typename LeaseObjectType, typename LeaseFileType,
+ typename StorageType>
+ void writeLeases(LeaseFileType& lease_file,
+ const StorageType& storage,
+ const std::string& compare) {
+ // Prepare for a new file, close and remove the old
+ lease_file.close();
+ io_.removeFile();
+
+ // Write the current leases to the file
+ LeaseFileLoader::write<LeaseObjectType, LeaseFileType, StorageType>
+ (lease_file, storage);
+
+ // Compare to see if we got what we expected.
+ EXPECT_EQ(compare, io_.readFile());
+ }
+
+ /// @brief Checks the stats for the file
+ ///
+ /// This method is passed a leasefile and the values for the statistics it
+ /// should have for comparison.
+ ///
+ /// @param lease_file A reference to the file we are using
+ /// @param reads the number of attempted reads
+ /// @param read_leases the number of valid leases read
+ /// @param read_errs the number of errors while reading leases
+ /// @param writes the number of attempted writes
+ /// @param write_leases the number of leases successfully written
+ /// @param write_errs the number of errors while writing
+ ///
+ /// @tparam LeaseFileType A @c CSVLeaseFile4 or @c CSVLeaseFile6.
+ template<typename LeaseFileType>
+ void checkStats(LeaseFileType& lease_file,
+ uint32_t reads, uint32_t read_leases,
+ uint32_t read_errs, uint32_t writes,
+ uint32_t write_leases, uint32_t write_errs) const {
+ EXPECT_EQ(reads, lease_file.getReads());
+ EXPECT_EQ(read_leases, lease_file.getReadLeases());
+ EXPECT_EQ(read_errs, lease_file.getReadErrs());
+ EXPECT_EQ(writes, lease_file.getWrites());
+ EXPECT_EQ(write_leases, lease_file.getWriteLeases());
+ EXPECT_EQ(write_errs, lease_file.getWriteErrs());
+ }
+
+ /// @brief Name of the test lease file.
+ std::string filename_;
+
+ /// @brief Object providing access to lease file IO.
+ LeaseFileIO io_;
+
+ std::string v4_hdr_; ///< String for the header of the v4 csv test file
+ std::string v6_hdr_; ///< String for the header of the v6 csv test file
+
+ Lease4Storage storage4_; ///< Storage for IPv4 leases
+ Lease6Storage storage6_; ///< Storage for IPv4 leases
+
+ /// @brief Creates IPv4 subnet with specified parameters
+ ///
+ /// @param subnet_txt subnet in textual form, e.g. 192.0.2.0/24
+ /// @param subnet_id id to be used.
+ /// @return A pointer to Subnet4 object
+ Subnet4Ptr createSubnet4(std::string subnet_txt, SubnetID id) {
+ size_t pos = subnet_txt.find("/");
+ isc::asiolink::IOAddress addr(subnet_txt.substr(0, pos));
+ size_t len = boost::lexical_cast<unsigned int>(subnet_txt.substr(pos + 1));
+
+ return (Subnet4Ptr(new Subnet4(addr, len, 1000, 2000, 3000, id)));
+ }
+
+ /// @brief Creates IPv6 subnet with specified parameters
+ ///
+ /// @param subnet_txt subnet in textual form, e.g. 2001:db8::/32
+ /// @param subnet_id id to be used.
+ /// @return A pointer to Subnet4 object
+ Subnet6Ptr createSubnet6(std::string subnet_txt, SubnetID id) {
+ size_t pos = subnet_txt.find("/");
+ isc::asiolink::IOAddress addr(subnet_txt.substr(0, pos));
+ size_t len = boost::lexical_cast<unsigned int>(subnet_txt.substr(pos + 1));
+
+ return (Subnet6Ptr(new Subnet6(addr, len, 1000, 2000, 3000, 4000, id)));
+ }
+
+ /// @brief Checks if IPv4 lease loaded from file is sanity checked.
+ ///
+ /// This method writes a simple lease file with one lease in it,
+ /// then sets sanity checks to tested level, then tries to load
+ /// the lease file and finally checks whether the lease was loaded
+ /// or not.
+ ///
+ /// @param lease address of the lease in text form
+ /// @param lease_id subnet-id to be used in a lease
+ /// @param subnet_txt Text representation of the subnet, e.g. 192.0.2.0/24
+ /// @param subnet_id Subnet-id of the subnet to be created
+ /// @param sanity level of sanity checks
+ /// @param exp_present is the lease expected to be loaded (true = yes)
+ /// @param exp_id expected subnet-id of the loaded lease
+ void sanityChecks4(std::string lease, SubnetID lease_id,
+ std::string subnet_txt, SubnetID subnet_id,
+ CfgConsistency::LeaseSanity sanity,
+ bool exp_present, SubnetID exp_id) {
+
+ // Create the subnet and add it to configuration.
+ if (!subnet_txt.empty()) {
+ Subnet4Ptr subnet = createSubnet4(subnet_txt, subnet_id);
+ ASSERT_NO_THROW(CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->add(subnet));
+ }
+
+ std::stringstream file_content;
+ file_content << v4_hdr_ << lease << ",dd:de:ba:0d:1b:2e,"
+ << "0a:00:01:04,100,100," << static_cast<int>(lease_id)
+ << ",0,0,,1,\n";
+
+ ASSERT_NO_THROW(CfgMgr::instance().getStagingCfg()->getConsistency()
+ ->setLeaseSanityCheck(sanity));
+
+ io_.writeFile(file_content.str());
+
+ boost::scoped_ptr<CSVLeaseFile4> lf(new CSVLeaseFile4(filename_));
+ ASSERT_NO_THROW(lf->open());
+
+ // Load leases from the file.
+ ASSERT_NO_THROW(LeaseFileLoader::load<Lease4>(*lf, storage4_, 10));
+
+ {
+ SCOPED_TRACE("Read leases");
+ checkStats(*lf, 2, 1, 0, 0, 0, 0);
+ }
+
+ // Check how many leases were actually loaded.
+ ASSERT_EQ( (exp_present ? 1 : 0), storage4_.size());
+
+ Lease4Ptr l = getLease<Lease4Ptr>(lease, storage4_);
+
+ if (exp_present) {
+ ASSERT_TRUE(l) << "lease not found, but expected";
+ EXPECT_EQ(l->subnet_id_, exp_id);
+ } else {
+ EXPECT_FALSE(l) << "lease found, but was not expected";
+ }
+ }
+
+ /// @brief Checks if IPv6 lease loaded from file is sanity checked.
+ ///
+ /// This method writes a simple lease file with one lease in it,
+ /// then sets sanity checks to tested level, then tries to load
+ /// the lease file and finally checks whether the lease was loaded
+ /// or not.
+ ///
+ /// @param lease address of the lease in text form
+ /// @param lease_id subnet-id to be used in a lease
+ /// @param subnet_txt Text representation of the subnet, e.g. 192.0.2.0/24
+ /// @param subnet_id Subnet-id of the subnet to be created
+ /// @param sanity level of sanity checks
+ /// @param exp_present is the lease expected to be loaded (true = yes)
+ /// @param exp_id expected subnet-id of the loaded lease
+ /// @param prefix_len length of the prefix if the lease created should be
+ /// a PD lease. Defaults to 0.
+ void sanityChecks6(std::string lease, SubnetID lease_id,
+ std::string subnet_txt, SubnetID subnet_id,
+ CfgConsistency::LeaseSanity sanity,
+ bool exp_present, SubnetID exp_id,
+ unsigned int prefix_len = 0) {
+
+ // Create the subnet and add it to configuration.
+ if (!subnet_txt.empty()) {
+ Subnet6Ptr subnet = createSubnet6(subnet_txt, subnet_id);
+ ASSERT_NO_THROW(CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->add(subnet));
+ }
+
+ std::stringstream file_content;
+
+ file_content << v6_hdr_ << lease << ",dd:de:ba:0d:1b:2e,"
+ << "300,300," << static_cast<int>(lease_id) << ",150,"
+ << (prefix_len > 0 ? Lease::TYPE_PD : Lease::TYPE_NA)
+ << ",8," << prefix_len << ",0,0,,,1,\n";
+
+ ASSERT_NO_THROW(CfgMgr::instance().getStagingCfg()->getConsistency()
+ ->setLeaseSanityCheck(sanity));
+
+ io_.writeFile(file_content.str());
+
+ boost::scoped_ptr<CSVLeaseFile6> lf(new CSVLeaseFile6(filename_));
+ ASSERT_NO_THROW(lf->open());
+
+ // Load leases from the file.
+ ASSERT_NO_THROW(LeaseFileLoader::load<Lease6>(*lf, storage6_, 10));
+
+ {
+ SCOPED_TRACE("Read leases");
+ checkStats(*lf, 2, 1, 0, 0, 0, 0);
+ }
+
+ // Check how many leases were actually loaded.
+ ASSERT_EQ( (exp_present ? 1 : 0), storage6_.size());
+
+ Lease6Ptr l = getLease<Lease6Ptr>(lease, storage6_);
+
+ if (exp_present) {
+ ASSERT_TRUE(l) << "lease not found, but expected";
+ EXPECT_EQ(exp_id, l->subnet_id_);
+ } else {
+ EXPECT_FALSE(l) << "lease found, but was not expected";
+ }
+ }
+
+protected:
+ /// @brief Sets up the header strings
+ virtual void SetUp() {
+ v4_hdr_ = "address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname,state,user_context\n";
+
+ v6_hdr_ = "address,duid,valid_lifetime,expire,subnet_id,"
+ "pref_lifetime,lease_type,iaid,prefix_len,fqdn_fwd,"
+ "fqdn_rev,hostname,hwaddr,state,user_context,"
+ "hwtype,hwaddr_source\n";
+ }
+};
+
+LeaseFileLoaderTest::LeaseFileLoaderTest()
+ : filename_(absolutePath("leases.csv")), io_(filename_) {
+ CfgMgr::instance().clear();
+}
+
+LeaseFileLoaderTest::~LeaseFileLoaderTest() {
+ CfgMgr::instance().clear();
+}
+
+std::string
+LeaseFileLoaderTest::absolutePath(const std::string& filename) {
+ std::ostringstream s;
+ s << DHCP_DATA_DIR << "/" << filename;
+ return (s.str());
+}
+
+// This test verifies that the DHCPv4 leases can be loaded from the lease
+// file and that only the most recent entry for each lease is loaded and
+// the previous entries are discarded.
+//
+// It also tests the write function by writing the storage to a file
+// and comparing that with the expected value.
+TEST_F(LeaseFileLoaderTest, loadWrite4) {
+ std::string test_str;
+ std::string a_1 = "192.0.2.1,06:07:08:09:0a:bc,,"
+ "200,200,8,1,1,host.example.com,1,"
+ "{ \"foobar\": true }\n";
+ std::string a_2 = "192.0.2.1,06:07:08:09:0a:bc,,"
+ "200,500,8,1,1,host.example.com,1,"
+ "{ \"foobar\": true }\n";
+
+ std::string b_1 = "192.0.3.15,dd:de:ba:0d:1b:2e:3e:4f,0a:00:01:04,"
+ "100,100,7,0,0,,1,\n";
+ std::string b_2 = "192.0.3.15,dd:de:ba:0d:1b:2e:3e:4f,0a:00:01:04,"
+ "100,135,7,0,0,,1,\n";
+
+ std::string c_1 = "192.0.2.3,,,"
+ "200,200,8,1,1,host.example.com,0,\n";
+
+ // Create lease file with leases for 192.0.2.1, 192.0.3.15. The lease
+ // entry for the 192.0.2.3 is invalid (lacks HW address and client id)
+ // and should be discarded.
+ test_str = v4_hdr_ + a_1 + b_1 + c_1 + b_2 + a_2;
+ io_.writeFile(test_str);
+
+ boost::scoped_ptr<CSVLeaseFile4> lf(new CSVLeaseFile4(filename_));
+ ASSERT_NO_THROW(lf->open());
+
+ // Load leases from the file.
+ Lease4Storage storage;
+ ASSERT_NO_THROW(LeaseFileLoader::load<Lease4>(*lf, storage, 10));
+
+ // We should have made 6 attempts to read, with 4 leases read and 1 error
+ {
+ SCOPED_TRACE("Read leases");
+ checkStats(*lf, 6, 4, 1, 0, 0, 0);
+ }
+
+ // There are two unique leases.
+ ASSERT_EQ(2, storage.size());
+
+ // The lease for 192.0.2.1 should exist and the cltt should be
+ // set to the expire-valid_lifetime for the second entry for
+ // this lease, i.e. 500 - 200 = 300.
+ Lease4Ptr lease = getLease<Lease4Ptr>("192.0.2.1", storage);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(300, lease->cltt_);
+
+ // The lease for 192.0.2.1 should have user context.
+ ASSERT_TRUE(lease->getContext());
+ EXPECT_EQ("{ \"foobar\": true }", lease->getContext()->str());
+
+ // The invalid entry should not be loaded.
+ lease = getLease<Lease4Ptr>("192.0.2.3", storage);
+ ASSERT_FALSE(lease);
+
+ // The other lease should be present and the cltt time should
+ // be set according to the values in the second entry for this
+ // lease.
+ lease = getLease<Lease4Ptr>("192.0.3.15", storage);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(35, lease->cltt_);
+ EXPECT_FALSE(lease->getContext());
+
+ test_str = v4_hdr_ + a_2 + b_2;
+ writeLeases<Lease4, CSVLeaseFile4, Lease4Storage>(*lf, storage, test_str);
+
+ // We should have made 2 attempts to write, with 2 leases written and 0 errors
+ {
+ SCOPED_TRACE("Write leases");
+ checkStats(*lf, 0, 0, 0, 2, 2, 0);
+ }
+}
+
+// This test verifies that the lease with a valid lifetime of 0
+// is removed from the storage. The valid lifetime of 0 is set
+// for the released leases.
+//
+// It also tests the write function by writing the storage to a file
+// and comparing that with the expected value.
+TEST_F(LeaseFileLoaderTest, loadWrite4LeaseRemove) {
+ std::string test_str;
+ std::string a_1 = "192.0.2.1,06:07:08:09:0a:bc,,"
+ "200,200,8,1,1,host.example.com,1,\n";
+ std::string a_2 = "192.0.2.1,06:07:08:09:0a:bc,,"
+ "0,500,8,1,1,host.example.com,1,\n";
+
+ std::string b_1 = "192.0.3.15,dd:de:ba:0d:1b:2e:3e:4f,0a:00:01:04,"
+ "100,100,7,0,0,,1,\n";
+ std::string b_2 = "192.0.3.15,dd:de:ba:0d:1b:2e:3e:4f,0a:00:01:04,"
+ "100,135,7,0,0,,1,\n";
+
+
+ // Create lease file in which one of the entries for 192.0.2.1
+ // has a valid_lifetime of 0 and results in the deletion of the
+ // lease.
+ test_str = v4_hdr_ + a_1 + b_1 + b_2 + a_2;
+ io_.writeFile(test_str);
+
+ boost::scoped_ptr<CSVLeaseFile4> lf(new CSVLeaseFile4(filename_));
+ ASSERT_NO_THROW(lf->open());
+
+ Lease4Storage storage;
+ ASSERT_NO_THROW(LeaseFileLoader::load<Lease4>(*lf, storage, 10));
+
+ // We should have made 5 attempts to read, with 4 leases read and 0 error
+ {
+ SCOPED_TRACE("Read leases");
+ checkStats(*lf, 5, 4, 0, 0, 0, 0);
+ }
+
+ // There should only be one lease. The one with the valid_lifetime
+ // of 0 should be removed.
+ ASSERT_EQ(1, storage.size());
+
+ Lease4Ptr lease = getLease<Lease4Ptr>("192.0.3.15", storage);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(35, lease->cltt_);
+
+ test_str = v4_hdr_ + b_2;
+ writeLeases<Lease4, CSVLeaseFile4, Lease4Storage>(*lf, storage, test_str);
+
+ // We should have made 1 attempts to write, with 1 leases written and 0 errors
+ {
+ SCOPED_TRACE("Write leases");
+ checkStats(*lf, 0, 0, 0, 1, 1, 0);
+ }
+}
+
+// This test verifies that max-row-errors works correctly for
+// DHCPv4 lease files
+TEST_F(LeaseFileLoaderTest, maxRowErrors4) {
+ // We have 9 rows: 2 that are good, 7 that are flawed (too few fields).
+ std::vector<std::string> rows = {
+ "192.0.2.100,08:00:27:25:d3:f4,31:31:31:31,3600,1565356064,1,0,0,,0,\n",
+ "192.0.2.101,FF:FF:FF:FF:FF:01,32:32:32:31,3600,1565356073,1,0,0\n",
+ "192.0.2.102,FF:FF:FF:FF:FF:02,32:32:32:32,3600,1565356073,1,0,0\n",
+ "192.0.2.103,FF:FF:FF:FF:FF:03,32:32:32:33,3600,1565356073,1,0,0\n",
+ "192.0.2.104,FF:FF:FF:FF:FF:04,32:32:32:34,3600,1565356073,1,0,0\n",
+ "192.0.2.105,FF:FF:FF:FF:FF:05,32:32:32:35,3600,1565356073,1,0,0\n",
+ "192.0.2.106,FF:FF:FF:FF:FF:06,32:32:32:36,3600,1565356073,1,0,0\n",
+ "192.0.2.107,FF:FF:FF:FF:FF:07,32:32:32:37,3600,1565356073,1,0,0\n",
+ "192.0.2.108,08:00:27:25:d3:f4,32:32:32:32,3600,1565356073,1,0,0,,0,\n"
+ };
+
+ std::ostringstream os;
+ os << v4_hdr_;
+ for (auto row : rows) {
+ os << row;
+ }
+
+ io_.writeFile(os.str());
+
+ boost::scoped_ptr<CSVLeaseFile4> lf(new CSVLeaseFile4(filename_));
+ ASSERT_NO_THROW(lf->open());
+
+ // Let's limit the number of errors to 5 (we have 7 in the data) and
+ // try to load the leases.
+ uint32_t max_errors = 5;
+ Lease4Storage storage;
+ ASSERT_THROW(LeaseFileLoader::load<Lease4>(*lf, storage, max_errors), util::CSVFileError);
+
+ // We should have made 7 reads, with 1 lease read, and 6 errors.
+ {
+ SCOPED_TRACE("Failed load stats");
+ checkStats(*lf, 7, 1, 6, 0, 0, 0);
+ }
+
+ // Now let's disable the error limit and try again.
+ max_errors = 0;
+
+ // Load leases from the file. Note, we have to reopen the file.
+ ASSERT_NO_THROW(lf->open());
+ ASSERT_NO_THROW(LeaseFileLoader::load<Lease4>(*lf, storage, max_errors));
+
+ // We should have made 10 reads, with 2 leases read, and 7 errors.
+ {
+ SCOPED_TRACE("Good load stats");
+ checkStats(*lf, 10, 2, 7, 0, 0, 0);
+ }
+}
+
+// This test verifies that the DHCPv6 leases can be loaded from the lease
+// file and that only the most recent entry for each lease is loaded and
+// the previous entries are discarded.
+//
+// It also tests the write function by writing the storage to a file
+// and comparing that with the expected value.
+TEST_F(LeaseFileLoaderTest, loadWrite6) {
+ std::string test_str;
+ std::string a_1 = "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,"
+ "200,200,8,100,0,7,0,1,1,host.example.com,,1,"
+ "{ \"foobar\": true },,\n";
+ std::string a_2 = "2001:db8:1::1,,"
+ "200,200,8,100,0,7,0,1,1,host.example.com,,1,"
+ "{ \"foobar\": true },,\n";
+ std::string a_3 = "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,"
+ "200,400,8,100,0,7,0,1,1,host.example.com,,1,"
+ "{ \"foobar\": true },,\n";
+ std::string b_1 = "2001:db8:2::10,01:01:01:01:0a:01:02:03:04:05,"
+ "300,300,6,150,0,8,0,0,0,,,1,,,\n";
+ std::string b_2 = "2001:db8:2::10,01:01:01:01:0a:01:02:03:04:05,"
+ "300,800,6,150,0,8,0,0,0,,,1,,,\n";
+
+ std::string c_1 = "3000:1::,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,"
+ "100,200,8,0,2,16,64,0,0,,,1,,,\n";
+
+
+ // Create a lease file with three valid leases: 2001:db8:1::1,
+ // 3000:1:: and 2001:db8:2::10.
+ test_str = v6_hdr_ + a_1 + a_2 + b_1 + c_1 + b_2 + a_3;
+ io_.writeFile(test_str);
+
+ boost::scoped_ptr<CSVLeaseFile6> lf(new CSVLeaseFile6(filename_));
+ ASSERT_NO_THROW(lf->open());
+
+ // Load leases from the lease file.
+ Lease6Storage storage;
+ ASSERT_NO_THROW(LeaseFileLoader::load<Lease6>(*lf, storage, 10));
+
+ // We should have made 7 attempts to read, with 5 leases read and 1 error
+ {
+ SCOPED_TRACE("Read leases");
+ checkStats(*lf, 7, 5, 1, 0, 0, 0);
+ }
+
+ // There should be 3 unique leases.
+ ASSERT_EQ(3, storage.size());
+
+ // The 2001:db8:1::1 should be present and its cltt should be
+ // calculated according to the expiration time and the valid
+ // lifetime from the last entry for this lease: 400 - 200 = 200.
+ Lease6Ptr lease = getLease<Lease6Ptr>("2001:db8:1::1", storage);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(200, lease->cltt_);
+
+ // The 2001:db8:1::1 should have user context.
+ ASSERT_TRUE(lease->getContext());
+ EXPECT_EQ("{ \"foobar\": true }", lease->getContext()->str());
+
+ // The 3000:1:: lease should be present.
+ lease = getLease<Lease6Ptr>("3000:1::", storage);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(100, lease->cltt_);
+ EXPECT_FALSE(lease->getContext());
+
+ // The 2001:db8:2::10 should be present and the cltt should be
+ // calculated according to the last entry in the lease file.
+ lease = getLease<Lease6Ptr>("2001:db8:2::10", storage);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(500, lease->cltt_);
+ EXPECT_FALSE(lease->getContext());
+
+ test_str = v6_hdr_ + a_3 + b_2 + c_1;
+ writeLeases<Lease6, CSVLeaseFile6, Lease6Storage>(*lf, storage, test_str);
+
+ // We should have made 3 attempts to write, with 3 leases written and 0 errors
+ {
+ SCOPED_TRACE("Write leases");
+ checkStats(*lf, 0, 0, 0, 3, 3, 0);
+ }
+}
+
+// This test verifies that the lease with a valid lifetime of 0
+// is removed from the storage. The valid lifetime of 0 set set
+// for the released leases.
+//
+// It also tests the write function by writing the storage to a file
+// and comparing that with the expected value.
+TEST_F(LeaseFileLoaderTest, loadWrite6LeaseRemove) {
+ std::string test_str;
+ std::string a_1 = "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,"
+ "200,200,8,100,0,7,0,1,1,host.example.com,,1,,,\n";
+ std::string a_2 = "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,"
+ "0,400,8,100,0,7,0,1,1,host.example.com,,1,,,\n";
+
+ std::string b_1 = "2001:db8:2::10,01:01:01:01:0a:01:02:03:04:05,"
+ "300,300,6,150,0,8,0,0,0,,,1,,,\n";
+ std::string b_2 = "2001:db8:2::10,01:01:01:01:0a:01:02:03:04:05,"
+ "300,800,6,150,0,8,0,0,0,,,1,,,\n";
+
+ // Create lease file in which one of the entries for the 2001:db8:1::1
+ // has valid lifetime set to 0, in which case the lease should be
+ // deleted.
+ test_str = v6_hdr_ + a_1 + b_1 + b_2 + a_2;
+ io_.writeFile(test_str);
+
+ boost::scoped_ptr<CSVLeaseFile6> lf(new CSVLeaseFile6(filename_));
+ ASSERT_NO_THROW(lf->open());
+
+ // Loaded leases.
+ Lease6Storage storage;
+ ASSERT_NO_THROW(LeaseFileLoader::load<Lease6>(*lf, storage, 10));
+
+ // We should have made 5 attempts to read, with 4 leases read and 0 error
+ {
+ SCOPED_TRACE("Read leases");
+ checkStats(*lf, 5, 4, 0, 0, 0, 0);
+ }
+
+ // There should be only one lease for 2001:db8:2::10. The other one
+ // should have been deleted (or rather not loaded).
+ ASSERT_EQ(1, storage.size());
+
+ Lease6Ptr lease = getLease<Lease6Ptr>("2001:db8:2::10", storage);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(500, lease->cltt_);
+
+ test_str = v6_hdr_ + b_2;
+ writeLeases<Lease6, CSVLeaseFile6, Lease6Storage>(*lf, storage, test_str);
+
+ // We should have made 1 attempts to write, with 1 leases written and 0 errors
+ {
+ SCOPED_TRACE("Write leases");
+ checkStats(*lf, 0, 0, 0, 1, 1, 0);
+ }
+}
+
+// This test verifies that max-row-errors works correctly for
+// DHCPv6 lease files
+TEST_F(LeaseFileLoaderTest, maxRowErrors6) {
+ // We have 9 rows: 2 that are good, 7 that are flawed (too few fields).
+ std::vector<std::string> rows = {
+ "3002::01,00:03:00:01:08:00:27:25:d3:01,30,1565361388,2,20,0,"
+ "11189196,128,0,0,,08:00:27:25:d3:f4,0,\n",
+ "3002::02,00:03:00:01:08:00:27:25:d3:02,30,1565361388,2,20,0\n",
+ "3002::03,00:03:00:01:08:00:27:25:d3:03,30,1565361388,2,20,0\n",
+ "3002::04,00:03:00:01:08:00:27:25:d3:04,30,1565361388,2,20,0\n",
+ "3002::05,00:03:00:01:08:00:27:25:d3:05,30,1565361388,2,20,0\n",
+ "3002::06,00:03:00:01:08:00:27:25:d3:06,30,1565361388,2,20,0\n",
+ "3002::07,00:03:00:01:08:00:27:25:d3:07,30,1565361388,2,20,0\n",
+ "3002::08,00:03:00:01:08:00:27:25:d3:08,30,1565361388,2,20,0\n",
+ "3002::09,00:03:00:01:08:00:27:25:d3:09,30,1565361388,2,20,0,"
+ "11189196,128,0,0,,08:00:27:25:d3:f4,0,\n"
+ };
+
+ std::ostringstream os;
+ os << v6_hdr_;
+ for (auto row : rows) {
+ os << row;
+ }
+
+ io_.writeFile(os.str());
+
+ boost::scoped_ptr<CSVLeaseFile6> lf(new CSVLeaseFile6(filename_));
+ ASSERT_NO_THROW(lf->open());
+
+ // Let's limit the number of errors to 5 (we have 7 in the data) and
+ // try to load the leases.
+ uint32_t max_errors = 5;
+ Lease6Storage storage;
+ ASSERT_THROW(LeaseFileLoader::load<Lease6>(*lf, storage, max_errors), util::CSVFileError);
+
+ // We should have made 7 reads, with 1 lease read, and 6 errors.
+ {
+ SCOPED_TRACE("Failed load stats");
+ checkStats(*lf, 7, 1, 6, 0, 0, 0);
+ }
+
+ // Now let's disable the error limit and try again.
+ max_errors = 0;
+
+ // Load leases from the file. Note, we have to reopen the file.
+ ASSERT_NO_THROW(lf->open());
+ ASSERT_NO_THROW(LeaseFileLoader::load<Lease6>(*lf, storage, max_errors));
+
+ // We should have made 10 reads, with 2 leases read, and 7 errors.
+ {
+ SCOPED_TRACE("Good load stats");
+ checkStats(*lf, 10, 2, 7, 0, 0, 0);
+ }
+}
+
+// This test verifies that the lease with a valid lifetime set to 0 is
+// not loaded if there are no previous entries for this lease in the
+// lease file.
+//
+// It also tests the write function by writing the storage to a file
+// and comparing that with the expected value.
+TEST_F(LeaseFileLoaderTest, loadWriteLeaseWithZeroLifetime) {
+ std::string test_str;
+ std::string a_1 = "192.0.2.1,06:07:08:09:0a:bc,,200,200,8,1,1,,1,\n";
+ std::string b_2 = "192.0.2.3,06:07:08:09:0a:bd,,0,200,8,1,1,,1,\n";
+
+ // Create lease file. The second lease has a valid lifetime of 0.
+ test_str = v4_hdr_ + a_1 + b_2;
+ io_.writeFile(test_str);
+
+ boost::scoped_ptr<CSVLeaseFile4> lf(new CSVLeaseFile4(filename_));
+ ASSERT_NO_THROW(lf->open());
+
+ // Set the error count to 0 to make sure that lease with a zero
+ // lifetime doesn't cause an error.
+ Lease4Storage storage;
+ ASSERT_NO_THROW(LeaseFileLoader::load<Lease4>(*lf, storage, 0));
+
+ // We should have made 3 attempts to read, with 2 leases read and 0 error
+ {
+ SCOPED_TRACE("Read leases");
+ checkStats(*lf, 3, 2, 0, 0, 0, 0);
+ }
+
+ // The first lease should be present.
+ Lease4Ptr lease = getLease<Lease4Ptr>("192.0.2.1", storage);
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+
+ // The lease with a valid lifetime of 0 should not be loaded.
+ EXPECT_FALSE(getLease<Lease4Ptr>("192.0.2.3", storage));
+
+ test_str = v4_hdr_ + a_1;
+ writeLeases<Lease4, CSVLeaseFile4, Lease4Storage>(*lf, storage, test_str);
+
+ // We should have made 1 attempts to write, with 1 leases written and 0 errors
+ {
+ SCOPED_TRACE("Write leases");
+ checkStats(*lf, 0, 0, 0, 1, 1, 0);
+ }
+}
+
+// This test checks if the lease can be loaded, even though there are no
+// subnets configured that it would match.
+// Scenario: print a warning, there's no subnet,
+// expected outcome: add the lease as is
+TEST_F(LeaseFileLoaderTest, sanityChecker4NoSubnetsWarn) {
+ sanityChecks4("192.0.2.1", 1, "", 0, CfgConsistency::LEASE_CHECK_WARN, true, 1);
+}
+
+// This test checks if the lease can be fixed.
+// Scenario: try to fix the lease, there's no subnet,
+// expected outcome: add the lease as is
+TEST_F(LeaseFileLoaderTest, sanityChecker4NoSubnetsFix) {
+ sanityChecks4("192.0.2.1", 1, "", 0, CfgConsistency::LEASE_CHECK_FIX, true, 1);
+}
+
+// This test checks if the lease can be discarded if it's impossible to fix.
+// Scenario: try to fix the lease, there's no subnet,
+// expected outcome: the lease is not loaded
+TEST_F(LeaseFileLoaderTest, sanityChecker4NoSubnetsFixDel) {
+ sanityChecks4("192.0.2.1", 1, "", 0, CfgConsistency::LEASE_CHECK_FIX_DEL, false, 1);
+}
+
+// This test checks if the lease can be discarded.
+// Scenario: try to fix the lease, there's no subnet,
+// expected outcome: the lease is not loaded
+TEST_F(LeaseFileLoaderTest, sanityChecker4NoSubnetsDel) {
+ sanityChecks4("192.0.2.1", 1, "", 0, CfgConsistency::LEASE_CHECK_DEL, false, 1);
+}
+
+// This test checks if the lease can be fixed.
+// Scenario: try to fix the lease, there's a subnet,
+// expected outcome: correct the lease
+TEST_F(LeaseFileLoaderTest, sanityChecker4Fix) {
+ sanityChecks4("192.0.2.1", 1, "192.0.2.0/24", 2, CfgConsistency::LEASE_CHECK_FIX, true, 2);
+}
+
+// This test checks if the lease can be fixed when it's possible.
+// Scenario: try to fix the lease, there's a subnet,
+// expected outcome: the lease is not loaded
+TEST_F(LeaseFileLoaderTest, sanityChecker4FixDel1) {
+ sanityChecks4("192.0.2.1", 1, "192.0.2.0/24", 2, CfgConsistency::LEASE_CHECK_FIX_DEL, true, 2);
+}
+
+// This test checks if the lease is discarded, when fix is not possible.
+// Scenario: try to fix the lease, there's a subnet, but it doesn't match,
+// expected outcome: the lease is not loaded
+TEST_F(LeaseFileLoaderTest, sanityChecker4FixDel2) {
+ sanityChecks4("192.0.2.1", 1, "192.0.3.0/24", 2, CfgConsistency::LEASE_CHECK_FIX_DEL, false, 1);
+}
+
+// This test checks if the lease is discarded.
+// Scenario: delete the lease, there's a subnet,
+// expected outcome: the lease is not loaded
+TEST_F(LeaseFileLoaderTest, sanityChecker4Del) {
+ sanityChecks4("192.0.2.1", 1, "192.0.2.0/24", 2, CfgConsistency::LEASE_CHECK_DEL, false, 1);
+}
+
+// This test checks if the lease can be loaded, even though there are no
+// subnets configured that it would match.
+TEST_F(LeaseFileLoaderTest, sanityChecker6NoSubnetsWarn) {
+ sanityChecks6("2001::1", 1, "", 0, CfgConsistency::LEASE_CHECK_WARN, true, 1);
+}
+
+// This test checks if the lease can be fixed.
+// Scenario: try to fix the lease, there's no subnet,
+// expected outcome: add the lease as is
+TEST_F(LeaseFileLoaderTest, sanityChecker6NoSubnetsFix) {
+ sanityChecks6("2001::1", 1, "", 0, CfgConsistency::LEASE_CHECK_FIX, true, 1);
+}
+
+// This test checks if the lease can be discarded if it's impossible to fix.
+// Scenario: try to fix the lease, there's no subnet,
+// expected outcome: the lease is not loaded
+TEST_F(LeaseFileLoaderTest, sanityChecker6NoSubnetsFixDel) {
+ sanityChecks6("2001::1", 1, "", 0, CfgConsistency::LEASE_CHECK_FIX_DEL, false, 1);
+}
+
+// This test checks if the lease can be discarded. Note we are
+// verifying whether the checks are conducted at all when loading a file.
+// Thorough tests were conducted in sanity_checks_unittest.cc.
+TEST_F(LeaseFileLoaderTest, sanityChecker6NoSubnetsDel) {
+ sanityChecks6("2001::1", 1, "", 0, CfgConsistency::LEASE_CHECK_DEL, false, 1);
+}
+
+// This test checks if the lease can be fixed.
+// Scenario: try to fix the lease, there's a subnet,
+// expected outcome: correct the lease
+TEST_F(LeaseFileLoaderTest, sanityChecker6Fix) {
+ sanityChecks6("2001::1", 1, "2001::/16", 2, CfgConsistency::LEASE_CHECK_FIX, true, 2);
+}
+
+// This test checks if the lease can be fixed when it's possible.
+// Scenario: try to fix the lease, there's a subnet,
+// expected outcome: the lease is not loaded
+TEST_F(LeaseFileLoaderTest, sanityChecker6FixDel1) {
+ sanityChecks6("2001::1", 1, "2001::/16", 2, CfgConsistency::LEASE_CHECK_FIX_DEL, true, 2);
+}
+
+// This test checks if the lease is discarded, when fix is not possible.
+// Scenario: try to fix the lease, there's a subnet, but it doesn't match,
+// expected outcome: the lease is not loaded
+TEST_F(LeaseFileLoaderTest, sanityChecker6FixDel2) {
+ sanityChecks6("2001::1", 1, "2002::/16", 2, CfgConsistency::LEASE_CHECK_FIX_DEL, false, 1);
+}
+
+// This test checks if the lease is discarded.
+// Scenario: delete the lease, there's a subnet,
+// expected outcome: the lease is not loaded
+TEST_F(LeaseFileLoaderTest, sanityChecker6Del) {
+ sanityChecks6("2001::1", 1, "2001::/16", 2, CfgConsistency::LEASE_CHECK_DEL, false, 1);
+}
+
+// This test checks to make sure PD leases are not sanity checked,
+// and thus not discarded.
+TEST_F(LeaseFileLoaderTest, sanityChecker6PD) {
+ int prefix_len = 64;
+
+ // We check a prefix lease whose subnet-id does not exist and
+ // is clearly outside the only known subnet.
+ sanityChecks6("2001:1::", 2, // create prefix lease in subnet 2
+ "3001::/64", 1, // create subnet 1
+ CfgConsistency::LEASE_CHECK_DEL,
+ true, 2, // lease should still exist with subnet id of 2
+ prefix_len);
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/lease_mgr_factory_unittest.cc b/src/lib/dhcpsrv/tests/lease_mgr_factory_unittest.cc
new file mode 100644
index 0000000..5e8b999
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/lease_mgr_factory_unittest.cc
@@ -0,0 +1,31 @@
+// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <exceptions/exceptions.h>
+
+#include <gtest/gtest.h>
+
+#include <iostream>
+#include <sstream>
+
+using namespace std;
+using namespace isc::dhcp;
+
+// This set of tests only check the parsing functions of LeaseMgrFactory.
+// Tests of the LeaseMgr create/instance/destroy are implicitly carried out
+// in the tests for the different concrete lease managers (e.g. MySqlLeaseMgr).
+
+// Currently there are no unit-tests as the sole testable method (parse)
+// was moved to its own class (DataSource). All existing unit-tests were
+// moved there.
+
+namespace {
+
+}; // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/lease_mgr_unittest.cc b/src/lib/dhcpsrv/tests/lease_mgr_unittest.cc
new file mode 100644
index 0000000..00675ec
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/lease_mgr_unittest.cc
@@ -0,0 +1,545 @@
+// Copyright (C) 2012-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/memfile_lease_mgr.h>
+#include <dhcpsrv/testutils/test_utils.h>
+#include <dhcpsrv/tests/generic_lease_mgr_unittest.h>
+
+#include <gtest/gtest.h>
+
+#include <iostream>
+#include <sstream>
+
+#include <time.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::db;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+
+// This is a concrete implementation of a Lease database. It does not do
+// anything useful and is used for abstract LeaseMgr class testing.
+class ConcreteLeaseMgr : public LeaseMgr {
+public:
+
+ /// @brief The sole lease manager constructor
+ ///
+ /// dbconfig is a generic way of passing parameters. Parameters
+ /// are passed in the "name=value" format, separated by spaces.
+ /// Values may be enclosed in double quotes, if needed.
+ ConcreteLeaseMgr(const DatabaseConnection::ParameterMap&)
+ : LeaseMgr()
+ {}
+
+ /// @brief Destructor
+ virtual ~ConcreteLeaseMgr()
+ {}
+
+ /// @brief Adds an IPv4 lease.
+ ///
+ /// @param lease lease to be added
+ virtual bool addLease(const Lease4Ptr&) override {
+ return (false);
+ }
+
+ /// @brief Adds an IPv6 lease.
+ ///
+ /// @param lease lease to be added
+ virtual bool addLease(const Lease6Ptr&) override {
+ return (false);
+ }
+
+ /// @brief Returns existing IPv4 lease for specified IPv4 address.
+ ///
+ /// @param addr address of the searched lease
+ ///
+ /// @return smart pointer to the lease (or NULL if a lease is not found)
+ virtual Lease4Ptr getLease4(const isc::asiolink::IOAddress&) const override {
+ return (Lease4Ptr());
+ }
+
+ /// @brief Returns existing IPv4 leases for specified hardware address.
+ ///
+ /// Although in the usual case there will be only one lease, for mobile
+ /// clients or clients with multiple static/fixed/reserved leases there
+ /// can be more than one. Thus return type is a container, not a single
+ /// pointer.
+ ///
+ /// @param hwaddr hardware address of the client
+ ///
+ /// @return lease collection
+ virtual Lease4Collection getLease4(const HWAddr&) const override {
+ return (Lease4Collection());
+ }
+
+ /// @brief Returns existing IPv4 leases for specified hardware address
+ /// and a subnet
+ ///
+ /// There can be at most one lease for a given HW address in a single
+ /// pool, so this method with either return a single lease or NULL.
+ ///
+ /// @param hwaddr hardware address of the client
+ /// @param subnet_id identifier of the subnet that lease must belong to
+ ///
+ /// @return a pointer to the lease (or NULL if a lease is not found)
+ virtual Lease4Ptr getLease4(const HWAddr&, SubnetID) const override {
+ return (Lease4Ptr());
+ }
+
+ /// @brief Returns existing IPv4 lease for specified client-id
+ ///
+ /// @param clientid client identifier
+ ///
+ /// @return lease collection
+ virtual Lease4Collection getLease4(const ClientId&) const override {
+ return (Lease4Collection());
+ }
+
+ /// @brief Returns existing IPv4 lease for specified client-id
+ ///
+ /// There can be at most one lease for a given HW address in a single
+ /// pool, so this method with either return a single lease or NULL.
+ ///
+ /// @param clientid client identifier
+ /// @param subnet_id identifier of the subnet that lease must belong to
+ ///
+ /// @return a pointer to the lease (or NULL if a lease is not found)
+ virtual Lease4Ptr getLease4(const ClientId&, SubnetID) const override {
+ return (Lease4Ptr());
+ }
+
+ /// @brief Returns all IPv4 leases for the particular subnet identifier.
+ ///
+ /// @param subnet_id subnet identifier.
+ ///
+ /// @return Lease collection (may be empty if no IPv4 lease found).
+ virtual Lease4Collection getLeases4(SubnetID) const override {
+ return (Lease4Collection());
+ }
+
+ /// @brief Returns all IPv4 leases for the particular hostname.
+ ///
+ /// @param hostname hostname in lower case.
+ ///
+ /// @return Lease collection (may be empty if no IPv4 lease found).
+ virtual Lease4Collection getLeases4(const std::string&) const override {
+ return (Lease4Collection());
+ }
+
+ /// @brief Returns all IPv4 leases.
+ ///
+ /// @return Lease collection (may be empty if no IPv4 lease found).
+ virtual Lease4Collection getLeases4() const override {
+ return (Lease4Collection());
+ }
+
+ /// @brief Returns range of IPv4 leases using paging.
+ ///
+ /// This method implements paged browsing of the lease database. The first
+ /// parameter specifies a page size. The second parameter is optional and
+ /// specifies the starting address of the range. This address is excluded
+ /// from the returned range. The IPv4 zero address (default) denotes that
+ /// the first page should be returned. There is no guarantee about the
+ /// order of returned leases.
+ ///
+ /// The typical usage of this method is as follows:
+ /// - Get the first page of leases by specifying IPv4 zero address as the
+ /// beginning of the range.
+ /// - Last address of the returned range should be used as a starting
+ /// address for the next page in the subsequent call.
+ /// - If the number of leases returned is lower than the page size, it
+ /// indicates that the last page has been retrieved.
+ /// - If there are no leases returned it indicates that the previous page
+ /// was the last page.
+ ///
+ /// @param lower_bound_address IPv4 address used as lower bound for the
+ /// returned range.
+ /// @param page_size maximum size of the page returned.
+ ///
+ /// @return Lease collection (may be empty if no IPv4 lease found).
+ virtual Lease4Collection
+ getLeases4(const asiolink::IOAddress& /* lower_bound_address */,
+ const LeasePageSize& /* page_size */) const override {
+ return (Lease4Collection());
+ }
+
+ /// @brief Returns existing IPv6 lease for a given IPv6 address.
+ ///
+ /// @param addr address of the searched lease
+ ///
+ /// @return smart pointer to the lease (or NULL if a lease is not found)
+ virtual Lease6Ptr getLease6(Lease::Type /* not used yet */,
+ const isc::asiolink::IOAddress&) const override {
+ return (Lease6Ptr());
+ }
+
+ /// @brief Returns existing IPv6 lease for a given DUID+IA combination
+ ///
+ /// @param duid ignored
+ /// @param iaid ignored
+ ///
+ /// @return whatever is set in leases6_ field
+ virtual Lease6Collection getLeases6(Lease::Type /* not used yet */,
+ const DUID&, uint32_t) const override {
+ return (leases6_);
+ }
+
+ /// @brief Returns existing IPv6 lease for a given DUID+IA+subnet-id combination
+ ///
+ /// @param duid ignored
+ /// @param iaid ignored
+ /// @param subnet_id ignored
+ ///
+ /// @return whatever is set in leases6_ field
+ virtual Lease6Collection getLeases6(Lease::Type /* not used yet */,
+ const DUID&, uint32_t, SubnetID) const override {
+ return (leases6_);
+ }
+
+ /// @brief Returns collection of lease for matching DUID
+ ///
+ /// @param duid ignored
+ /// @return whatever is set in leases6_ field
+ virtual Lease6Collection getLeases6(const DUID&) const override {
+ return (leases6_);
+ }
+
+ /// @brief Returns all IPv6 leases for the particular subnet identifier.
+ ///
+ /// @param subnet_id subnet identifier.
+ ///
+ /// @return Lease collection (may be empty if no IPv6 lease found).
+ virtual Lease6Collection getLeases6(SubnetID) const override {
+ return (Lease6Collection());
+ }
+
+ /// @brief Returns all IPv6 leases for the particular hostname.
+ ///
+ /// @param hostname hostname in lower case.
+ ///
+ /// @return Lease collection (may be empty if no IPv6 lease found).
+ virtual Lease6Collection getLeases6(const std::string&) const override {
+ return (Lease6Collection());
+ }
+
+ /// @brief Returns all IPv6 leases.
+ ///
+ /// @return Lease collection (may be empty if no IPv6 lease found).
+ virtual Lease6Collection getLeases6() const override {
+ return (Lease6Collection());
+ }
+
+ /// @brief Returns range of IPv6 leases using paging.
+ ///
+ /// This method implements paged browsing of the lease database. The first
+ /// parameter specifies a page size. The second parameter is optional and
+ /// specifies the starting address of the range. This address is excluded
+ /// from the returned range. The IPv6 zero address (default) denotes that
+ /// the first page should be returned. There is no guarantee about the
+ /// order of returned leases.
+ ///
+ /// The typical usage of this method is as follows:
+ /// - Get the first page of leases by specifying IPv6 zero address as the
+ /// beginning of the range.
+ /// - Last address of the returned range should be used as a starting
+ /// address for the next page in the subsequent call.
+ /// - If the number of leases returned is lower than the page size, it
+ /// indicates that the last page has been retrieved.
+ /// - If there are no leases returned it indicates that the previous page
+ /// was the last page.
+ ///
+ /// @param lower_bound_address IPv4 address used as lower bound for the
+ /// returned range.
+ /// @param page_size maximum size of the page returned.
+ ///
+ /// @return Lease collection (may be empty if no IPv6 lease found).
+ virtual Lease6Collection
+ getLeases6(const asiolink::IOAddress& /* lower_bound_address */,
+ const LeasePageSize& /* page_size */) const override {
+ return (Lease6Collection());
+ };
+
+ /// @brief Returns expired DHCPv6 leases.
+ ///
+ /// This method is not implemented.
+ virtual void getExpiredLeases6(Lease6Collection&, const size_t) const override {
+ isc_throw(NotImplemented, "ConcreteLeaseMgr::getExpiredLeases6 is not"
+ " implemented");
+ }
+
+ /// @brief Returns expired DHCPv4 leases.
+ ///
+ /// This method is not implemented.
+ virtual void getExpiredLeases4(Lease4Collection&, const size_t) const override {
+ isc_throw(NotImplemented, "ConcreteLeaseMgr::getExpiredLeases4 is not"
+ " implemented");
+ }
+
+ /// @brief Updates IPv4 lease.
+ ///
+ /// @param lease4 The lease to be updated.
+ ///
+ /// If no such lease is present, an exception will be thrown.
+ virtual void updateLease4(const Lease4Ptr&) override {}
+
+ /// @brief Updates IPv4 lease.
+ ///
+ /// @param lease4 The lease to be updated.
+ ///
+ /// If no such lease is present, an exception will be thrown.
+ virtual void updateLease6(const Lease6Ptr&) override {}
+
+ /// @brief Deletes an IPv4 lease.
+ ///
+ /// @param lease IPv4 lease to be deleted.
+ ///
+ /// @return true if deletion was successful, false if no such lease exists.
+ virtual bool deleteLease(const Lease4Ptr&) override {
+ return (false);
+ }
+
+ /// @brief Deletes an IPv6 lease.
+ ///
+ /// @param lease IPv6 lease to be deleted.
+ ///
+ /// @return true if deletion was successful, false if no such lease exists.
+ virtual bool deleteLease(const Lease6Ptr&) override {
+ return (false);
+ }
+
+ /// @brief Deletes all expired and reclaimed DHCPv4 leases.
+ ///
+ /// @param secs Number of seconds since expiration of leases before
+ /// they can be removed. Leases which have expired later than this
+ /// time will not be deleted.
+ virtual uint64_t deleteExpiredReclaimedLeases4(const uint32_t) override {
+ isc_throw(NotImplemented, "ConcreteLeaseMgr::deleteExpiredReclaimedLeases4"
+ " is not implemented");
+ }
+
+ /// @brief Deletes all expired and reclaimed DHCPv6 leases.
+ ///
+ /// @param secs Number of seconds since expiration of leases before
+ /// they can be removed. Leases which have expired later than this
+ /// time will not be deleted.
+ virtual uint64_t deleteExpiredReclaimedLeases6(const uint32_t) override {
+ isc_throw(NotImplemented, "ConcreteLeaseMgr::deleteExpiredReclaimedLeases6"
+ " is not implemented");
+ }
+
+ /// @brief Pretends to wipe all IPv4 leases from a subnet
+ /// @param subnet_id (ignored, but one day may specify the subnet)
+ virtual size_t wipeLeases4(const SubnetID&) override {
+ isc_throw(NotImplemented, "ConcreteLeaseMgr::wipeLeases4 not implemented");
+ }
+
+ /// @brief Pretends to wipe all IPv4 leases from a subnet
+ /// @param subnet_id (ignored, but one day may specify the subnet)
+ virtual size_t wipeLeases6(const SubnetID&) override {
+ isc_throw(NotImplemented, "ConcreteLeaseMgr::wipeLeases6 not implemented");
+ }
+
+ /// @brief Pretends to check if the IPv4 lease limits set in the given user
+ /// context are exceeded.
+ virtual std::string
+ checkLimits4(isc::data::ConstElementPtr const& /* user_context */) const override {
+ isc_throw(NotImplemented, "ConcreteLeaseMgr::checkLimits4() not implemented");
+ }
+
+ /// @brief Pretends to check if the IPv6 lease limits set in the given user
+ /// context are exceeded.
+ virtual std::string
+ checkLimits6(isc::data::ConstElementPtr const& /* user_context */) const override {
+ isc_throw(NotImplemented, "ConcreteLeaseMgr::checkLimits6() not implemented");
+ }
+
+ /// @brief Pretends to check if JSON support is enabled in the database.
+ ///
+ /// @return true if there is JSON support, false otherwise
+ virtual bool isJsonSupported() const override {
+ isc_throw(NotImplemented, "ConcreteLeaseMgr::isJsonSupported() not implemented");
+ }
+
+ /// @brief Pretends to return the class lease count for a given class and lease type.
+ ///
+ /// @param client_class client class for which the count is desired
+ /// @param ltype type of lease for which the count is desired. Defaults to
+ /// Lease::TYPE_V4.
+ ///
+ /// @return number of leases
+ virtual size_t getClassLeaseCount(const ClientClass& /* client_class */,
+ const Lease::Type& /* ltype = Lease::TYPE_V4 */) const override {
+ isc_throw(NotImplemented, "ConcreteLeaseMgr::getClassLeaseCount() not implemented");
+ }
+
+ /// @brief Pretends to recount the leases per class for V4 leases.
+ virtual void recountClassLeases4() override {
+ isc_throw(NotImplemented, "ConcreteLeaseMgr::recountClassLeases4() not implemented");
+ }
+
+ /// @brief Pretends to recount the leases per class for V6 leases.
+ virtual void recountClassLeases6() override {
+ isc_throw(NotImplemented, "ConcreteLeaseMgr::recountClassLeases6() not implemented");
+ }
+
+ /// @brief Pretends to clear the class-lease count map.
+ virtual void clearClassLeaseCounts() override {
+ isc_throw(NotImplemented, "ConcreteLeaseMgr::clearClassLeaseCounts() not implemented");
+ }
+
+ /// @brief Returns backend type.
+ ///
+ /// Returns the type of the backend (e.g. "mysql", "memfile" etc.)
+ ///
+ /// @return Type of the backend.
+ virtual std::string getType() const override {
+ return (std::string("concrete"));
+ }
+
+ /// @brief Returns backend name.
+ ///
+ /// If the backend is a database, this is the name of the database or the
+ /// file. Otherwise it is just the same as the type.
+ ///
+ /// @return Name of the backend.
+ virtual std::string getName() const override {
+ return (std::string("concrete"));
+ }
+
+ /// @brief Returns description of the backend.
+ ///
+ /// This description may be multiline text that describes the backend.
+ ///
+ /// @return Description of the backend.
+ virtual std::string getDescription() const override {
+ return (std::string("This is a dummy concrete backend implementation."));
+ }
+
+ /// @brief Returns backend version.
+ virtual std::pair<uint32_t, uint32_t> getVersion() const override {
+ return (make_pair(uint32_t(0), uint32_t(0)));
+ }
+
+ /// @brief Commit transactions
+ virtual void commit() override {
+ }
+
+ /// @brief Rollback transactions
+ virtual void rollback() override {
+ }
+
+ // We need to use it in ConcreteLeaseMgr
+ using LeaseMgr::getLease6;
+
+ Lease6Collection leases6_; ///< getLease6 methods return this as is
+};
+
+class LeaseMgrTest : public GenericLeaseMgrTest {
+public:
+ LeaseMgrTest() {
+ }
+
+ /// @brief Reopen the database
+ ///
+ /// No-op implementation. We need to provide concrete implementation,
+ /// as this is a pure virtual method in GenericLeaseMgrTest.
+ virtual void reopen(Universe) {
+ }
+
+};
+
+namespace {
+
+// This test checks if getLease6() method is working properly for 0 (NULL),
+// 1 (return the lease) and more than 1 leases (throw).
+TEST_F(LeaseMgrTest, getLease6) {
+
+ DatabaseConnection::ParameterMap pmap;
+ boost::scoped_ptr<ConcreteLeaseMgr> mgr(new ConcreteLeaseMgr(pmap));
+
+ vector<Lease6Ptr> leases = createLeases6();
+
+ mgr->leases6_.clear();
+ // For no leases, the function should return NULL pointer
+ Lease6Ptr lease;
+
+ // the getLease6() is calling getLeases6(), which is a dummy. It returns
+ // whatever is there in leases6_ field.
+ EXPECT_NO_THROW(lease = mgr->getLease6(leasetype6_[1], *leases[1]->duid_,
+ leases[1]->iaid_,
+ leases[1]->subnet_id_));
+ EXPECT_FALSE(lease);
+
+ // For a single lease, the function should return that lease
+ mgr->leases6_.push_back(leases[1]);
+ EXPECT_NO_THROW(lease = mgr->getLease6(leasetype6_[1], *leases[1]->duid_,
+ leases[1]->iaid_,
+ leases[1]->subnet_id_));
+ EXPECT_TRUE(lease);
+
+ EXPECT_NO_THROW(detailCompareLease(lease, leases[1]));
+
+ // Add one more lease. There are 2 now. It should throw
+ mgr->leases6_.push_back(leases[2]);
+
+ EXPECT_THROW(lease = mgr->getLease6(leasetype6_[1], *leases[1]->duid_,
+ leases[1]->iaid_,
+ leases[1]->subnet_id_),
+ MultipleRecords);
+}
+
+// Verify LeaseStatsQuery default construction
+TEST (LeaseStatsQueryTest, defaultCtor) {
+ LeaseStatsQueryPtr qry;
+
+ // Valid construction, verifiy member values.
+ ASSERT_NO_THROW(qry.reset(new LeaseStatsQuery()));
+ ASSERT_EQ(0, qry->getFirstSubnetID());
+ ASSERT_EQ(0, qry->getLastSubnetID());
+ ASSERT_EQ(LeaseStatsQuery::ALL_SUBNETS, qry->getSelectMode());
+}
+
+// Verify LeaseStatsQuery single-subnet construction
+TEST (LeaseStatsQueryTest, singleSubnetCtor) {
+ LeaseStatsQueryPtr qry;
+
+ // Invalid values for subnet_id
+ ASSERT_THROW(qry.reset(new LeaseStatsQuery(0)), BadValue);
+
+ // Valid values should work and set mode accordingly.
+ ASSERT_NO_THROW(qry.reset(new LeaseStatsQuery(77)));
+ ASSERT_EQ(77, qry->getFirstSubnetID());
+ ASSERT_EQ(0, qry->getLastSubnetID());
+ ASSERT_EQ(LeaseStatsQuery::SINGLE_SUBNET, qry->getSelectMode());
+}
+
+// Verify LeaseStatsQuery subnet-range construction
+TEST (LeaseStatsQueryTest, subnetRangeCtor) {
+ LeaseStatsQueryPtr qry;
+
+ // Either ID set to 0, or a backward range should throw
+ ASSERT_THROW(qry.reset(new LeaseStatsQuery(0,1)), BadValue);
+ ASSERT_THROW(qry.reset(new LeaseStatsQuery(1,0)), BadValue);
+ ASSERT_THROW(qry.reset(new LeaseStatsQuery(2,1)), BadValue);
+
+ // Valid values should work and set mode accordingly.
+ ASSERT_NO_THROW(qry.reset(new LeaseStatsQuery(1,2)));
+ ASSERT_EQ(1, qry->getFirstSubnetID());
+ ASSERT_EQ(2, qry->getLastSubnetID());
+ ASSERT_EQ(LeaseStatsQuery::SUBNET_RANGE, qry->getSelectMode());
+}
+
+// There's no point in calling any other methods in LeaseMgr, as they
+// are purely virtual, so we would only call ConcreteLeaseMgr methods.
+// Those methods are just stubs that do not return anything.
+
+} // namespace
diff --git a/src/lib/dhcpsrv/tests/lease_unittest.cc b/src/lib/dhcpsrv/tests/lease_unittest.cc
new file mode 100644
index 0000000..9fae66d
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/lease_unittest.cc
@@ -0,0 +1,1385 @@
+// Copyright (C) 2013-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcp/duid.h>
+#include <dhcpsrv/lease.h>
+#include <util/pointer_util.h>
+#include <testutils/test_to_element.h>
+#include <cc/data.h>
+#include <gtest/gtest.h>
+#include <vector>
+#include <sstream>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::data;
+using namespace isc::test;
+
+namespace {
+
+/// Hardware address used by different tests.
+const uint8_t HWADDR[] = {0x08, 0x00, 0x2b, 0x02, 0x3f, 0x4e};
+/// Client id used by different tests.
+const uint8_t CLIENTID[] = {0x17, 0x34, 0xe2, 0xff, 0x09, 0x92, 0x54};
+/// Valid lifetime value used by different tests.
+const uint32_t VALID_LIFETIME = 500;
+/// Subnet ID used by different tests.
+const uint32_t SUBNET_ID = 42;
+/// IAID value used by different tests.
+const uint32_t IAID = 7;
+
+/// @brief Creates an instance of the lease with certain FQDN data.
+///
+/// @param hostname Hostname.
+/// @param fqdn_fwd Forward FQDN update setting for a created lease.
+/// @param fqdn_rev Reverse FQDN update setting for a created lease.
+///
+/// @return Instance of the created lease.
+Lease4 createLease4(const std::string& hostname, const bool fqdn_fwd,
+ const bool fqdn_rev) {
+ Lease4 lease;
+ lease.hostname_ = hostname;
+ lease.fqdn_fwd_ = fqdn_fwd;
+ lease.fqdn_rev_ = fqdn_rev;
+ return (lease);
+}
+
+/// @brief Tests that an exception is thrown when one of the lease
+/// parameters is missing or invalid during lease parsing.
+///
+/// If the tested parameter is mandatory the test first removes the
+/// specified parameter from the JSON structure and tries to re-create
+/// the lease, expecting an error.
+/// Next, the test sets invalid value for this parameter and also
+/// expected an error.
+///
+/// @tparam LeaseType @c Lease4 or @c Lease6.
+/// @tparam ValueType type of the invalid value to be used for the
+/// specified parameter.
+/// @param lease_as_json a string contains lease in a JSON format.
+/// @param parameter_name name of the lease parameter to be tested.
+/// @param invalid_value invalid parameter value.
+/// @param mandatory boolean value indicating if the parameter is
+/// mandatory.
+template<typename LeaseType, typename ValueType>
+void testInvalidElement(const std::string& lease_as_json,
+ const std::string& parameter_name,
+ ValueType invalid_value,
+ const bool mandatory = true) {
+ ElementPtr lease;
+
+ // If the parameter is mandatory, check that the exception is
+ // thrown if it is missing.
+ if (mandatory) {
+ ASSERT_NO_THROW(lease = Element::fromJSON(lease_as_json));
+ lease->remove(parameter_name);
+ EXPECT_THROW(LeaseType::fromElement(lease), BadValue)
+ << "test failed for " << parameter_name;
+ }
+
+ // Set invalid value and expect an error.
+ ASSERT_NO_THROW(lease = Element::fromJSON(lease_as_json));
+ lease->set(parameter_name, Element::create(invalid_value));
+ EXPECT_THROW(LeaseType::fromElement(lease), BadValue)
+ << "test failed for " << parameter_name
+ << " and invalid value " << invalid_value;
+}
+
+/// @brief Fixture class used in Lease4 testing.
+class Lease4Test : public ::testing::Test {
+public:
+
+ /// @brief Default constructor
+ ///
+ /// Currently it only initializes hardware address.
+ Lease4Test() {
+ hwaddr_.reset(new HWAddr(HWADDR, sizeof(HWADDR), HTYPE_ETHER));
+ clientid_.reset(new ClientId(CLIENTID, sizeof(CLIENTID)));
+ }
+
+ /// Hardware address, used by tests.
+ HWAddrPtr hwaddr_;
+
+ /// Pointer to the client identifier used by tests.
+ ClientIdPtr clientid_;
+};
+
+// This test checks if the Lease4 structure can be instantiated correctly.
+TEST_F(Lease4Test, constructor) {
+ // Get current time for the use in Lease.
+ const time_t current_time = time(NULL);
+
+ // We want to check that various addresses work, so let's iterate over
+ // these.
+ const uint32_t ADDRESS[] = {
+ 0x00000000, 0x01020304, 0x7fffffff, 0x80000000, 0x80000001, 0xffffffff
+ };
+
+ for (int i = 0; i < sizeof(ADDRESS) / sizeof(ADDRESS[0]); ++i) {
+
+ // Create the lease
+ Lease4 lease(ADDRESS[i], hwaddr_, clientid_, VALID_LIFETIME,
+ current_time, SUBNET_ID, true, true,
+ "Hostname.Example.Com.");
+
+ EXPECT_EQ(ADDRESS[i], lease.addr_.toUint32());
+ EXPECT_TRUE(util::equalValues(hwaddr_, lease.hwaddr_));
+ EXPECT_TRUE(util::equalValues(clientid_, lease.client_id_));
+ EXPECT_EQ(VALID_LIFETIME, lease.valid_lft_);
+ EXPECT_EQ(current_time, lease.cltt_);
+ EXPECT_EQ(SUBNET_ID, lease.subnet_id_);
+ EXPECT_EQ("hostname.example.com.", lease.hostname_);
+ EXPECT_TRUE(lease.fqdn_fwd_);
+ EXPECT_TRUE(lease.fqdn_rev_);
+ EXPECT_EQ(Lease::STATE_DEFAULT, lease.state_);
+ EXPECT_FALSE(lease.getContext());
+ }
+}
+
+// This test verifies that copy constructor copies Lease4 fields correctly.
+TEST_F(Lease4Test, copyConstructor) {
+
+ // Get current time for the use in Lease4.
+ const time_t current_time = time(NULL);
+
+ // Create the lease
+ Lease4 lease(0xffffffff, hwaddr_, clientid_, VALID_LIFETIME, current_time,
+ SUBNET_ID);
+
+ // Declined is a non-default state. We'll see if the state will be copied
+ // or the default state will be set for the copied lease.
+ lease.state_ = Lease::STATE_DECLINED;
+
+ // Set an user context.
+ lease.setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+
+ // Use copy constructor to copy the lease.
+ Lease4 copied_lease(lease);
+
+ // Both leases should be now equal. When doing this check we assume that
+ // the equality operator works correctly.
+ EXPECT_TRUE(lease == copied_lease);
+ // Client IDs are equal, but they should be in two distinct pointers.
+ EXPECT_FALSE(lease.client_id_ == copied_lease.client_id_);
+
+ // User context are equal and point to the same object.
+ ASSERT_TRUE(copied_lease.getContext());
+ EXPECT_TRUE(lease.getContext() == copied_lease.getContext());
+ EXPECT_TRUE(*lease.getContext() == *copied_lease.getContext());
+
+ // Hardware addresses are equal, but they should point to two objects,
+ // each holding the same data. The content should be equal...
+ EXPECT_TRUE(*lease.hwaddr_ == *copied_lease.hwaddr_);
+
+ // ... but it should point to different objects.
+ EXPECT_FALSE(lease.hwaddr_ == copied_lease.hwaddr_);
+
+ // Now let's check that the hwaddr pointer is copied even if it's NULL:
+ lease.hwaddr_.reset();
+ Lease4 copied_lease2(lease);
+ EXPECT_TRUE(lease == copied_lease2);
+}
+
+// This test verifies that the assignment operator copies all Lease4 fields
+// correctly.
+TEST_F(Lease4Test, operatorAssign) {
+
+ // Get the current time for the use in Lease4.
+ const time_t current_time = time(NULL);
+
+ // Create the lease
+ Lease4 lease(0xffffffff, hwaddr_, clientid_, VALID_LIFETIME, current_time,
+ SUBNET_ID);
+
+ // Declined is a non-default state. We'll see if the state will be copied
+ // or the default state will be set for the copied lease.
+ lease.state_ = Lease::STATE_DECLINED;
+
+ // Set an user context.
+ lease.setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+
+ // Create a default lease.
+ Lease4 copied_lease;
+ // Use assignment operator to assign new lease.
+ copied_lease = lease;
+
+ // Both leases should be now equal. When doing this check we assume that
+ // the equality operator works correctly.
+ EXPECT_TRUE(lease == copied_lease);
+ // Client IDs are equal, but they should be in two distinct pointers.
+ EXPECT_FALSE(lease.client_id_ == copied_lease.client_id_);
+
+ // User context are equal and point to the same object.
+ ASSERT_TRUE(copied_lease.getContext());
+ EXPECT_TRUE(lease.getContext() == copied_lease.getContext());
+ EXPECT_TRUE(*lease.getContext() == *copied_lease.getContext());
+
+ // Hardware addresses are equal, but they should point to two objects,
+ // each holding the same data. The content should be equal...
+ EXPECT_TRUE(*lease.hwaddr_ == *copied_lease.hwaddr_);
+
+ // ... but it should point to different objects.
+ EXPECT_FALSE(lease.hwaddr_ == copied_lease.hwaddr_);
+
+ // Now let's check that the hwaddr pointer is copied even if it's NULL:
+ lease.hwaddr_.reset();
+ copied_lease = lease;
+ EXPECT_TRUE(lease == copied_lease);
+}
+
+// This test verifies that it is correctly determined when the lease
+// belongs to the particular client identified by the client identifier
+// and hw address.
+TEST_F(Lease4Test, leaseBelongsToClient) {
+ // Client identifier that matches the one in the lease.
+ ClientIdPtr matching_client_id = ClientId::fromText("01:02:03:04");
+ // Client identifier that doesn't match the one in the lease.
+ ClientIdPtr diff_client_id = ClientId::fromText("01:02:03:05");
+ // Null (no) client identifier.
+ ClientIdPtr null_client_id;
+
+ // HW Address that matches the one in the lease.
+ HWAddrPtr matching_hw(new HWAddr(HWAddr::fromText("00:01:02:03:04:05",
+ HTYPE_ETHER)));
+ // HW Address that doesn't match the one in the lease.
+ HWAddrPtr diff_hw(new HWAddr(HWAddr::fromText("00:01:02:03:04:06",
+ HTYPE_ETHER)));
+ // Null HW Address.
+ HWAddrPtr null_hw;
+
+ // Create the lease with MAC address and Client Identifier.
+ Lease4 lease(IOAddress("192.0.2.1"), matching_hw, matching_client_id,
+ 60, time(NULL), 0, 0, 1);
+
+ // Verify cases for lease that has both hw address and client identifier.
+ EXPECT_TRUE(lease.belongsToClient(matching_hw, matching_client_id));
+ EXPECT_FALSE(lease.belongsToClient(matching_hw, diff_client_id));
+ EXPECT_TRUE(lease.belongsToClient(matching_hw, null_client_id));
+ EXPECT_TRUE(lease.belongsToClient(diff_hw, matching_client_id));
+ EXPECT_FALSE(lease.belongsToClient(diff_hw, diff_client_id));
+ EXPECT_FALSE(lease.belongsToClient(diff_hw, null_client_id));
+ EXPECT_TRUE(lease.belongsToClient(null_hw, matching_client_id));
+ EXPECT_FALSE(lease.belongsToClient(null_hw, diff_client_id));
+ EXPECT_FALSE(lease.belongsToClient(null_hw, null_client_id));
+
+
+ // Verify cases for lease that has only HW address.
+ lease.client_id_ = null_client_id;
+ EXPECT_TRUE(lease.belongsToClient(matching_hw, matching_client_id));
+ EXPECT_TRUE(lease.belongsToClient(matching_hw, diff_client_id));
+ EXPECT_TRUE(lease.belongsToClient(matching_hw, null_client_id));
+ EXPECT_FALSE(lease.belongsToClient(diff_hw, matching_client_id));
+ EXPECT_FALSE(lease.belongsToClient(diff_hw, diff_client_id));
+ EXPECT_FALSE(lease.belongsToClient(diff_hw, null_client_id));
+ EXPECT_FALSE(lease.belongsToClient(null_hw, matching_client_id));
+ EXPECT_FALSE(lease.belongsToClient(null_hw, diff_client_id));
+ EXPECT_FALSE(lease.belongsToClient(null_hw, null_client_id));
+
+ // Verify cases for lease that has only client identifier.
+ lease.client_id_ = matching_client_id;
+ lease.hwaddr_ = null_hw;
+ EXPECT_TRUE(lease.belongsToClient(matching_hw, matching_client_id));
+ EXPECT_FALSE(lease.belongsToClient(matching_hw, diff_client_id));
+ EXPECT_FALSE(lease.belongsToClient(matching_hw, null_client_id));
+ EXPECT_TRUE(lease.belongsToClient(diff_hw, matching_client_id));
+ EXPECT_FALSE(lease.belongsToClient(diff_hw, diff_client_id));
+ EXPECT_FALSE(lease.belongsToClient(diff_hw, null_client_id));
+ EXPECT_TRUE(lease.belongsToClient(null_hw, matching_client_id));
+ EXPECT_FALSE(lease.belongsToClient(null_hw, diff_client_id));
+ EXPECT_FALSE(lease.belongsToClient(null_hw, null_client_id));
+}
+
+/// @brief Lease4 Equality Test
+///
+/// Checks that the operator==() correctly compares two leases for equality.
+/// As operator!=() is also defined for this class, every check on operator==()
+/// is followed by the reverse check on operator!=().
+TEST_F(Lease4Test, operatorEquals) {
+
+ // Random values for the tests
+ const uint32_t ADDRESS = 0x01020304;
+ const time_t current_time = time(NULL);
+
+ // Check when the leases are equal.
+ Lease4 lease1(ADDRESS, hwaddr_, clientid_, VALID_LIFETIME, current_time,
+ SUBNET_ID);
+ lease1.setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+
+ // We need to make an explicit copy. Otherwise the second lease will just
+ // store a pointer and we'll have two leases pointing to a single HWAddr
+ // or client. That would make modifications to only one impossible.
+ HWAddrPtr hwcopy(new HWAddr(*hwaddr_));
+ ClientIdPtr clientid_copy(new ClientId(*clientid_));
+
+ Lease4 lease2(ADDRESS, hwcopy, clientid_copy, VALID_LIFETIME, current_time,
+ SUBNET_ID);
+ lease2.setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+ EXPECT_TRUE(lease1 == lease2);
+ EXPECT_FALSE(lease1 != lease2);
+
+ // Now vary individual fields in a lease and check that the leases compare
+ // not equal in every case.
+ lease1.addr_ = IOAddress(ADDRESS + 1);
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.addr_ = lease2.addr_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ ++lease1.hwaddr_->hwaddr_[0];
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.hwaddr_ = lease2.hwaddr_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ std::vector<uint8_t> clientid_vec = clientid_->getClientId();
+ ++clientid_vec[0];
+ lease1.client_id_.reset(new ClientId(clientid_vec));
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ --clientid_vec[0];
+ lease1.client_id_.reset(new ClientId(clientid_vec));
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ ++lease1.valid_lft_;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.valid_lft_ = lease2.valid_lft_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ ++lease1.cltt_;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.cltt_ = lease2.cltt_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ ++lease1.subnet_id_;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.subnet_id_ = lease2.subnet_id_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ lease1.hostname_ += std::string("something random");
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.hostname_ = lease2.hostname_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ lease1.fqdn_fwd_ = !lease1.fqdn_fwd_;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.fqdn_fwd_ = lease2.fqdn_fwd_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ lease1.fqdn_rev_ = !lease1.fqdn_rev_;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.fqdn_rev_ = lease2.fqdn_rev_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ lease1.state_ += 1;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease2.state_ += 1;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ lease1.setContext(Element::fromJSON("{ \"foobar\": 5678 }"));
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ lease1.setContext(ConstElementPtr());
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease2.setContext(ConstElementPtr());
+ EXPECT_TRUE(lease1 == lease2); // Check that no user context has mase the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+}
+
+// Verify that the client id can be returned as a vector object and if client
+// id is NULL the empty vector is returned.
+TEST_F(Lease4Test, getClientIdVector) {
+ // Create a lease.
+ Lease4 lease;
+ // By default, the lease should have client id set to NULL. If it doesn't,
+ // continuing the test makes no sense.
+ ASSERT_FALSE(lease.client_id_);
+ // When client id is NULL the vector returned should be empty.
+ EXPECT_TRUE(lease.getClientIdVector().empty());
+
+ // Initialize client identifier to non-null value.
+ lease.client_id_ = clientid_;
+ // Check that the returned vector, encapsulating client id is equal to
+ // the one that has been used to set the client id for the lease.
+ std::vector<uint8_t> returned_vec = lease.getClientIdVector();
+ EXPECT_TRUE(returned_vec == clientid_->getClientId());
+}
+
+// Verify the behavior of the function which checks FQDN data for equality.
+TEST_F(Lease4Test, hasIdenticalFqdn) {
+ Lease4 lease = createLease4("myhost.example.com.", true, true);
+ EXPECT_TRUE(lease.hasIdenticalFqdn(createLease4("myhost.example.com.",
+ true, true)));
+ // Case insensitive comparison.
+ EXPECT_TRUE(lease.hasIdenticalFqdn(createLease4("myHOst.ExamplE.coM.",
+ true, true)));
+ EXPECT_FALSE(lease.hasIdenticalFqdn(createLease4("other.example.com.",
+ true, true)));
+ EXPECT_FALSE(lease.hasIdenticalFqdn(createLease4("myhost.example.com.",
+ false, true)));
+ EXPECT_FALSE(lease.hasIdenticalFqdn(createLease4("myhost.example.com.",
+ true, false)));
+ EXPECT_FALSE(lease.hasIdenticalFqdn(createLease4("myhost.example.com.",
+ false, false)));
+ EXPECT_FALSE(lease.hasIdenticalFqdn(createLease4("other.example.com.",
+ false, false)));
+}
+
+// Verify that toText() method reports Lease4 structure properly.
+TEST_F(Lease4Test, toText) {
+
+ const time_t current_time = 12345678;
+ Lease4 lease(IOAddress("192.0.2.3"), hwaddr_, clientid_, 3600,
+ current_time, 789);
+ lease.setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+
+ std::stringstream expected;
+ expected << "Address: 192.0.2.3\n"
+ << "Valid life: 3600\n"
+ << "Cltt: 12345678\n"
+ << "Hardware addr: " << hwaddr_->toText(false) << "\n"
+ << "Client id: " << clientid_->toText() << "\n"
+ << "Subnet ID: 789\n"
+ << "State: default\n"
+ << "User context: { \"foobar\": 1234 }\n";
+
+ EXPECT_EQ(expected.str(), lease.toText());
+
+ // Now let's try with a lease without hardware address, client identifier
+ // and user context.
+ lease.hwaddr_.reset();
+ lease.client_id_.reset();
+ lease.setContext(ConstElementPtr());
+ expected.str("");
+ expected << "Address: 192.0.2.3\n"
+ << "Valid life: 3600\n"
+ << "Cltt: 12345678\n"
+ << "Hardware addr: (none)\n"
+ << "Client id: (none)\n"
+ << "Subnet ID: 789\n"
+ << "State: default\n";
+ EXPECT_EQ(expected.str(), lease.toText());
+}
+
+// Verify that Lease4 structure can be converted to JSON properly.
+TEST_F(Lease4Test, toElement) {
+
+ const time_t current_time = 12345678;
+ Lease4 lease(IOAddress("192.0.2.3"), hwaddr_, clientid_, 3600,
+ current_time, 789, true, true, "URANIA.example.org");
+ lease.setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+
+ std::string expected = "{"
+ "\"client-id\": \"17:34:e2:ff:09:92:54\","
+ "\"cltt\": 12345678,"
+ "\"fqdn-fwd\": true,"
+ "\"fqdn-rev\": true,"
+ "\"hostname\": \"urania.example.org\","
+ "\"hw-address\": \"08:00:2b:02:3f:4e\","
+ "\"ip-address\": \"192.0.2.3\","
+ "\"state\": 0,"
+ "\"subnet-id\": 789,"
+ "\"user-context\": { \"foobar\": 1234 },"
+ "\"valid-lft\": 3600 "
+ "}";
+
+ runToElementTest<Lease4>(expected, lease);
+
+ // Now let's try with a lease without client-id and user context.
+ lease.client_id_.reset();
+ lease.setContext(ConstElementPtr());
+
+ expected = "{"
+ "\"cltt\": 12345678,"
+ "\"fqdn-fwd\": true,"
+ "\"fqdn-rev\": true,"
+ "\"hostname\": \"urania.example.org\","
+ "\"hw-address\": \"08:00:2b:02:3f:4e\","
+ "\"ip-address\": \"192.0.2.3\","
+ "\"state\": 0,"
+ "\"subnet-id\": 789,"
+ "\"valid-lft\": 3600 "
+ "}";
+
+ runToElementTest<Lease4>(expected, lease);
+
+ // And to finish try with a comment.
+ lease.setContext(Element::fromJSON("{ \"comment\": \"a comment\" }"));
+
+ expected = "{"
+ "\"cltt\": 12345678,"
+ "\"user-context\": { \"comment\": \"a comment\" },"
+ "\"fqdn-fwd\": true,"
+ "\"fqdn-rev\": true,"
+ "\"hostname\": \"urania.example.org\","
+ "\"hw-address\": \"08:00:2b:02:3f:4e\","
+ "\"ip-address\": \"192.0.2.3\","
+ "\"state\": 0,"
+ "\"subnet-id\": 789,"
+ "\"valid-lft\": 3600 "
+ "}";
+
+ runToElementTest<Lease4>(expected, lease);
+}
+
+// Verify that the Lease4 can be created from JSON.
+TEST_F(Lease4Test, fromElement) {
+ std::string json = "{"
+ "\"client-id\": \"17:34:e2:ff:09:92:54\","
+ "\"cltt\": 12345678,"
+ "\"fqdn-fwd\": true,"
+ "\"fqdn-rev\": true,"
+ "\"hostname\": \"urania.example.ORG\","
+ "\"hw-address\": \"08:00:2b:02:3f:4e\","
+ "\"ip-address\": \"192.0.2.3\","
+ "\"state\": 0,"
+ "\"subnet-id\": 789,"
+ "\"user-context\": { \"foo\": \"bar\" },"
+ "\"valid-lft\": 3600 "
+ "}";
+
+ Lease4Ptr lease;
+ ASSERT_NO_THROW(lease = Lease4::fromElement(Element::fromJSON(json)));
+
+ ASSERT_TRUE(lease);
+
+ EXPECT_EQ("192.0.2.3", lease->addr_.toText());
+ EXPECT_EQ(789, static_cast<uint32_t>(lease->subnet_id_));
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_EQ("hwtype=1 08:00:2b:02:3f:4e", lease->hwaddr_->toText());
+ ASSERT_TRUE(lease->client_id_);
+ EXPECT_EQ("17:34:e2:ff:09:92:54", lease->client_id_->toText());
+ EXPECT_EQ(12345678, lease->cltt_);
+ EXPECT_EQ(lease->cltt_, lease->current_cltt_);
+ EXPECT_EQ(3600, lease->valid_lft_);
+ EXPECT_EQ(lease->valid_lft_, lease->current_valid_lft_);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("urania.example.org", lease->hostname_);
+ EXPECT_EQ(Lease::STATE_DEFAULT, lease->state_);
+ ASSERT_TRUE(lease->getContext());
+ EXPECT_EQ("{ \"foo\": \"bar\" }", lease->getContext()->str());
+}
+
+// Test that specifying invalid values for a lease or not specifying
+// mandatory lease parameters causes an error while parsing the lease.
+TEST_F(Lease4Test, fromElementInvalidValues) {
+ // Create valid configuration. We use it as a base from which we will
+ // be removing some of the parameters and some values will be selectively
+ // modified.
+ std::string json = "{"
+ "\"client-id\": \"17:34:e2:ff:09:92:54\","
+ "\"cltt\": 12345678,"
+ "\"fqdn-fwd\": true,"
+ "\"fqdn-rev\": true,"
+ "\"hostname\": \"urania.example.org\","
+ "\"hw-address\": \"08:00:2b:02:3f:4e\","
+ "\"ip-address\": \"192.0.2.3\","
+ "\"state\": 0,"
+ "\"subnet-id\": 789,"
+ "\"valid-lft\": 3600 "
+ "}";
+
+ // Test invalid parameter values and missing parameters.
+ testInvalidElement<Lease4>(json, "client-id", std::string("rock"), false);
+ testInvalidElement<Lease4>(json, "cltt", std::string("xyz"));
+ testInvalidElement<Lease4>(json, "cltt", -1, false);
+ testInvalidElement<Lease4>(json, "fqdn-fwd", 123);
+ testInvalidElement<Lease4>(json, "fqdn-rev", std::string("foo"));
+ testInvalidElement<Lease4, bool>(json, "hostname", true);
+ testInvalidElement<Lease4>(json, "hw-address", "01::00::");
+ testInvalidElement<Lease4>(json, "hw-address", 1234, false);
+ testInvalidElement<Lease4, long int>(json, "ip-address", 0xFF000201);
+ testInvalidElement<Lease4>(json, "ip-address", "2001:db8:1::1", false);
+ testInvalidElement<Lease4>(json, "state", std::string("xyz"));
+ testInvalidElement<Lease4>(json, "state", 1234, false);
+ testInvalidElement<Lease4>(json, "subnet-id", std::string("xyz"));
+ testInvalidElement<Lease4>(json, "subnet-id", -5, false);
+ testInvalidElement<Lease4>(json, "subnet-id", 0x100000000, false);
+ testInvalidElement<Lease4>(json, "valid-lft", std::string("xyz"));
+ testInvalidElement<Lease4>(json, "valid-lft", -3, false);
+ testInvalidElement<Lease4>(json, "user-context", "[ ]", false);
+ testInvalidElement<Lease4>(json, "user-context", 1234, false);
+ testInvalidElement<Lease4>(json, "user-context", false, false);
+ testInvalidElement<Lease4>(json, "user-context", "foo", false);
+}
+
+// Verify that decline() method properly clears up specific fields.
+TEST_F(Lease4Test, decline) {
+
+ const time_t current_time = 12345678;
+ Lease4 lease(IOAddress("192.0.2.3"), hwaddr_, clientid_, 3600,
+ current_time, 789);
+ lease.hostname_ = "foo.example.org";
+ lease.fqdn_fwd_ = true;
+ lease.fqdn_rev_ = true;
+
+ time_t now = time(NULL);
+
+ // Move lease to declined state and set its valid-lifetime to 123 seconds
+ lease.decline(123);
+ ASSERT_TRUE(lease.hwaddr_);
+ EXPECT_EQ("", lease.hwaddr_->toText(false));
+ EXPECT_FALSE(lease.client_id_);
+
+ EXPECT_TRUE(now <= lease.cltt_);
+ EXPECT_TRUE(lease.cltt_ <= now + 1);
+ EXPECT_EQ("", lease.hostname_);
+ EXPECT_FALSE(lease.fqdn_fwd_);
+ EXPECT_FALSE(lease.fqdn_rev_);
+ EXPECT_EQ(Lease::STATE_DECLINED, lease.state_);
+ EXPECT_EQ(123, lease.valid_lft_);
+ EXPECT_FALSE(lease.getContext());
+}
+
+// Verify that the lease states are correctly returned in the textual format.
+TEST_F(Lease4Test, stateToText) {
+ EXPECT_EQ("default", Lease4::statesToText(Lease::STATE_DEFAULT));
+ EXPECT_EQ("declined", Lease4::statesToText(Lease::STATE_DECLINED));
+ EXPECT_EQ("expired-reclaimed", Lease4::statesToText(Lease::STATE_EXPIRED_RECLAIMED));
+}
+
+/// @brief Creates an instance of the lease with certain FQDN data.
+///
+/// @param hostname Hostname.
+/// @param fqdn_fwd Forward FQDN update setting for a created lease.
+/// @param fqdn_rev Reverse FQDN update setting for a created lease.
+///
+/// @return Instance of the created lease.
+Lease6 createLease6(const std::string& hostname, const bool fqdn_fwd,
+ const bool fqdn_rev) {
+ Lease6 lease;
+ lease.hostname_ = hostname;
+ lease.fqdn_fwd_ = fqdn_fwd;
+ lease.fqdn_rev_ = fqdn_rev;
+ return (lease);
+}
+
+// Lease6 is also defined in lease_mgr.h, so is tested in this file as well.
+// This test checks if the Lease6 structure can be instantiated correctly
+TEST(Lease6Test, Lease6ConstructorDefault) {
+
+ // check a variety of addresses with different bits set.
+ const char* ADDRESS[] = {
+ "::", "::1", "2001:db8:1::456",
+ "7fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
+ "8000::", "8000::1",
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"
+ };
+
+ // Other values
+ uint8_t llt[] = {0, 1, 2, 3, 4, 5, 6, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};
+ DuidPtr duid(new DUID(llt, sizeof(llt)));
+ uint32_t iaid = IAID; // Just a number
+ SubnetID subnet_id = 8; // Just another number
+
+ for (int i = 0; i < sizeof(ADDRESS) / sizeof(ADDRESS[0]); ++i) {
+ IOAddress addr(ADDRESS[i]);
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr,
+ duid, iaid, 100, 200,
+ subnet_id));
+
+ EXPECT_TRUE(lease->addr_ == addr);
+ EXPECT_TRUE(*lease->duid_ == *duid);
+ EXPECT_TRUE(lease->iaid_ == iaid);
+ EXPECT_TRUE(lease->subnet_id_ == subnet_id);
+ EXPECT_TRUE(lease->type_ == Lease::TYPE_NA);
+ EXPECT_TRUE(lease->preferred_lft_ == 100);
+ EXPECT_TRUE(lease->valid_lft_ == 200);
+ EXPECT_FALSE(lease->fqdn_fwd_);
+ EXPECT_FALSE(lease->fqdn_rev_);
+ EXPECT_TRUE(lease->hostname_.empty());
+ EXPECT_FALSE(lease->getContext());
+ }
+
+ // Lease6 must be instantiated with a DUID, not with NULL pointer
+ IOAddress addr(ADDRESS[0]);
+ Lease6Ptr lease2;
+ EXPECT_THROW(lease2.reset(new Lease6(Lease::TYPE_NA, addr,
+ DuidPtr(), iaid, 100, 200,
+ subnet_id)), InvalidOperation);
+}
+
+// This test verifies that the Lease6 constructor which accepts FQDN data,
+// sets the data correctly for the lease.
+TEST(Lease6Test, Lease6ConstructorWithFQDN) {
+
+ // check a variety of addresses with different bits set.
+ const char* ADDRESS[] = {
+ "::", "::1", "2001:db8:1::456",
+ "7fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
+ "8000::", "8000::1",
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"
+ };
+
+ // Other values
+ uint8_t llt[] = {0, 1, 2, 3, 4, 5, 6, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};
+ DuidPtr duid(new DUID(llt, sizeof(llt)));
+ uint32_t iaid = IAID; // Just a number
+ SubnetID subnet_id = 8; // Just another number
+
+ for (int i = 0; i < sizeof(ADDRESS) / sizeof(ADDRESS[0]); ++i) {
+ IOAddress addr(ADDRESS[i]);
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr,
+ duid, iaid, 100, 200, subnet_id,
+ true, true, "Host.Example.Com."));
+
+ EXPECT_TRUE(lease->addr_ == addr);
+ EXPECT_TRUE(*lease->duid_ == *duid);
+ EXPECT_TRUE(lease->iaid_ == iaid);
+ EXPECT_TRUE(lease->subnet_id_ == subnet_id);
+ EXPECT_TRUE(lease->type_ == Lease::TYPE_NA);
+ EXPECT_TRUE(lease->preferred_lft_ == 100);
+ EXPECT_TRUE(lease->valid_lft_ == 200);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("host.example.com.", lease->hostname_);
+ }
+
+ // Lease6 must be instantiated with a DUID, not with NULL pointer
+ IOAddress addr(ADDRESS[0]);
+ Lease6Ptr lease2;
+ EXPECT_THROW(lease2.reset(new Lease6(Lease::TYPE_NA, addr,
+ DuidPtr(), iaid, 100, 200,
+ subnet_id)), InvalidOperation);
+}
+
+/// @brief Lease6 Equality Test
+///
+/// Checks that the operator==() correctly compares two leases for equality.
+/// As operator!=() is also defined for this class, every check on operator==()
+/// is followed by the reverse check on operator!=().
+TEST(Lease6Test, operatorEquals) {
+
+ // check a variety of addresses with different bits set.
+ const IOAddress addr("2001:db8:1::456");
+ uint8_t duid_array[] = {0, 1, 2, 3, 4, 5, 6, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};
+ DuidPtr duid(new DUID(duid_array, sizeof(duid_array)));
+ uint32_t iaid = IAID; // just a number
+ SubnetID subnet_id = 8; // just another number
+
+ // Check for equality.
+ Lease6 lease1(Lease::TYPE_NA, addr, duid, iaid, 100, 200, subnet_id);
+ Lease6 lease2(Lease::TYPE_NA, addr, duid, iaid, 100, 200, subnet_id);
+
+ lease1.setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+ lease2.setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+
+ // cltt_ constructs with time(NULL), make sure they are always equal
+ lease1.cltt_ = lease2.cltt_;
+
+ EXPECT_TRUE(lease1 == lease2);
+ EXPECT_FALSE(lease1 != lease2);
+
+ // Go through and alter all the fields one by one
+
+ lease1.addr_ = IOAddress("::1");
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.addr_ = lease2.addr_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ lease1.type_ = Lease::TYPE_PD;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.type_ = lease2.type_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ ++lease1.prefixlen_;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.prefixlen_ = lease2.prefixlen_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ ++lease1.iaid_;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.iaid_ = lease2.iaid_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ ++duid_array[0];
+ lease1.duid_.reset(new DUID(duid_array, sizeof(duid_array)));
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ --duid_array[0];
+ lease1.duid_.reset(new DUID(duid_array, sizeof(duid_array)));
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ ++lease1.preferred_lft_;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.preferred_lft_ = lease2.preferred_lft_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ ++lease1.valid_lft_;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.valid_lft_ = lease2.valid_lft_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ ++lease1.cltt_;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.cltt_ = lease2.cltt_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ ++lease1.subnet_id_;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.subnet_id_ = lease2.subnet_id_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ lease1.hostname_ += std::string("something random");
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.hostname_ = lease2.hostname_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ lease1.fqdn_fwd_ = !lease1.fqdn_fwd_;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.fqdn_fwd_ = lease2.fqdn_fwd_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ lease1.fqdn_rev_ = !lease1.fqdn_rev_;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.fqdn_rev_ = lease2.fqdn_rev_;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ lease1.state_ += 1;
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease2.state_ += 1;
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ lease1.setContext(Element::fromJSON("{ \"foobar\": 5678 }"));
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease1.setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+ EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+
+ lease1.setContext(ConstElementPtr());
+ EXPECT_FALSE(lease1 == lease2);
+ EXPECT_TRUE(lease1 != lease2);
+ lease2.setContext(ConstElementPtr());
+ EXPECT_TRUE(lease1 == lease2); // Check that no user context has mase the
+ EXPECT_FALSE(lease1 != lease2); // ... leases equal
+}
+
+// Checks if lease expiration is calculated properly
+TEST(Lease6Test, Lease6Expired) {
+ const IOAddress addr("2001:db8:1::456");
+ const uint8_t duid_array[] = {0, 1, 2, 3, 4, 5, 6, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};
+ const DuidPtr duid(new DUID(duid_array, sizeof(duid_array)));
+ const uint32_t iaid = IAID; // Just a number
+ const SubnetID subnet_id = 8; // Just another number
+ Lease6 lease(Lease::TYPE_NA, addr, duid, iaid, 100, 200, subnet_id);
+
+ // Case 1: a second before expiration
+ lease.cltt_ = time(NULL) - 100;
+ lease.valid_lft_ = 101;
+ EXPECT_FALSE(lease.expired());
+
+ // Case 2: the lease will expire after this second is concluded
+ lease.cltt_ = time(NULL) - 101;
+ EXPECT_FALSE(lease.expired());
+
+ // Case 3: the lease is expired
+ lease.cltt_ = time(NULL) - 102;
+ EXPECT_TRUE(lease.expired());
+
+ // Case 4: the lease is static
+ lease.cltt_ = 1;
+ lease.valid_lft_ = Lease::INFINITY_LFT;
+ EXPECT_FALSE(lease.expired());
+}
+
+// Verify that the DUID can be returned as a vector object and if DUID is NULL
+// the empty vector is returned.
+TEST(Lease6Test, getDuidVector) {
+ // Create a lease.
+ Lease6 lease;
+ // By default, the lease should have client id set to NULL. If it doesn't,
+ // continuing the test makes no sense.
+ ASSERT_FALSE(lease.duid_);
+ // When client id is NULL the vector returned should be empty.
+ EXPECT_TRUE(lease.getDuidVector().empty());
+ // Now, let's set the non NULL DUID. Fill it with the 8 bytes, each
+ // holding a value of 0x42.
+ std::vector<uint8_t> duid_vec(8, 0x42);
+ lease.duid_ = DuidPtr(new DUID(duid_vec));
+ // Check that the returned vector, encapsulating DUID is equal to
+ // the one that has been used to set the DUID for the lease.
+ std::vector<uint8_t> returned_vec = lease.getDuidVector();
+ EXPECT_TRUE(returned_vec == duid_vec);
+}
+
+// Verify that decline() method properly clears up specific fields.
+TEST(Lease6Test, decline) {
+
+ uint8_t llt[] = {0, 1, 2, 3, 4, 5, 6, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};
+ DuidPtr duid(new DUID(llt, sizeof(llt)));
+
+ HWAddrPtr hwaddr(new HWAddr(HWADDR, sizeof(HWADDR), HTYPE_ETHER));
+
+ // Let's create a lease for 2001:db8::1, DUID, iaid=1234,
+ // pref=3000, valid=4000, subnet-id = 1
+ Lease6 lease(Lease::TYPE_NA, IOAddress("2001:db8::1"), duid,
+ 1234, 3000, 4000, 1, hwaddr);
+ lease.cltt_ = 12345678;
+ lease.hostname_ = "foo.example.org";
+ lease.fqdn_fwd_ = true;
+ lease.fqdn_rev_ = true;
+
+ time_t now = time(NULL);
+
+ // Move the lease to declined state and set probation-period to 123 seconds
+ lease.decline(123);
+
+ ASSERT_TRUE(lease.duid_);
+ ASSERT_EQ("00", lease.duid_->toText());
+ ASSERT_FALSE(lease.hwaddr_);
+ EXPECT_EQ(0, lease.preferred_lft_);
+
+ EXPECT_TRUE(now <= lease.cltt_);
+ EXPECT_TRUE(lease.cltt_ <= now + 1);
+ EXPECT_EQ("", lease.hostname_);
+ EXPECT_FALSE(lease.fqdn_fwd_);
+ EXPECT_FALSE(lease.fqdn_rev_);
+ EXPECT_EQ(Lease::STATE_DECLINED, lease.state_);
+ EXPECT_EQ(123, lease.valid_lft_);
+ EXPECT_FALSE(lease.getContext());
+}
+
+// Verify the behavior of the function which checks FQDN data for equality.
+TEST(Lease6Test, hasIdenticalFqdn) {
+ Lease6 lease = createLease6("myhost.example.com.", true, true);
+ EXPECT_TRUE(lease.hasIdenticalFqdn(createLease6("myhost.example.com.",
+ true, true)));
+ // Case insensitive comparison.
+ EXPECT_TRUE(lease.hasIdenticalFqdn(createLease6("myHOst.ExamplE.coM.",
+ true, true)));
+ EXPECT_FALSE(lease.hasIdenticalFqdn(createLease6("other.example.com.",
+ true, true)));
+ EXPECT_FALSE(lease.hasIdenticalFqdn(createLease6("myhost.example.com.",
+ false, true)));
+ EXPECT_FALSE(lease.hasIdenticalFqdn(createLease6("myhost.example.com.",
+ true, false)));
+ EXPECT_FALSE(lease.hasIdenticalFqdn(createLease6("myhost.example.com.",
+ false, false)));
+ EXPECT_FALSE(lease.hasIdenticalFqdn(createLease6("other.example.com.",
+ false, false)));
+}
+
+// Verify that toText() method reports Lease6 structure properly.
+TEST(Lease6Test, toText) {
+
+ HWAddrPtr hwaddr(new HWAddr(HWADDR, sizeof(HWADDR), HTYPE_ETHER));
+
+ uint8_t llt[] = {0, 1, 2, 3, 4, 5, 6, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};
+ DuidPtr duid(new DUID(llt, sizeof(llt)));
+
+ Lease6 lease(Lease::TYPE_NA, IOAddress("2001:db8::1"), duid, 123456,
+ 400, 800, 5678, hwaddr, 128);
+ lease.cltt_ = 12345678;
+ lease.state_ = Lease::STATE_DECLINED;
+ lease.setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+
+ std::stringstream expected;
+ expected << "Type: IA_NA(" << static_cast<int>(Lease::TYPE_NA) << ")\n"
+ << "Address: 2001:db8::1\n"
+ << "Prefix length: 128\n"
+ << "IAID: 123456\n"
+ << "Pref life: 400\n"
+ << "Valid life: 800\n"
+ << "Cltt: 12345678\n"
+ << "DUID: 00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f\n"
+ << "Hardware addr: " << hwaddr->toText(false) << "\n"
+ << "Subnet ID: 5678\n"
+ << "State: declined\n"
+ << "User context: { \"foobar\": 1234 }\n";
+
+ EXPECT_EQ(expected.str(), lease.toText());
+
+ // Now let's try with a lease without hardware address and user context.
+ lease.hwaddr_.reset();
+ lease.setContext(ConstElementPtr());
+ expected.str("");
+ expected << "Type: IA_NA(" << static_cast<int>(Lease::TYPE_NA) << ")\n"
+ << "Address: 2001:db8::1\n"
+ << "Prefix length: 128\n"
+ << "IAID: 123456\n"
+ << "Pref life: 400\n"
+ << "Valid life: 800\n"
+ << "Cltt: 12345678\n"
+ << "DUID: 00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f\n"
+ << "Hardware addr: (none)\n"
+ << "Subnet ID: 5678\n"
+ << "State: declined\n";
+ EXPECT_EQ(expected.str(), lease.toText());
+}
+
+// Verify that Lease6 structure can be converted to JSON properly.
+// This tests an address lease conversion.
+TEST(Lease6Test, toElementAddress) {
+
+ HWAddrPtr hwaddr(new HWAddr(HWADDR, sizeof(HWADDR), HTYPE_ETHER));
+
+ uint8_t llt[] = {0, 1, 2, 3, 4, 5, 6, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};
+ DuidPtr duid(new DUID(llt, sizeof(llt)));
+
+ Lease6 lease(Lease::TYPE_NA, IOAddress("2001:db8::1"), duid, 123456,
+ 400, 800, 5678, hwaddr, 128);
+ lease.cltt_ = 12345678;
+ lease.state_ = Lease::STATE_DECLINED;
+ lease.hostname_ = "urania.example.org";
+ lease.setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+
+ std::string expected = "{"
+ "\"cltt\": 12345678,"
+ "\"duid\": \"00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f\","
+ "\"fqdn-fwd\": false,"
+ "\"fqdn-rev\": false,"
+ "\"hostname\": \"urania.example.org\","
+ "\"hw-address\": \"08:00:2b:02:3f:4e\","
+ "\"iaid\": 123456,"
+ "\"ip-address\": \"2001:db8::1\","
+ "\"preferred-lft\": 400,"
+ "\"state\": 1,"
+ "\"subnet-id\": 5678,"
+ "\"type\": \"IA_NA\","
+ "\"user-context\": { \"foobar\": 1234 },"
+ "\"valid-lft\": 800"
+ "}";
+
+ runToElementTest<Lease6>(expected, lease);
+
+ // Now let's try with a lease without hardware address and user context.
+ lease.hwaddr_.reset();
+ lease.setContext(ConstElementPtr());
+
+ expected = "{"
+ "\"cltt\": 12345678,"
+ "\"duid\": \"00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f\","
+ "\"fqdn-fwd\": false,"
+ "\"fqdn-rev\": false,"
+ "\"hostname\": \"urania.example.org\","
+ "\"iaid\": 123456,"
+ "\"ip-address\": \"2001:db8::1\","
+ "\"preferred-lft\": 400,"
+ "\"state\": 1,"
+ "\"subnet-id\": 5678,"
+ "\"type\": \"IA_NA\","
+ "\"valid-lft\": 800"
+ "}";
+
+ runToElementTest<Lease6>(expected, lease);
+
+ // And to finish try with a comment.
+ lease.setContext(Element::fromJSON("{ \"comment\": \"a comment\" }"));
+
+ expected = "{"
+ "\"cltt\": 12345678,"
+ "\"user-context\": { \"comment\": \"a comment\" },"
+ "\"duid\": \"00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f\","
+ "\"fqdn-fwd\": false,"
+ "\"fqdn-rev\": false,"
+ "\"hostname\": \"urania.example.org\","
+ "\"iaid\": 123456,"
+ "\"ip-address\": \"2001:db8::1\","
+ "\"preferred-lft\": 400,"
+ "\"state\": 1,"
+ "\"subnet-id\": 5678,"
+ "\"type\": \"IA_NA\","
+ "\"valid-lft\": 800"
+ "}";
+
+ runToElementTest<Lease6>(expected, lease);
+}
+
+// Verify that Lease6 structure can be converted to JSON properly.
+// This tests an address lease conversion.
+TEST(Lease6Test, toElementPrefix) {
+
+ HWAddrPtr hwaddr(new HWAddr(HWADDR, sizeof(HWADDR), HTYPE_ETHER));
+
+ uint8_t llt[] = {0, 1, 2, 3, 4, 5, 6, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};
+ DuidPtr duid(new DUID(llt, sizeof(llt)));
+
+ Lease6 lease(Lease::TYPE_PD, IOAddress("2001:db8::"), duid, 123456,
+ 400, 800, 5678, hwaddr, 56);
+ lease.cltt_ = 12345678;
+ lease.state_ = Lease::STATE_DEFAULT;
+ lease.hostname_ = "urania.example.org";
+ lease.setContext(Element::fromJSON("{ \"foobar\": 1234 }"));
+
+ ElementPtr l = lease.toElement();
+
+ ASSERT_TRUE(l);
+
+ ASSERT_TRUE(l->contains("ip-address"));
+ EXPECT_EQ("2001:db8::", l->get("ip-address")->stringValue());
+
+ ASSERT_TRUE(l->contains("type"));
+ EXPECT_EQ("IA_PD", l->get("type")->stringValue());
+
+ // This is a prefix lease, it must have a prefix length.
+ ASSERT_TRUE(l->contains("prefix-len"));
+ EXPECT_EQ(56, l->get("prefix-len")->intValue());
+
+ ASSERT_TRUE(l->contains("iaid"));
+ EXPECT_EQ(123456, l->get("iaid")->intValue());
+
+ ASSERT_TRUE(l->contains("preferred-lft"));
+ EXPECT_EQ(400, l->get("preferred-lft")->intValue());
+
+ ASSERT_TRUE(l->contains("valid-lft"));
+ EXPECT_EQ(800, l->get("valid-lft")->intValue());
+
+ ASSERT_TRUE(l->contains("duid"));
+ EXPECT_EQ("00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f",
+ l->get("duid")->stringValue());
+
+ ASSERT_TRUE(l->contains("hw-address"));
+ EXPECT_EQ(hwaddr->toText(false), l->get("hw-address")->stringValue());
+
+ ASSERT_TRUE(l->contains("subnet-id"));
+ EXPECT_EQ(5678, l->get("subnet-id")->intValue());
+
+ ASSERT_TRUE(l->contains("state"));
+ EXPECT_EQ(static_cast<int>(Lease::STATE_DEFAULT),
+ l->get("state")->intValue());
+
+ ASSERT_TRUE(l->contains("fqdn-fwd"));
+ EXPECT_FALSE(l->get("fqdn-fwd")->boolValue());
+
+ ASSERT_TRUE(l->contains("fqdn-rev"));
+ EXPECT_FALSE(l->get("fqdn-rev")->boolValue());
+
+ ASSERT_TRUE(l->contains("hostname"));
+ EXPECT_EQ("urania.example.org", l->get("hostname")->stringValue());
+
+ ASSERT_TRUE(l->contains("user-context"));
+ EXPECT_EQ("{ \"foobar\": 1234 }", l->get("user-context")->str());
+
+ // Now let's try with a lease without hardware address or user context.
+ lease.hwaddr_.reset();
+ lease.setContext(ConstElementPtr());
+ l = lease.toElement();
+ EXPECT_FALSE(l->contains("hw-address"));
+ EXPECT_FALSE(l->contains("user-context"));
+
+ // And to finish try with a comment.
+ lease.setContext(Element::fromJSON("{ \"comment\": \"a comment\" }"));
+ l = lease.toElement();
+ EXPECT_FALSE(l->contains("hw-address"));
+ ConstElementPtr ctx = l->get("user-context");
+ ASSERT_TRUE(ctx);
+ ASSERT_EQ(Element::map, ctx->getType());
+ EXPECT_EQ(1, ctx->size());
+ ASSERT_TRUE(ctx->contains("comment"));
+ EXPECT_EQ("a comment", ctx->get("comment")->stringValue());
+}
+
+// Verify that the IA_NA can be created from JSON.
+TEST(Lease6Test, fromElementNA) {
+ std::string json = "{"
+ "\"cltt\": 12345678,"
+ "\"duid\": \"00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f\","
+ "\"fqdn-fwd\": false,"
+ "\"fqdn-rev\": false,"
+ "\"hostname\": \"urania.EXAMPLE.org\","
+ "\"hw-address\": \"08:00:2b:02:3f:4e\","
+ "\"iaid\": 123456,"
+ "\"ip-address\": \"2001:db8::1\","
+ "\"preferred-lft\": 400,"
+ "\"state\": 1,"
+ "\"subnet-id\": 5678,"
+ "\"type\": \"IA_NA\","
+ "\"user-context\": { \"foobar\": 1234 },"
+ "\"valid-lft\": 800"
+ "}";
+
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = Lease6::fromElement(Element::fromJSON(json)));
+
+ ASSERT_TRUE(lease);
+
+ EXPECT_EQ("2001:db8::1", lease->addr_.toText());
+ EXPECT_EQ(5678, static_cast<uint32_t>(lease->subnet_id_));
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_EQ("hwtype=1 08:00:2b:02:3f:4e", lease->hwaddr_->toText());
+ EXPECT_EQ(12345678, lease->cltt_);
+ EXPECT_EQ(lease->cltt_, lease->current_cltt_);
+ EXPECT_EQ(800, lease->valid_lft_);
+ EXPECT_EQ(lease->valid_lft_, lease->current_valid_lft_);
+ EXPECT_FALSE(lease->fqdn_fwd_);
+ EXPECT_FALSE(lease->fqdn_rev_);
+ EXPECT_EQ("urania.example.org", lease->hostname_);
+ EXPECT_EQ(Lease::STATE_DECLINED, lease->state_);
+ ASSERT_TRUE(lease->getContext());
+ EXPECT_EQ("{ \"foobar\": 1234 }", lease->getContext()->str());
+
+ // IPv6 specific properties.
+ EXPECT_EQ(Lease::TYPE_NA, lease->type_);
+ EXPECT_EQ(0, lease->prefixlen_);
+ EXPECT_EQ(123456, lease->iaid_);
+ ASSERT_TRUE(lease->duid_);
+ EXPECT_EQ("00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f", lease->duid_->toText());
+ EXPECT_EQ(400, lease->preferred_lft_);
+}
+
+// Verify that the IA_PD can be created from JSON.
+TEST(Lease6Test, fromElementPD) {
+ std::string json = "{"
+ "\"cltt\": 12345678,"
+ "\"duid\": \"00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f\","
+ "\"fqdn-fwd\": false,"
+ "\"fqdn-rev\": false,"
+ "\"hostname\": \"uraniA.exaMple.orG\","
+ "\"hw-address\": \"08:00:2b:02:3f:4e\","
+ "\"iaid\": 123456,"
+ "\"ip-address\": \"3000::\","
+ "\"preferred-lft\": 400,"
+ "\"prefix-len\": 32,"
+ "\"state\": 0,"
+ "\"subnet-id\": 1234,"
+ "\"type\": \"IA_PD\","
+ "\"valid-lft\": 600"
+ "}";
+
+ Lease6Ptr lease;
+ ASSERT_NO_THROW(lease = Lease6::fromElement(Element::fromJSON(json)));
+
+ ASSERT_TRUE(lease);
+
+ EXPECT_EQ("3000::", lease->addr_.toText());
+ EXPECT_EQ(1234, static_cast<uint32_t>(lease->subnet_id_));
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_EQ("hwtype=1 08:00:2b:02:3f:4e", lease->hwaddr_->toText());
+ EXPECT_EQ(12345678, lease->cltt_);
+ EXPECT_EQ(lease->cltt_, lease->current_cltt_);
+ EXPECT_EQ(600, lease->valid_lft_);
+ EXPECT_EQ(lease->valid_lft_, lease->current_valid_lft_);
+ EXPECT_FALSE(lease->fqdn_fwd_);
+ EXPECT_FALSE(lease->fqdn_rev_);
+ EXPECT_EQ("urania.example.org", lease->hostname_);
+ EXPECT_EQ(Lease::STATE_DEFAULT , lease->state_);
+ EXPECT_FALSE(lease->getContext());
+
+ // IPv6 specific properties.
+ EXPECT_EQ(Lease::TYPE_PD, lease->type_);
+ EXPECT_EQ(32, static_cast<int>(lease->prefixlen_));
+ EXPECT_EQ(123456, lease->iaid_);
+ ASSERT_TRUE(lease->duid_);
+ EXPECT_EQ("00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f", lease->duid_->toText());
+ EXPECT_EQ(400, lease->preferred_lft_);
+}
+
+// Test that specifying invalid values for a lease or not specifying
+// mandatory lease parameters causes an error while parsing the lease.
+TEST(Lease6Test, fromElementInvalidValues) {
+ // Create valid configuration. We use it as a base from which we will
+ // be removing some of the parameters and some values will be selectively
+ // modified.
+ std::string json = "{"
+ "\"cltt\": 12345678,"
+ "\"duid\": \"00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f\","
+ "\"fqdn-fwd\": false,"
+ "\"fqdn-rev\": false,"
+ "\"hostname\": \"urania.example.org\","
+ "\"hw-address\": \"08:00:2b:02:3f:4e\","
+ "\"iaid\": 123456,"
+ "\"ip-address\": \"3000::\","
+ "\"preferred-lft\": 400,"
+ "\"prefix-len\": 32,"
+ "\"state\": 0,"
+ "\"subnet-id\": 1234,"
+ "\"type\": \"IA_PD\","
+ "\"valid-lft\": 600"
+ "}";
+
+ // Test invalid parameter values and missing parameters.
+ testInvalidElement<Lease6>(json, "cltt", std::string("xyz"));
+ testInvalidElement<Lease6>(json, "cltt", -1, false);
+ testInvalidElement<Lease6>(json, "duid", "01::00::");
+ testInvalidElement<Lease6>(json, "duid", 1234, false);
+ testInvalidElement<Lease6>(json, "fqdn-fwd", 123);
+ testInvalidElement<Lease6>(json, "fqdn-rev", std::string("foo"));
+ testInvalidElement<Lease6, bool>(json, "hostname", true);
+ testInvalidElement<Lease6>(json, "hw-address", "01::00::", false);
+ testInvalidElement<Lease6>(json, "hw-address", 1234, false);
+ testInvalidElement<Lease6>(json, "iaid", std::string("1234"));
+ testInvalidElement<Lease6>(json, "iaid", -1);
+ testInvalidElement<Lease6, long int>(json, "ip-address", 0xFF000201);
+ testInvalidElement<Lease6>(json, "ip-address", "192.0.2.0", false);
+ testInvalidElement<Lease6>(json, "preferred-lft", std::string("1234"));
+ testInvalidElement<Lease6>(json, "preferred-lft", -1, false);
+ testInvalidElement<Lease6>(json, "prefix-len", std::string("1234"));
+ testInvalidElement<Lease6>(json, "prefix-len", 130);
+ testInvalidElement<Lease6>(json, "state", std::string("xyz"));
+ testInvalidElement<Lease6>(json, "state", 1234, false);
+ testInvalidElement<Lease6>(json, "subnet-id", std::string("xyz"));
+ testInvalidElement<Lease6>(json, "subnet-id", -5, false);
+ testInvalidElement<Lease6>(json, "subnet-id", 0x100000000, false);
+ testInvalidElement<Lease6>(json, "type", std::string("IA_XY"));
+ testInvalidElement<Lease6>(json, "type", -3, false);
+ testInvalidElement<Lease6>(json, "valid-lft", std::string("xyz"));
+ testInvalidElement<Lease6>(json, "valid-lft", -3, false);
+ testInvalidElement<Lease6>(json, "user-context", "[ ]", false);
+ testInvalidElement<Lease6>(json, "user-context", 1234, false);
+ testInvalidElement<Lease6>(json, "user-context", false, false);
+ testInvalidElement<Lease6>(json, "user-context", "foo", false);
+}
+
+// Verify that the lease states are correctly returned in the textual format.
+TEST(Lease6Test, stateToText) {
+ EXPECT_EQ("default", Lease6::statesToText(Lease::STATE_DEFAULT));
+ EXPECT_EQ("declined", Lease6::statesToText(Lease::STATE_DECLINED));
+ EXPECT_EQ("expired-reclaimed", Lease6::statesToText(Lease::STATE_EXPIRED_RECLAIMED));
+}
+
+
+}; // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/memfile_lease_limits_unittest.cc b/src/lib/dhcpsrv/tests/memfile_lease_limits_unittest.cc
new file mode 100644
index 0000000..5344a4b
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/memfile_lease_limits_unittest.cc
@@ -0,0 +1,600 @@
+// Copyright (C) 2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <dhcpsrv/memfile_lease_limits.h>
+#include <testutils/gtest_utils.h>
+
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::data;
+
+namespace {
+
+/// @brief Test fixture for exercising ClassLeaseCounter.
+class ClassLeaseCounterTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ ClassLeaseCounterTest() {
+ // Create two lists of classes and user contexts containing each.
+ // Note the lists have one class in common, "three".
+ classes1_.push_back("one");
+ classes1_.push_back("three");
+ classes1_.push_back("five");
+ ctx1_ = makeContextWithClasses(classes1_);
+
+ classes2_.push_back("two");
+ classes2_.push_back("three");
+ classes2_.push_back("four");
+ ctx2_ = makeContextWithClasses(classes2_);
+ }
+
+ /// @brief Destructor
+ virtual ~ClassLeaseCounterTest() = default;
+
+ /// @brief Create a user-context with a given list of classes
+ ///
+ /// Creates an Element::map with the following content:
+ ///
+ /// {
+ /// "ISC": {
+ /// "client-classes": [ "class0", "class1", ... ]
+ /// }
+ /// }
+ ///
+ /// @param classes list of classes to include in the context
+ /// @return ElementPtr containing the user-context
+ ElementPtr makeContextWithClasses(const std::list<ClientClass>& classes) {
+ ElementPtr ctx = Element::createMap();
+ if (classes.size()) {
+ ElementPtr clist = Element::createList();
+ for (auto client_class : classes ) {
+ clist->add(Element::create(client_class));
+ }
+
+ ElementPtr client_classes = Element::createMap();
+ client_classes->set("client-classes", clist);
+ ctx->set("ISC", client_classes);
+ }
+
+ return (ctx);
+ }
+
+ /// @brief Constructs a lease of a given Lease::Type
+ ///
+ /// @param ltype type of the lease desired (e.g Lease::TYPE_V4,
+ /// TYPE_NA, or TYPE_PD
+ ///
+ /// @return LeasePtr pointing the newly created lease.
+ LeasePtr leaseFactory(const Lease::Type& ltype) {
+ LeasePtr lease;
+ switch (ltype) {
+ case Lease::TYPE_V4: {
+ uint8_t hwaddr_data[] = { 0, 0x11, 0x11, 0x11, 0x11, 0x11 };
+ HWAddrPtr hwaddr(new HWAddr(hwaddr_data, sizeof(hwaddr_data), HTYPE_ETHER));
+ time_t now = time(NULL);
+ lease.reset(new Lease4(IOAddress("192.0.2.100"), hwaddr, ClientIdPtr(), 500, now, 100));
+ break;
+ }
+ case Lease::TYPE_NA: {
+ DuidPtr duid(new DUID(DUID::fromText("01:02:03:04:05:06:07:08:09")));
+ lease.reset(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"), duid, 1010,
+ 2400, 3600, 100));
+ break;
+ }
+ case Lease::TYPE_PD: {
+ DuidPtr duid(new DUID(DUID::fromText("11:22:33:44:55:66:77:88:99")));
+ lease.reset(new Lease6(Lease::TYPE_PD, IOAddress("3001::"), duid, 1010,
+ 2400, 3600, 100));
+ break;
+ }
+ default:
+ ADD_FAILURE() << "TYPE_TA is not supported";
+ break;
+ }
+
+ return (lease);
+ }
+
+ /// @brief Verifies that the ClassLeaseCounter has the expected entries
+ ///
+ /// @param classes list of classes to verify
+ /// @param classes list of expected lease count for each class (assumed to be
+ /// in the same order as classes)
+ /// @param ltype type of lease counted
+ void checkClassCounts(std::list<ClientClass> classes, std::list<size_t> expected_counts,
+ const Lease::Type& ltype = Lease::TYPE_V4) {
+ ASSERT_EQ(classes.size(), expected_counts.size())
+ << "test is broken, number of classes and counts do not match";
+
+ EXPECT_EQ(classes.size(), clc_.size(ltype));
+ auto expected_count = expected_counts.begin();
+ for (auto client_class : classes) {
+ size_t count;
+ ASSERT_NO_THROW_LOG(count = clc_.getClassCount(client_class, ltype));
+ EXPECT_EQ(count, *expected_count) << ", count is wrong for client_class: " << client_class;
+ ++expected_count;
+ }
+ }
+
+ /// @brief Exercises ClassLeaseCounter::addLease() and removeLease()
+ ///
+ /// Performs a series of adds and removes of a given lease in different
+ /// lease states.
+ ///
+ /// @param lease Lease to add and remove
+ void addAndRemoveLeaseTest(LeasePtr lease) {
+ // Get the lease type for convenience.
+ Lease::Type ltype = lease->getType();
+
+ // Set the lease state to DEFAULT.
+ lease->state_ = Lease::STATE_DEFAULT;
+
+ // Add a lease with no classes. Should create no counts.
+ ASSERT_NO_THROW_LOG(clc_.addLease(lease));
+ EXPECT_EQ(0, clc_.size(ltype));
+
+ // Remove the lease with no classes. Should create no counts.
+ ASSERT_NO_THROW_LOG(clc_.removeLease(lease));
+ EXPECT_EQ(0, clc_.size(ltype));
+
+ // Add user-context with class list to lease.
+ lease->setContext(ctx1_);
+ ASSERT_NO_THROW_LOG(clc_.addLease(lease));
+ checkClassCounts(classes1_, std::list<size_t>({ 1, 1, 1 }), ltype);
+
+ // Add the same lease again. Counts should increment.
+ ASSERT_NO_THROW_LOG(clc_.addLease(lease));
+ checkClassCounts(classes1_, std::list<size_t>({ 2, 2, 2 }), ltype);
+
+ // Set lease state to DECLINED and add it. Counts should not increment.
+ lease->state_ = Lease::STATE_DECLINED;
+ ASSERT_NO_THROW_LOG(clc_.addLease(lease));
+ checkClassCounts(classes1_, std::list<size_t>({ 2, 2, 2 }), ltype);
+
+ // Set lease state to EXPIRED_RECLAIMED and add it. Counts should not increment.
+ lease->state_ = Lease::STATE_EXPIRED_RECLAIMED;
+ ASSERT_NO_THROW_LOG(clc_.addLease(lease));
+ checkClassCounts(classes1_, std::list<size_t>({ 2, 2, 2 }), ltype);
+
+ // Set lease state back to DEFAULT and remove it. Counts should decrement.
+ lease->state_ = Lease::STATE_DEFAULT;
+ ASSERT_NO_THROW_LOG(clc_.removeLease(lease));
+ checkClassCounts(classes1_, std::list<size_t>({ 1, 1, 1 }), ltype);
+
+ // Set lease state to DECLINED and remove it. Counts should not decrement.
+ lease->state_ = Lease::STATE_DECLINED;
+ ASSERT_NO_THROW_LOG(clc_.removeLease(lease));
+ checkClassCounts(classes1_, std::list<size_t>({ 1, 1, 1 }), ltype);
+
+ // Set lease state to EXPIRED_RECLAIMED and remove it. Counts should not decrement.
+ lease->state_ = Lease::STATE_EXPIRED_RECLAIMED;
+ ASSERT_NO_THROW_LOG(clc_.removeLease(lease));
+ checkClassCounts(classes1_, std::list<size_t>({ 1, 1, 1 }), ltype);
+
+ // Set lease state back to DEFAULT and remove it. Counts should reach 0.
+ lease->state_ = Lease::STATE_DEFAULT;
+ ASSERT_NO_THROW_LOG(clc_.removeLease(lease));
+ checkClassCounts(classes1_, std::list<size_t>({ 0, 0, 0 }), ltype);
+
+ // Remove it again. Counts should not go negative.
+ ASSERT_NO_THROW_LOG(clc_.removeLease(lease));
+ checkClassCounts(classes1_, std::list<size_t>({ 0, 0, 0 }), ltype);
+ }
+
+ /// #brief Test updating type of lease for given old and new states and class lists
+ ///
+ /// The test creates an old and new version of the same lease and passes them into
+ /// @c ClassLeaseCounter::updateLease(). It then verifies the class lease counts
+ /// against expected count lists.
+ ///
+ /// @param old_state state of the old lease
+ /// @param old_classes class list of the old lease
+ /// @param expected_old_counts list of expected class lease counts (assumed to be
+ /// in same order as old_classes)
+ /// @param new_state state of the new lease
+ /// @param new_classes class list of the new lease
+ /// @param expected_new_counts list of expected class lease counts (assumed to be
+ /// in same order as new_classes)
+ /// @param ltype type of lease to update
+ void updateLeaseTest(uint32_t old_state, const std::list<ClientClass> old_classes,
+ std::list<size_t> expected_old_counts,
+ uint32_t new_state, const std::list<ClientClass> new_classes,
+ std::list<size_t> expected_new_counts, const Lease::Type& ltype) {
+ // Start the map with a count of one for all the old classes.
+ clc_.clear();
+ for (auto client_class : old_classes) {
+ ASSERT_NO_THROW_LOG(clc_.setClassCount(client_class, 1, ltype));
+ }
+
+ // Create the old and new lease, that beyond classes and state, are the same.
+ LeasePtr old_lease = leaseFactory(ltype);
+ old_lease->state_ = old_state;
+ old_lease->setContext(makeContextWithClasses(old_classes));
+
+ LeasePtr new_lease = leaseFactory(ltype);
+ new_lease->state_ = new_state;
+ new_lease->setContext(makeContextWithClasses(new_classes));
+
+ // Update the count.
+ ASSERT_NO_THROW_LOG(clc_.updateLease(new_lease, old_lease));
+
+ // Verify class counts for the old lease's classes are right.
+ auto expected_count = expected_old_counts.begin();
+ for (auto client_class : old_classes) {
+ size_t count;
+ ASSERT_NO_THROW_LOG(count = clc_.getClassCount(client_class, ltype));
+ EXPECT_EQ(count, *expected_count)
+ << ", old count is wrong for client_class: " << client_class;
+ ++expected_count;
+ }
+
+ // Verify class counts for the new lease's classes are right.
+ expected_count = expected_new_counts.begin();
+ for (auto client_class : new_classes) {
+ size_t count;
+ ASSERT_NO_THROW_LOG(count = clc_.getClassCount(client_class, ltype));
+ EXPECT_EQ(count, *expected_count)
+ << ", new count is wrong for client_class: " << client_class;
+ ++expected_count;
+ }
+ }
+
+ /// @brief Verifies updateLease() over a permutation of class lists and lease states
+ /// for a given lease type
+ /// @param ltype type of lease to test
+ void updateLeaseTests(const Lease::Type& ltype) {
+ // Both state DEFAULT, no old class list.
+ updateLeaseTest(Lease::STATE_DEFAULT, no_classes_, std::list<size_t>{},
+ Lease::STATE_DEFAULT, classes2_, std::list<size_t>{1,1,1}, ltype);
+
+ // Both state DEFAULT, same class list.
+ updateLeaseTest(Lease::STATE_DEFAULT, classes1_, std::list<size_t>{1,1,1},
+ Lease::STATE_DEFAULT, classes1_, std::list<size_t>{1,1,1}, ltype);
+
+ // Both state DEFAULT, different class list. Class "three" is in both lists.
+ updateLeaseTest(Lease::STATE_DEFAULT, classes1_, std::list<size_t>{0,1,0},
+ Lease::STATE_DEFAULT, classes2_, std::list<size_t>{1,1,1}, ltype);
+
+ // Old state is DEFAULT, new state is DECLINED. Old classes should be decremented,
+ // new classes should not be incremented.
+ updateLeaseTest(Lease::STATE_DEFAULT, classes1_, std::list<size_t>{0,0,0},
+ Lease::STATE_DECLINED, classes2_, std::list<size_t>{0,0,0}, ltype);
+
+ // Old state is DEFAULT, new state is EXPIRED_RECLAIMED. Old classes should be decremented,
+ // new classes should not be incremented.
+ updateLeaseTest(Lease::STATE_DEFAULT, classes1_, std::list<size_t>{0,0,0},
+ Lease::STATE_EXPIRED_RECLAIMED, classes2_, std::list<size_t>{0,0,0}, ltype);
+
+ // Old state is DECLINED, new state is DEFAULT. Old classes should not decremented,
+ // new classes should be incremented. Class "three" is in both lists.
+ updateLeaseTest(Lease::STATE_DECLINED, classes1_, std::list<size_t>{1,2,1},
+ Lease::STATE_DEFAULT, classes2_, std::list<size_t>{1,2,1}, ltype);
+
+ // Old state is DECLINED, new state is DECLINED. No count changes.
+ updateLeaseTest(Lease::STATE_DECLINED, classes1_, std::list<size_t>{1,1,1},
+ Lease::STATE_DECLINED, classes2_, std::list<size_t>{0,1,0}, ltype);
+
+ // Old state is DECLINED, new state is EXPIRED-RECLAIMED. No count changes.
+ updateLeaseTest(Lease::STATE_DECLINED, classes1_, std::list<size_t>{1,1,1},
+ Lease::STATE_EXPIRED_RECLAIMED, classes2_, std::list<size_t>{0,1,0}, ltype);
+
+ // Old state is EXPIRED_RECLAIMED , new state is DEFAULT. Old classes should not decremented,
+ // new classes should be incremented. Class "three" is in both lists.
+ updateLeaseTest(Lease::STATE_EXPIRED_RECLAIMED, classes1_, std::list<size_t>{1,2,1},
+ Lease::STATE_DEFAULT, classes2_, std::list<size_t>{1,2,1}, ltype);
+
+ // Old state is EXPIRED_RECLAIMED , new state is DECLINED. No count changes.
+ updateLeaseTest(Lease::STATE_EXPIRED_RECLAIMED, classes1_, std::list<size_t>{1,1,1},
+ Lease::STATE_DECLINED, classes2_, std::list<size_t>{0,1,0}, ltype);
+
+ // Old state is EXPIRED_RECLAIMED , new state is EXPIRED_RECLAIMED. No count changes.
+ updateLeaseTest(Lease::STATE_EXPIRED_RECLAIMED, classes1_, std::list<size_t>{1,1,1},
+ Lease::STATE_EXPIRED_RECLAIMED, classes2_, std::list<size_t>{0,1,0}, ltype);
+
+ }
+
+ /// @brief Verifies ClassLeaseCounter::adjustClassCounts
+ ///
+ /// @param ltype type of lease to count.
+ void adjustClassCountsTest(const Lease::Type ltype) {
+ ConstElementPtr class_list1 = ctx1_->find("ISC/client-classes");
+ ASSERT_TRUE(class_list1);
+
+ // Increment the classes by 2 and verify.
+ ASSERT_NO_THROW_LOG(clc_.adjustClassCounts(class_list1, 2, ltype));
+ checkClassCounts(classes1_, std::list<size_t>({ 2, 2, 2 }), ltype);
+
+ // Decrement the classes by 1 and verify.
+ ASSERT_NO_THROW_LOG(clc_.adjustClassCounts(class_list1, -1, ltype));
+ checkClassCounts(classes1_, std::list<size_t>({ 1, 1, 1 }), ltype);
+
+ // Decrement the classes by 2 and verify that roll-over is avoided..
+ ASSERT_NO_THROW_LOG(clc_.adjustClassCounts(class_list1, -2, ltype));
+ checkClassCounts(classes1_, std::list<size_t>({ 0, 0, 0 }), ltype);
+ }
+
+ /// @brief First test list of classes
+ std::list<ClientClass> classes1_;
+
+ /// @brief Second test list of classes
+ std::list<ClientClass> classes2_;
+
+ /// @brief Empty list of classes
+ std::list<ClientClass> no_classes_;
+
+ /// @brief User context containing classes1_;
+ ElementPtr ctx1_;
+
+ /// @brief User context containing classes2_;
+ ElementPtr ctx2_;
+
+ /// @brief ClassLeaseCounter instance to use in the test.
+ ClassLeaseCounter clc_;
+};
+
+// Tests getting and adjusting basic class counts for
+// a Lease::TYPE_V4.
+TEST_F(ClassLeaseCounterTest, basicCountingTests4) {
+ // Create test classes.
+ ClientClass melon("melon");
+ ClientClass water("water");
+
+ // Fetching the count for a non-existent class
+ // should return 0.
+ ASSERT_EQ(0, clc_.size());
+ size_t count;
+ ASSERT_NO_THROW(count = clc_.getClassCount(melon));
+ EXPECT_EQ(0, count);
+
+ // Calling adjustClassCount() for non-existent class
+ // should result in an entry with the adjustment value.
+ ASSERT_EQ(0, clc_.size());
+ ASSERT_NO_THROW(clc_.adjustClassCount(melon, 1));
+ EXPECT_EQ(1, clc_.size());
+ ASSERT_NO_THROW_LOG(count = clc_.getClassCount(melon));
+ EXPECT_EQ(1, count);
+
+ // Calling adjust() again to add 1 should work.
+ ASSERT_NO_THROW(clc_.adjustClassCount(melon, 1));
+ ASSERT_NO_THROW(count = clc_.getClassCount(melon));
+ EXPECT_EQ(2, count);
+
+ // Calling adjust() to subtract 1 should work.
+ ASSERT_NO_THROW(clc_.adjustClassCount(melon, -1));
+ ASSERT_NO_THROW(count = clc_.getClassCount(melon));
+ EXPECT_EQ(1, count);
+
+ // Calling adjust() to subtract 2 should not rollover.
+ ASSERT_NO_THROW(clc_.adjustClassCount(melon, -2));
+ ASSERT_NO_THROW(count = clc_.getClassCount(melon));
+ EXPECT_EQ(0, count);
+
+ // Set class value for a new class.
+ ASSERT_NO_THROW(clc_.setClassCount(water, 40));
+ EXPECT_EQ(2, clc_.size());
+ ASSERT_NO_THROW(count = clc_.getClassCount(water));
+ EXPECT_EQ(40, count);
+
+ // Should be able to adjust the new class entry.
+ ASSERT_NO_THROW(clc_.adjustClassCount(water, -1));
+ ASSERT_NO_THROW(count = clc_.getClassCount(water));
+ EXPECT_EQ(39, count);
+}
+
+// Tests getting and adjusting basic class counts for
+// a Lease::TYPE_V4.
+TEST_F(ClassLeaseCounterTest, adjustClassCountsTest4) {
+ adjustClassCountsTest(Lease::TYPE_V4);
+}
+
+// Tests getting and adjusting basic class counts for
+// a Lease::TYPE_NA and TYPE_PD.
+TEST_F(ClassLeaseCounterTest, adjustClassCountsTest6) {
+ adjustClassCountsTest(Lease::TYPE_NA);
+ adjustClassCountsTest(Lease::TYPE_PD);
+}
+
+// Tests getting and adjusting basic class counts for
+// a Lease::TYPE_NA and TYPE_PD.
+TEST_F(ClassLeaseCounterTest, basicCountingTests6) {
+ // Create test classes.
+ ClientClass melon("melon");
+ ClientClass water("water");
+
+ // Fetching the count for a non-existent class
+ // should return 0.
+ ASSERT_EQ(0, clc_.size(Lease::TYPE_NA));
+ size_t count;
+ ASSERT_NO_THROW(count = clc_.getClassCount(melon, Lease::TYPE_NA));
+ EXPECT_EQ(0, count);
+
+ // Calling adjustClassCount() for non-existent class
+ // should result in an entry with the adjustment value.
+ ASSERT_NO_THROW(clc_.adjustClassCount(melon, 1, Lease::TYPE_NA));
+ EXPECT_EQ(1, clc_.size(Lease::TYPE_NA));
+ ASSERT_NO_THROW_LOG(count = clc_.getClassCount(melon, Lease::TYPE_NA));
+ EXPECT_EQ(1, count);
+
+ // Calling adjust() again to add 1 should work.
+ ASSERT_NO_THROW(clc_.adjustClassCount(melon, 1, Lease::TYPE_NA));
+ ASSERT_NO_THROW(count = clc_.getClassCount(melon, Lease::TYPE_NA));
+ EXPECT_EQ(2, count);
+
+ // Calling adjust() to subtract 1 should work.
+ ASSERT_NO_THROW(clc_.adjustClassCount(melon, -1, Lease::TYPE_NA));
+ ASSERT_NO_THROW(count = clc_.getClassCount(melon, Lease::TYPE_NA));
+ EXPECT_EQ(1, count);
+
+ // Calling adjust() to subtract 2 should not rollover.
+ ASSERT_NO_THROW(clc_.adjustClassCount(melon, -2, Lease::TYPE_NA));
+ ASSERT_NO_THROW(count = clc_.getClassCount(melon, Lease::TYPE_NA));
+ EXPECT_EQ(0, count);
+
+ // Set class value for a new class.
+ ASSERT_NO_THROW(clc_.setClassCount(water, 40, Lease::TYPE_NA));
+ EXPECT_EQ(2, clc_.size(Lease::TYPE_NA));
+ ASSERT_NO_THROW(count = clc_.getClassCount(water, Lease::TYPE_NA));
+ EXPECT_EQ(40, count);
+
+ // Should be able to adjust the new class entry.
+ ASSERT_NO_THROW(clc_.adjustClassCount(water, -1, Lease::TYPE_NA));
+ ASSERT_NO_THROW(count = clc_.getClassCount(water, Lease::TYPE_NA));
+ EXPECT_EQ(39, count);
+
+ // Existing class should be able to count prefixes independently.
+ ASSERT_NO_THROW(clc_.adjustClassCount(melon, 5, Lease::TYPE_PD));
+ EXPECT_EQ(1, clc_.size(Lease::TYPE_PD));
+ ASSERT_NO_THROW(count = clc_.getClassCount(melon, Lease::TYPE_PD));
+ EXPECT_EQ(5, count);
+
+ // Should be able to adjust the new class prefix entry.
+ ASSERT_NO_THROW(clc_.adjustClassCount(melon, -2, Lease::TYPE_PD));
+ ASSERT_NO_THROW(count = clc_.getClassCount(melon, Lease::TYPE_PD));
+ EXPECT_EQ(3, count);
+
+ // Calling adjust() to subtract 4 should not rollover.
+ ASSERT_NO_THROW(clc_.adjustClassCount(melon, -4, Lease::TYPE_PD));
+ ASSERT_NO_THROW(count = clc_.getClassCount(melon, Lease::TYPE_PD));
+ EXPECT_EQ(0, count);
+}
+
+// Exercises ClassLeaseCounter::getLeaseClientClasses()
+// No need for v4 and v6 versions of this test, getLeaseClientClasses()
+// is not protocol specific.
+TEST_F(ClassLeaseCounterTest, getLeaseClientClassesTest) {
+ LeasePtr lease;
+
+ // Describes an invalid context scenario, that
+ // is expected to cause an exception throw.
+ struct InvalidScenario {
+ std::string ctx_json_; // User context contents in JSON form.
+ std::string exp_message_; // Expected exception text.
+ };
+
+ // Invalid context scenarios.
+ std::list<InvalidScenario> invalid_scenarios {
+ {
+ " \"bogus\" ",
+ "getLeaseClientClasses - invalid context: \"bogus\","
+ " find(string) called on a non-map Element in (<string>:1:2)"
+ },
+ {
+ "{\"ISC\": \"bogus\" }",
+ "getLeaseClientClasses - invalid context: {\n \"ISC\": \"bogus\"\n},"
+ " find(string) called on a non-map Element in (<string>:1:9)"
+ },
+ {
+ "{\"ISC\": { \"client-classes\": \"bogus\" } }",
+ "getLeaseClientClasses - invalid context:"
+ " {\n \"ISC\": {\n \"client-classes\": \"bogus\"\n }\n},"
+ " client-classes is not a list"
+ }
+ };
+
+ // Iterate over the invalid scenarios.
+ for (auto scenario : invalid_scenarios) {
+ // Construct the lease and context.
+ lease = leaseFactory(Lease::TYPE_V4);
+ ElementPtr ctx;
+ ASSERT_NO_THROW(ctx = Element::fromJSON(scenario.ctx_json_))
+ << "test is broken" << scenario.ctx_json_;
+ lease->setContext(ctx);
+
+ // Calling getLeaseClientClasses() should throw.
+ ASSERT_THROW_MSG(clc_.getLeaseClientClasses(lease), BadValue, scenario.exp_message_);
+ }
+
+ // Describes an valid context scenario, that is expected
+ // to return normally.
+ struct ValidScenario {
+ std::string ctx_json_; // User context contents in JSON form.
+ std::string exp_classes_; // Expected class list in JSON form.
+ };
+
+ // Valid scenarios.
+ std::list<ValidScenario> valid_scenarios {
+ {
+ // No context
+ "",
+ ""
+ },
+ {
+ // No client-classes element
+ "{\"ISC\": {} }",
+ ""
+ },
+ {
+ // Empty client-classes list
+ "{\"ISC\": { \"client-classes\": []} }",
+ "[]"
+ },
+ {
+ "{\"ISC\": { \"client-classes\": [ \"one\", \"two\", \"three\" ]} }",
+ "[ \"one\", \"two\", \"three\" ]"
+ }
+ };
+
+ // Iterate over the scenarios.
+ for (auto scenario : valid_scenarios) {
+ // Construct the lease and context.
+ lease = leaseFactory(Lease::TYPE_V4);
+ if (!scenario.ctx_json_.empty()) {
+ ElementPtr ctx;
+ ASSERT_NO_THROW(ctx = Element::fromJSON(scenario.ctx_json_))
+ << "test is broken" << scenario.ctx_json_;
+ lease->setContext(ctx);
+ }
+
+ // Call getLeaseClientClasses().
+ ConstElementPtr classes;
+ ASSERT_NO_THROW_LOG(classes = clc_.getLeaseClientClasses(lease));
+
+ // Verify we got the expected outcome for a class list.
+ if (scenario.exp_classes_.empty()) {
+ ASSERT_FALSE(classes);
+ } else {
+ ASSERT_TRUE(classes);
+ ElementPtr exp_classes;
+ ASSERT_NO_THROW(exp_classes = Element::fromJSON(scenario.exp_classes_))
+ << "test is broken" << scenario.exp_classes_;
+ EXPECT_EQ(*classes, *exp_classes);
+ }
+ }
+}
+
+TEST_F(ClassLeaseCounterTest, addRemoveLeaseTest4) {
+ addAndRemoveLeaseTest(leaseFactory(Lease::TYPE_V4));
+}
+
+TEST_F(ClassLeaseCounterTest, addRemoveLeaseTest6_NA) {
+ addAndRemoveLeaseTest(leaseFactory(Lease::TYPE_NA));
+}
+
+TEST_F(ClassLeaseCounterTest, addRemoveLeaseTest6_PD) {
+ addAndRemoveLeaseTest(leaseFactory(Lease::TYPE_PD));
+}
+
+TEST_F(ClassLeaseCounterTest, updateLeaseTests4) {
+ updateLeaseTests(Lease::TYPE_V4);
+}
+
+TEST_F(ClassLeaseCounterTest, updateLeaseTests6_NA) {
+ updateLeaseTests(Lease::TYPE_NA);
+}
+
+TEST_F(ClassLeaseCounterTest, updateLeaseTests6_PD) {
+ updateLeaseTests(Lease::TYPE_PD);
+}
+
+} // namespace
diff --git a/src/lib/dhcpsrv/tests/memfile_lease_mgr_unittest.cc b/src/lib/dhcpsrv/tests/memfile_lease_mgr_unittest.cc
new file mode 100644
index 0000000..a0986d9
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/memfile_lease_mgr_unittest.cc
@@ -0,0 +1,2807 @@
+// Copyright (C) 2012-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/interval_timer.h>
+#include <asiolink/io_address.h>
+#include <dhcp/duid.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/csv_lease_file4.h>
+#include <dhcpsrv/csv_lease_file6.h>
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/memfile_lease_mgr.h>
+#include <dhcpsrv/timer_mgr.h>
+#include <dhcpsrv/testutils/lease_file_io.h>
+#include <dhcpsrv/testutils/test_utils.h>
+#include <dhcpsrv/tests/generic_lease_mgr_unittest.h>
+#include <testutils/gtest_utils.h>
+#include <util/multi_threading_mgr.h>
+#include <util/pid_file.h>
+#include <util/range_utilities.h>
+#include <util/stopwatch.h>
+
+#include <gtest/gtest.h>
+
+#include <cstdlib>
+#include <iostream>
+#include <fstream>
+#include <queue>
+#include <sstream>
+
+#include <unistd.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::db;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Class derived from @c Memfile_LeaseMgr to test LFC timer.
+///
+/// This class provides a custom callback function which is invoked
+/// when the timer for Lease File Cleanup goes off. It is used to
+/// test that the timer is correctly installed.
+class LFCMemfileLeaseMgr : public Memfile_LeaseMgr {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Sets the counter for callbacks to 0.
+ LFCMemfileLeaseMgr(const DatabaseConnection::ParameterMap& parameters)
+ : Memfile_LeaseMgr(parameters), lfc_cnt_(0) {
+ }
+
+ /// @brief Returns the number of callback executions.
+ int getLFCCount() {
+ return (lfc_cnt_);
+ }
+
+protected:
+
+ /// @brief Custom callback.
+ ///
+ /// This callback function increases the counter of callback executions.
+ /// By examining the counter value a test may verify that the callback
+ /// was triggered an expected number of times.
+ virtual void lfcCallback() {
+ ++lfc_cnt_;
+ }
+
+private:
+
+ /// @brief Counter of callback function executions.
+ int lfc_cnt_;
+
+};
+
+/// @brief A derivation of the lease manager exposing protected methods.
+class NakedMemfileLeaseMgr : public Memfile_LeaseMgr {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Creates instance of the lease manager.
+ NakedMemfileLeaseMgr(const DatabaseConnection::ParameterMap& parameters)
+ : Memfile_LeaseMgr(parameters) {
+ }
+
+ using Memfile_LeaseMgr::lfcCallback;
+};
+
+/// @brief Test fixture class for @c Memfile_LeaseMgr
+class MemfileLeaseMgrTest : public GenericLeaseMgrTest {
+public:
+
+ /// @brief memfile lease mgr test constructor
+ ///
+ /// Creates memfile and stores it in lmptr_ pointer
+ MemfileLeaseMgrTest() :
+ io4_(getLeaseFilePath("leasefile4_0.csv")),
+ io6_(getLeaseFilePath("leasefile6_0.csv")),
+ io_service_(getIOService()),
+ timer_mgr_(TimerMgr::instance()) {
+
+ timer_mgr_->setIOService(io_service_);
+ LeaseMgr::setIOService(io_service_);
+
+ std::ostringstream s;
+ s << KEA_LFC_BUILD_DIR << "/kea-lfc";
+ setenv("KEA_LFC_EXECUTABLE", s.str().c_str(), 1);
+
+ // Remove lease files and products of Lease File Cleanup.
+ removeFiles(getLeaseFilePath("leasefile4_0.csv"));
+ removeFiles(getLeaseFilePath("leasefile6_0.csv"));
+
+ MultiThreadingMgr::instance().setMode(false);
+ }
+
+ /// @brief Reopens the connection to the backend.
+ ///
+ /// This function is called by unit tests to force reconnection of the
+ /// backend to check that the leases are stored and can be retrieved
+ /// from the storage.
+ ///
+ /// @param u Universe (V4 or V6)
+ virtual void reopen(Universe u) {
+ LeaseMgrFactory::destroy();
+ startBackend(u);
+ }
+
+ /// @brief destructor
+ ///
+ /// destroys lease manager backend.
+ virtual ~MemfileLeaseMgrTest() {
+ // Stop TimerMgr worker thread if it is running.
+ // Make sure there are no timers registered.
+ timer_mgr_->unregisterTimers();
+ LeaseMgrFactory::destroy();
+ // Remove lease files and products of Lease File Cleanup.
+ removeFiles(getLeaseFilePath("leasefile4_0.csv"));
+ removeFiles(getLeaseFilePath("leasefile6_0.csv"));
+ // Disable multi-threading.
+ MultiThreadingMgr::instance().setMode(false);
+ }
+
+ /// @brief Remove files being products of Lease File Cleanup.
+ ///
+ /// @param base_name Path to the lease file name. This file is removed
+ /// and all files which names are created from this name (having specific
+ /// suffixes used by Lease File Cleanup mechanism).
+ void removeFiles(const std::string& base_name) const {
+ // Generate suffixes and append them to the base name. The
+ // resulting file names are the ones that may exist as a
+ // result of LFC.
+ for (int i = static_cast<int>(Memfile_LeaseMgr::FILE_CURRENT);
+ i <= static_cast<int>(Memfile_LeaseMgr::FILE_FINISH);
+ ++i) {
+ Memfile_LeaseMgr::LFCFileType type = static_cast<
+ Memfile_LeaseMgr::LFCFileType>(i);
+ LeaseFileIO io(Memfile_LeaseMgr::appendSuffix(base_name, type));
+ io.removeFile();
+ }
+ }
+
+ /// @brief Return path to the lease file used by unit tests.
+ ///
+ /// @param filename Name of the lease file appended to the path to the
+ /// directory where test data is held.
+ ///
+ /// @return Full path to the lease file.
+ static std::string getLeaseFilePath(const std::string& filename) {
+ std::ostringstream s;
+ s << TEST_DATA_BUILDDIR << "/" << filename;
+ return (s.str());
+ }
+
+ /// @brief Returns the configuration string for the backend.
+ ///
+ /// This string configures the @c LeaseMgrFactory to create the memfile
+ /// backend and use leasefile4_0.csv and leasefile6_0.csv files as
+ /// storage for leases.
+ ///
+ /// @param no_universe Indicates whether universe parameter should be
+ /// included (false), or not (true).
+ ///
+ /// @return Configuration string for @c LeaseMgrFactory.
+ static std::string getConfigString(Universe u) {
+ std::ostringstream s;
+ s << "type=memfile " << (u == V4 ? "universe=4 " : "universe=6 ")
+ << "name="
+ << getLeaseFilePath(u == V4 ? "leasefile4_0.csv" : "leasefile6_0.csv")
+ << " lfc-interval=0";
+ return (s.str());
+ }
+
+ /// @brief Creates instance of the backend.
+ ///
+ /// @param u Universe (v4 or V6).
+ void startBackend(Universe u) {
+ try {
+ LeaseMgrFactory::create(getConfigString(u));
+ } catch (...) {
+ std::cerr << "*** ERROR: unable to create instance of the Memfile"
+ " lease database backend.\n";
+ throw;
+ }
+ lmptr_ = &(LeaseMgrFactory::instance());
+ }
+
+ /// @brief Runs IOService and stops after a specified time.
+ ///
+ /// @param ms Duration in milliseconds.
+ void setTestTime(const uint32_t ms) {
+ IntervalTimer timer(*io_service_);
+ timer.setup([this]() {
+ io_service_->stop();
+ }, ms, IntervalTimer::ONE_SHOT);
+
+ io_service_->run();
+ io_service_->get_io_service().reset();
+ }
+
+ /// @brief Waits for the specified process to finish.
+ ///
+ /// @param process An object which started the process.
+ /// @param timeout Timeout in seconds.
+ ///
+ /// @return true if the process ended, false otherwise
+ bool waitForProcess(const Memfile_LeaseMgr& lease_mgr,
+ const uint8_t timeout) {
+ const uint32_t iterations_max = timeout * 1000;
+ IntervalTimer fast_path_timer(*io_service_);
+ IntervalTimer timer(*io_service_);
+ bool elapsed = false;
+ timer.setup([&]() {
+ io_service_->stop();
+ elapsed = true;
+ }, iterations_max, IntervalTimer::ONE_SHOT);
+
+ fast_path_timer.setup([&]() {
+ if (!lease_mgr.isLFCRunning()) {
+ io_service_->stop();
+ }
+ }, 1, IntervalTimer::REPEATING);
+
+ io_service_->run();
+ io_service_->get_io_service().reset();
+ return (!elapsed);
+ }
+
+ /// @brief Single instance of IOService.
+ static asiolink::IOServicePtr getIOService() {
+ static asiolink::IOServicePtr io_service(new asiolink::IOService());
+ return (io_service);
+ }
+
+ /// @brief Generates a DHCPv4 lease with random content.
+ ///
+ /// The following lease parameters are randomly generated:
+ /// - HW address,
+ /// - client identifier,
+ /// - hostname,
+ /// - subnet identifier,
+ /// - client last transmission time,
+ ///
+ /// The following lease parameters are set to constant values:
+ /// - valid lifetime = 1200,
+ /// - DNS update forward flag = false,
+ /// - DNS update reverse flag = false,
+ ///
+ /// The lease address is set to address passed as function
+ /// argument.
+ ///
+ /// @param address Lease address.
+ ///
+ /// @return new lease with random content
+ Lease4Ptr initiateRandomLease4(const IOAddress& address) {
+
+ // Randomize HW address.
+ vector<uint8_t> mac(6);
+ fillRandom(mac.begin(), mac.end());
+ HWAddrPtr hwaddr(new HWAddr(mac, HTYPE_ETHER));
+
+ // Let's generate clientid of random length
+ vector<uint8_t> clientid(4 + random()%20);
+ // And then fill it with random value.
+ fillRandom(clientid.begin(), clientid.end());
+
+ uint32_t valid_lft = 1200;
+ time_t timestamp = time(NULL) - 86400 + random()%86400;
+ bool fqdn_fwd = false;
+ bool fqdn_rev = false;
+ uint32_t subnet_id = 1000 + random()%16;
+
+ std::ostringstream hostname;
+ hostname << "hostname" << (random() % 2048);
+
+ // Return created lease.
+ return (Lease4Ptr(new Lease4(address, hwaddr, &clientid[0],
+ clientid.size(), valid_lft,
+ timestamp, subnet_id, fqdn_fwd,
+ fqdn_rev, hostname.str())));
+ }
+
+ /// @brief Generates a DHCPv6 lease with random content.
+ ///
+ /// The following lease parameters are randomly generated:
+ /// - DUID,
+ /// - IAID,
+ /// - hostname,
+ /// - subnet identifier,
+ /// - client last transmission time,
+ ///
+ /// The following lease parameters are set to constant values:
+ /// - lease type = IA_NA
+ /// - valid lifetime = 1200,
+ /// - preferred lifetime = 1000
+ /// - DNS update forward flag = false,
+ /// - DNS update reverse flag = false,
+ ///
+ /// The lease address is set to address passed as function
+ /// argument.
+ ///
+ /// @param address Lease address.
+ ///
+ /// @return new lease with random content
+ Lease6Ptr initiateRandomLease6(const IOAddress& address) {
+ // Let's generate DUID of random length.
+ std::vector<uint8_t> duid_vec(8 + random()%20);
+ // And then fill it with random value.
+ fillRandom(duid_vec.begin(), duid_vec.end());
+ DuidPtr duid(new DUID(duid_vec));
+
+ Lease::Type lease_type = Lease::TYPE_NA;
+ uint32_t iaid = 1 + random()%100;
+ uint32_t valid_lft = 1200;
+ uint32_t preferred_lft = 1000;
+ time_t timestamp = time(NULL) - 86400 + random()%86400;
+ bool fqdn_fwd = false;
+ bool fqdn_rev = false;
+ uint32_t subnet_id = 1000 + random()%16;
+
+ std::ostringstream hostname;
+ hostname << "hostname" << (random() % 2048);
+
+ // Return created lease.
+ Lease6Ptr lease(new Lease6(lease_type, address, duid, iaid,
+ preferred_lft, valid_lft,
+ subnet_id, fqdn_fwd, fqdn_rev,
+ hostname.str()));
+ lease->cltt_ = timestamp;
+ return (lease);
+ }
+
+ /// @brief Object providing access to v4 lease IO.
+ LeaseFileIO io4_;
+
+ /// @brief Object providing access to v6 lease IO.
+ LeaseFileIO io6_;
+
+ /// @brief Pointer to the IO service used by the tests.
+ IOServicePtr io_service_;
+
+ /// @brief Pointer to the instance of the @c TimerMgr.
+ TimerMgrPtr timer_mgr_;
+};
+
+/// @brief This test checks if the LeaseMgr can be instantiated and that it
+/// parses parameters string properly.
+TEST_F(MemfileLeaseMgrTest, constructor) {
+ DatabaseConnection::ParameterMap pmap;
+ pmap["universe"] = "4";
+ pmap["persist"] = "false";
+ boost::scoped_ptr<Memfile_LeaseMgr> lease_mgr;
+
+ EXPECT_NO_THROW(lease_mgr.reset(new Memfile_LeaseMgr(pmap)));
+
+ pmap["lfc-interval"] = "10";
+ pmap["persist"] = "true";
+ pmap["name"] = getLeaseFilePath("leasefile4_1.csv");
+ pmap["max-row-errors"] = "5";
+ EXPECT_NO_THROW(lease_mgr.reset(new Memfile_LeaseMgr(pmap)));
+
+ // Expecting that persist parameter is yes or no. Everything other than
+ // that is wrong.
+ pmap["persist"] = "bogus";
+ pmap["name"] = getLeaseFilePath("leasefile4_1.csv");
+ EXPECT_THROW(lease_mgr.reset(new Memfile_LeaseMgr(pmap)), isc::BadValue);
+
+ // The lfc-interval must be an integer.
+ pmap["persist"] = "true";
+ pmap["lfc-interval"] = "bogus";
+ EXPECT_THROW(lease_mgr.reset(new Memfile_LeaseMgr(pmap)), isc::BadValue);
+
+ // The max-row-errors must be an integer.
+ pmap["persist"] = "true";
+ pmap["max-row-errors"] = "bogus";
+ EXPECT_THROW(lease_mgr.reset(new Memfile_LeaseMgr(pmap)), isc::BadValue);
+
+ // The max-row-errors must be >= 0.
+ pmap["persist"] = "true";
+ pmap["max-row-errors"] = "-1";
+ EXPECT_THROW(lease_mgr.reset(new Memfile_LeaseMgr(pmap)), isc::BadValue);
+}
+
+/// @brief Checks if there is no lease manager NoLeaseManager is thrown.
+TEST_F(MemfileLeaseMgrTest, noLeaseManager) {
+ LeaseMgrFactory::destroy();
+ EXPECT_THROW(LeaseMgrFactory::instance(), NoLeaseManager);
+}
+
+/// @brief Checks if the getType() and getName() methods both return "memfile".
+TEST_F(MemfileLeaseMgrTest, getTypeAndName) {
+ startBackend(V4);
+ EXPECT_EQ(std::string("memfile"), lmptr_->getType());
+ EXPECT_EQ(std::string("memory"), lmptr_->getName());
+}
+
+/// @brief Checks if the path to the lease files is initialized correctly.
+TEST_F(MemfileLeaseMgrTest, getLeaseFilePath) {
+ // Initialize IO objects, so as the test csv files get removed after the
+ // test (when destructors are called).
+ LeaseFileIO io4(getLeaseFilePath("leasefile4_1.csv"));
+ LeaseFileIO io6(getLeaseFilePath("leasefile6_1.csv"));
+
+ DatabaseConnection::ParameterMap pmap;
+ pmap["universe"] = "4";
+ pmap["name"] = getLeaseFilePath("leasefile4_1.csv");
+ boost::scoped_ptr<Memfile_LeaseMgr> lease_mgr(new Memfile_LeaseMgr(pmap));
+
+ EXPECT_EQ(pmap["name"],
+ lease_mgr->getLeaseFilePath(Memfile_LeaseMgr::V4));
+
+ pmap["persist"] = "false";
+ lease_mgr.reset(new Memfile_LeaseMgr(pmap));
+ EXPECT_TRUE(lease_mgr->getLeaseFilePath(Memfile_LeaseMgr::V4).empty());
+ EXPECT_TRUE(lease_mgr->getLeaseFilePath(Memfile_LeaseMgr::V6).empty());
+}
+
+/// @brief Check if the persistLeases correctly checks that leases should not be written
+/// to disk when disabled through configuration.
+TEST_F(MemfileLeaseMgrTest, persistLeases) {
+ // Initialize IO objects, so as the test csv files get removed after the
+ // test (when destructors are called).
+ LeaseFileIO io4(getLeaseFilePath("leasefile4_1.csv"));
+ LeaseFileIO io6(getLeaseFilePath("leasefile6_1.csv"));
+
+ DatabaseConnection::ParameterMap pmap;
+ pmap["universe"] = "4";
+ pmap["lfc-interval"] = "0";
+ // Specify the names of the lease files. Leases will be written.
+ pmap["name"] = getLeaseFilePath("leasefile4_1.csv");
+ boost::scoped_ptr<Memfile_LeaseMgr> lease_mgr(new Memfile_LeaseMgr(pmap));
+
+ lease_mgr.reset(new Memfile_LeaseMgr(pmap));
+ EXPECT_TRUE(lease_mgr->persistLeases(Memfile_LeaseMgr::V4));
+ EXPECT_FALSE(lease_mgr->persistLeases(Memfile_LeaseMgr::V6));
+
+ pmap["universe"] = "6";
+ pmap["name"] = getLeaseFilePath("leasefile6_1.csv");
+ lease_mgr.reset(new Memfile_LeaseMgr(pmap));
+ EXPECT_FALSE(lease_mgr->persistLeases(Memfile_LeaseMgr::V4));
+ EXPECT_TRUE(lease_mgr->persistLeases(Memfile_LeaseMgr::V6));
+
+ // This should disable writes of leases to disk.
+ pmap["persist"] = "false";
+ lease_mgr.reset(new Memfile_LeaseMgr(pmap));
+ EXPECT_FALSE(lease_mgr->persistLeases(Memfile_LeaseMgr::V4));
+ EXPECT_FALSE(lease_mgr->persistLeases(Memfile_LeaseMgr::V6));
+}
+
+/// @brief Check if it is possible to schedule the timer to perform the Lease
+/// File Cleanup periodically.
+TEST_F(MemfileLeaseMgrTest, lfcTimer) {
+ DatabaseConnection::ParameterMap pmap;
+ pmap["type"] = "memfile";
+ pmap["universe"] = "4";
+ // Specify the names of the lease files. Leases will be written.
+ pmap["name"] = getLeaseFilePath("leasefile4_0.csv");
+ pmap["lfc-interval"] = "1";
+
+ boost::scoped_ptr<LFCMemfileLeaseMgr>
+ lease_mgr(new LFCMemfileLeaseMgr(pmap));
+
+ // Run the test for at most 2.9 seconds.
+ setTestTime(2900);
+
+ // Within 2.9 we should record two LFC executions.
+ EXPECT_EQ(2, lease_mgr->getLFCCount());
+}
+
+/// @brief This test checks if the LFC timer is disabled (doesn't trigger)
+/// cleanups when the lfc-interval is set to 0.
+TEST_F(MemfileLeaseMgrTest, lfcTimerDisabled) {
+ DatabaseConnection::ParameterMap pmap;
+ pmap["type"] = "memfile";
+ pmap["universe"] = "4";
+ pmap["name"] = getLeaseFilePath("leasefile4_0.csv");
+ // Set the LFC interval to 0, which should effectively disable it.
+ pmap["lfc-interval"] = "0";
+
+ boost::scoped_ptr<LFCMemfileLeaseMgr>
+ lease_mgr(new LFCMemfileLeaseMgr(pmap));
+
+ // Run the test for at most 1.9 seconds.
+ setTestTime(1900);
+
+ // There should be no LFC execution recorded.
+ EXPECT_EQ(0, lease_mgr->getLFCCount());
+}
+
+/// @brief This test checks that the callback function executing the cleanup of the
+/// DHCPv4 lease file works as expected.
+TEST_F(MemfileLeaseMgrTest, leaseFileCleanup4) {
+ // This string contains the lease file header, which matches
+ // the contents of the new file in which no leases have been
+ // stored.
+ std::string new_file_contents =
+ "address,hwaddr,client_id,valid_lifetime,expire,"
+ "subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context\n";
+
+ // This string contains the contents of the lease file with exactly
+ // one lease, but two entries. One of the entries should be removed
+ // as a result of lease file cleanup.
+ std::string current_file_contents = new_file_contents +
+ "192.0.2.2,02:02:02:02:02:02,,200,200,8,1,1,,1,{ \"foo\": true }\n"
+ "192.0.2.2,02:02:02:02:02:02,,200,800,8,1,1,,1,\n";
+ LeaseFileIO current_file(getLeaseFilePath("leasefile4_0.csv"));
+ current_file.writeFile(current_file_contents);
+
+ std::string previous_file_contents = new_file_contents +
+ "192.0.2.3,03:03:03:03:03:03,,200,200,8,1,1,,1,\n"
+ "192.0.2.3,03:03:03:03:03:03,,200,800,8,1,1,,1,{ \"bar\": true }\n";
+ LeaseFileIO previous_file(getLeaseFilePath("leasefile4_0.csv.2"));
+ previous_file.writeFile(previous_file_contents);
+
+ // Create the backend.
+ DatabaseConnection::ParameterMap pmap;
+ pmap["type"] = "memfile";
+ pmap["universe"] = "4";
+ pmap["name"] = getLeaseFilePath("leasefile4_0.csv");
+ pmap["lfc-interval"] = "1";
+ boost::scoped_ptr<NakedMemfileLeaseMgr> lease_mgr(new NakedMemfileLeaseMgr(pmap));
+
+ // Try to run the lease file cleanup.
+ ASSERT_NO_THROW(lease_mgr->lfcCallback());
+
+ // The new lease file should have been created and it should contain
+ // no leases.
+ ASSERT_TRUE(current_file.exists());
+ EXPECT_EQ(new_file_contents, current_file.readFile());
+
+ // Wait for the LFC process to complete.
+ ASSERT_TRUE(waitForProcess(*lease_mgr, 2));
+
+ // And make sure it has returned an exit status of 0.
+ EXPECT_EQ(0, lease_mgr->getLFCExitStatus())
+ << "Executing the LFC process failed: make sure that"
+ " the kea-lfc program has been compiled.";
+
+ // Check if we can still write to the lease file.
+ std::vector<uint8_t> hwaddr_vec(6);
+ HWAddrPtr hwaddr(new HWAddr(hwaddr_vec, HTYPE_ETHER));
+ Lease4Ptr new_lease(new Lease4(IOAddress("192.0.2.45"), hwaddr,
+ static_cast<const uint8_t*>(0), 0,
+ 100, 0, 1));
+ ASSERT_NO_THROW(lease_mgr->addLease(new_lease));
+
+ std::string updated_file_contents = new_file_contents +
+ "192.0.2.45,00:00:00:00:00:00,,100,100,1,0,0,,0,\n";
+ EXPECT_EQ(updated_file_contents, current_file.readFile());
+
+ // This string contains the contents of the lease file we
+ // expect after the LFC run. It has two leases with one
+ // entry each.
+ std::string result_file_contents = new_file_contents +
+ "192.0.2.2,02:02:02:02:02:02,,200,800,8,1,1,,1,\n"
+ "192.0.2.3,03:03:03:03:03:03,,200,800,8,1,1,,1,{ \"bar\": true }\n";
+
+ // The LFC should have created a file with the two leases and moved it
+ // to leasefile4_0.csv.2
+ LeaseFileIO input_file(getLeaseFilePath("leasefile4_0.csv.2"), false);
+ ASSERT_TRUE(input_file.exists());
+ // And this file should contain the contents of the result file.
+ EXPECT_EQ(result_file_contents, input_file.readFile());
+}
+
+/// @brief This test checks that the callback function executing the cleanup of the
+/// DHCPv6 lease file works as expected.
+TEST_F(MemfileLeaseMgrTest, leaseFileCleanup6) {
+ // This string contains the lease file header, which matches
+ // the contents of the new file in which no leases have been
+ // stored.
+ std::string new_file_contents =
+ "address,duid,valid_lifetime,expire,subnet_id,"
+ "pref_lifetime,lease_type,iaid,prefix_len,fqdn_fwd,"
+ "fqdn_rev,hostname,hwaddr,state,user_context,"
+ "hwtype,hwaddr_source\n";
+
+ // This string contains the contents of the lease file with exactly
+ // one lease, but two entries. One of the entries should be removed
+ // as a result of lease file cleanup.
+ std::string current_file_contents = new_file_contents +
+ "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,200,200,"
+ "8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,200,800,"
+ "8,100,0,7,0,1,1,,,1,{ \"foo\": true },,\n";
+ LeaseFileIO current_file(getLeaseFilePath("leasefile6_0.csv"));
+ current_file.writeFile(current_file_contents);
+
+ std::string previous_file_contents = new_file_contents +
+ "2001:db8:1::2,01:01:01:01:01:01:01:01:01:01:01:01:01,200,200,"
+ "8,100,0,7,0,1,1,,,1,{ \"bar\": true },,\n"
+ "2001:db8:1::2,01:01:01:01:01:01:01:01:01:01:01:01:01,200,800,"
+ "8,100,0,7,0,1,1,,,1,,\n";
+ LeaseFileIO previous_file(getLeaseFilePath("leasefile6_0.csv.2"));
+ previous_file.writeFile(previous_file_contents);
+
+ // Create the backend.
+ DatabaseConnection::ParameterMap pmap;
+ pmap["type"] = "memfile";
+ pmap["universe"] = "6";
+ pmap["name"] = getLeaseFilePath("leasefile6_0.csv");
+ pmap["lfc-interval"] = "1";
+ boost::scoped_ptr<NakedMemfileLeaseMgr> lease_mgr(new NakedMemfileLeaseMgr(pmap));
+
+ // Try to run the lease file cleanup.
+ ASSERT_NO_THROW(lease_mgr->lfcCallback());
+
+ // The new lease file should have been created and it should contain
+ // no leases.
+ ASSERT_TRUE(current_file.exists());
+ EXPECT_EQ(new_file_contents, current_file.readFile());
+
+ // Wait for the LFC process to complete.
+ ASSERT_TRUE(waitForProcess(*lease_mgr, 2));
+
+ // And make sure it has returned an exit status of 0.
+ EXPECT_EQ(0, lease_mgr->getLFCExitStatus())
+ << "Executing the LFC process failed: make sure that"
+ " the kea-lfc program has been compiled.";
+
+ // Check if we can still write to the lease file.
+ std::vector<uint8_t> duid_vec(13);
+ DuidPtr duid(new DUID(duid_vec));
+ Lease6Ptr new_lease(new Lease6(Lease::TYPE_NA, IOAddress("3000::1"), duid,
+ 123, 300, 400, 2));
+ new_lease->cltt_ = 0;
+ ASSERT_NO_THROW(lease_mgr->addLease(new_lease));
+
+ std::string update_file_contents = new_file_contents +
+ "3000::1,00:00:00:00:00:00:00:00:00:00:00:00:00,400,"
+ "400,2,300,0,123,128,0,0,,,0,,,\n";
+ EXPECT_EQ(update_file_contents, current_file.readFile());
+
+ // This string contains the contents of the lease file we
+ // expect after the LFC run. It has two leases with one
+ // entry each.
+ std::string result_file_contents = new_file_contents +
+ "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,200,800,"
+ "8,100,0,7,0,1,1,,,1,{ \"foo\": true },,\n"
+ "2001:db8:1::2,01:01:01:01:01:01:01:01:01:01:01:01:01,200,800,"
+ "8,100,0,7,0,1,1,,,1,,,\n";
+
+ // The LFC should have created a file with the two leases and moved it
+ // to leasefile6_0.csv.2
+ LeaseFileIO input_file(getLeaseFilePath("leasefile6_0.csv.2"), false);
+ ASSERT_TRUE(input_file.exists());
+ // And this file should contain the contents of the result file.
+ EXPECT_EQ(result_file_contents, input_file.readFile());
+}
+
+/// @brief This test verifies that EXIT_FAILURE status code is returned when
+/// the LFC process fails to start.
+TEST_F(MemfileLeaseMgrTest, leaseFileCleanupStartFail) {
+ // This string contains the lease file header, which matches
+ // the contents of the new file in which no leases have been
+ // stored.
+ std::string new_file_contents =
+ "address,hwaddr,client_id,valid_lifetime,expire,"
+ "subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context\n";
+
+ // Create the lease file to be used by the backend.
+ std::string current_file_contents = new_file_contents +
+ "192.0.2.2,02:02:02:02:02:02,,200,200,8,1,1,,1,\n";
+ LeaseFileIO current_file(getLeaseFilePath("leasefile4_0.csv"));
+ current_file.writeFile(current_file_contents);
+
+ // Specify invalid path to the kea-lfc executable. This should result
+ // in failure status code returned by the child process.
+ setenv("KEA_LFC_EXECUTABLE", "foobar", 1);
+
+ // Create the backend.
+ DatabaseConnection::ParameterMap pmap;
+ pmap["type"] = "memfile";
+ pmap["universe"] = "4";
+ pmap["name"] = getLeaseFilePath("leasefile4_0.csv");
+ pmap["lfc-interval"] = "1";
+ boost::scoped_ptr<NakedMemfileLeaseMgr> lease_mgr;
+ ASSERT_THROW(lease_mgr.reset(new NakedMemfileLeaseMgr(pmap)), ProcessSpawnError);
+}
+
+/// @brief This test checks that the callback function executing the cleanup of the
+/// files doesn't move the current file if the finish file exists
+TEST_F(MemfileLeaseMgrTest, leaseFileFinish) {
+ // This string contains the lease file header, which matches
+ // the contents of the new file in which no leases have been
+ // stored.
+ std::string new_file_contents =
+ "address,duid,valid_lifetime,expire,subnet_id,"
+ "pref_lifetime,lease_type,iaid,prefix_len,fqdn_fwd,"
+ "fqdn_rev,hostname,hwaddr,state,user_context,"
+ "hwtype,hwaddr_source\n";
+
+ // This string contains the contents of the current lease file.
+ // It should not be moved.
+ std::string current_file_contents = new_file_contents +
+ "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,200,200,"
+ "8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,200,800,"
+ "8,100,0,7,0,1,1,,,1,,,\n";
+ LeaseFileIO current_file(getLeaseFilePath("leasefile6_0.csv"));
+ current_file.writeFile(current_file_contents);
+
+ // This string contains the contents of the finish file. It should
+ // be moved to the previous file.
+ std::string finish_file_contents = new_file_contents +
+ "2001:db8:1::2,01:01:01:01:01:01:01:01:01:01:01:01:01,200,800,"
+ "8,100,0,7,0,1,1,,,1,\n";
+ LeaseFileIO finish_file(getLeaseFilePath("leasefile6_0.csv.completed"));
+ finish_file.writeFile(finish_file_contents);
+
+ // Create the backend.
+ DatabaseConnection::ParameterMap pmap;
+ pmap["type"] = "memfile";
+ pmap["universe"] = "6";
+ pmap["name"] = getLeaseFilePath("leasefile6_0.csv");
+ pmap["lfc-interval"] = "1";
+ boost::scoped_ptr<NakedMemfileLeaseMgr> lease_mgr(new NakedMemfileLeaseMgr(pmap));
+
+ // Try to run the lease file cleanup.
+ ASSERT_NO_THROW(lease_mgr->lfcCallback());
+
+ // The current lease file should not have been touched.
+ ASSERT_TRUE(current_file.exists());
+ EXPECT_EQ(current_file_contents, current_file.readFile());
+
+ // Wait for the LFC process to complete.
+ ASSERT_TRUE(waitForProcess(*lease_mgr, 5));
+
+ // And make sure it has returned an exit status of 0.
+ EXPECT_EQ(0, lease_mgr->getLFCExitStatus())
+ << "Executing the LFC process failed: make sure that"
+ " the kea-lfc program has been compiled.";
+
+ // The LFC should have moved the finish file to the previous file -
+ // leasefile6_0.csv.2
+ LeaseFileIO previous_file(getLeaseFilePath("leasefile6_0.csv.2"), false);
+ ASSERT_TRUE(previous_file.exists());
+ // And this file should contain the contents of the finish file.
+ EXPECT_EQ(finish_file_contents, previous_file.readFile());
+
+ // The finish file should have been removed
+ ASSERT_FALSE(finish_file.exists());
+}
+
+/// @brief This test checks that the callback function executing the cleanup of the
+/// files doesn't move the current file if the copy file exists
+TEST_F(MemfileLeaseMgrTest, leaseFileCopy) {
+ // This string contains the lease file header, which matches
+ // the contents of the new file in which no leases have been
+ // stored.
+ std::string new_file_contents =
+ "address,duid,valid_lifetime,expire,subnet_id,"
+ "pref_lifetime,lease_type,iaid,prefix_len,fqdn_fwd,"
+ "fqdn_rev,hostname,hwaddr,state,user_context,"
+ "hwtype,hwaddr_source\n";
+
+ // This string contains the contents of the current lease file.
+ // It should not be moved.
+ std::string current_file_contents = new_file_contents +
+ "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,200,200,"
+ "8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,200,800,"
+ "8,100,0,7,0,1,1,,,1,{ \"foo\": true },,\n";
+ LeaseFileIO current_file(getLeaseFilePath("leasefile6_0.csv"));
+ current_file.writeFile(current_file_contents);
+
+ // This string contains the contents of the copy file. It should
+ // be processed and moved to the previous file. As there is only
+ // one lease the processing should result in the previous file being
+ // the same.
+ std::string input_file_contents = new_file_contents +
+ "2001:db8:1::2,01:01:01:01:01:01:01:01:01:01:01:01:01,200,800,"
+ "8,100,0,7,0,1,1,,,1,{ \"foo\": true },,\n";
+ LeaseFileIO input_file(getLeaseFilePath("leasefile6_0.csv.1"));
+ input_file.writeFile(input_file_contents);
+
+ // Create the backend.
+ DatabaseConnection::ParameterMap pmap;
+ pmap["type"] = "memfile";
+ pmap["universe"] = "6";
+ pmap["name"] = getLeaseFilePath("leasefile6_0.csv");
+ pmap["lfc-interval"] = "1";
+ boost::scoped_ptr<NakedMemfileLeaseMgr> lease_mgr(new NakedMemfileLeaseMgr(pmap));
+
+ // Try to run the lease file cleanup.
+ ASSERT_NO_THROW(lease_mgr->lfcCallback());
+
+ // The current lease file should not have been touched.
+ ASSERT_TRUE(current_file.exists());
+ EXPECT_EQ(current_file_contents, current_file.readFile());
+
+ // Wait for the LFC process to complete.
+ ASSERT_TRUE(waitForProcess(*lease_mgr, 5));
+
+ // And make sure it has returned an exit status of 0.
+ EXPECT_EQ(0, lease_mgr->getLFCExitStatus())
+ << "Executing the LFC process failed: make sure that"
+ " the kea-lfc program has been compiled.";
+
+ // The LFC should have processed the lease and moved it to the previous
+ // file - leasefile6_0.csv.2
+ LeaseFileIO previous_file(getLeaseFilePath("leasefile6_0.csv.2"), false);
+ ASSERT_TRUE(previous_file.exists());
+ // And this file should contain the contents of the copy file.
+ EXPECT_EQ(input_file_contents, previous_file.readFile());
+
+ // The input file should have been removed
+ ASSERT_FALSE(input_file.exists());
+}
+
+/// @brief Checks that adding/getting/deleting a Lease6 object works.
+TEST_F(MemfileLeaseMgrTest, addGetDelete6) {
+ startBackend(V6);
+ testAddGetDelete6();
+}
+
+/// @brief Checks that adding/getting/deleting a Lease6 object works.
+TEST_F(MemfileLeaseMgrTest, addGetDelete6MultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testAddGetDelete6();
+}
+
+/// @brief Basic Lease4 Checks
+///
+/// Checks that the addLease, getLease4 (by address) and deleteLease (with an
+/// IPv4 address) works.
+TEST_F(MemfileLeaseMgrTest, basicLease4) {
+ startBackend(V4);
+ testBasicLease4();
+}
+
+/// @brief Basic Lease4 Checks
+TEST_F(MemfileLeaseMgrTest, basicLease4MultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testBasicLease4();
+}
+
+/// @todo Write more memfile tests
+
+/// @brief Simple test about lease4 retrieval through client id method
+TEST_F(MemfileLeaseMgrTest, getLease4ClientId) {
+ startBackend(V4);
+ testGetLease4ClientId();
+}
+
+/// @brief Simple test about lease4 retrieval through client id method
+TEST_F(MemfileLeaseMgrTest, getLease4ClientIdMultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLease4ClientId();
+}
+
+/// @brief Checks that lease4 retrieval client id is null is working
+TEST_F(MemfileLeaseMgrTest, getLease4NullClientId) {
+ startBackend(V4);
+ testGetLease4NullClientId();
+}
+
+/// @brief Checks that lease4 retrieval client id is null is working
+TEST_F(MemfileLeaseMgrTest, getLease4NullClientIdMultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLease4NullClientId();
+}
+
+/// @brief Checks lease4 retrieval through HWAddr
+TEST_F(MemfileLeaseMgrTest, getLease4HWAddr1) {
+ startBackend(V4);
+ testGetLease4HWAddr1();
+}
+
+/// @brief Checks lease4 retrieval through HWAddr
+TEST_F(MemfileLeaseMgrTest, getLease4HWAddr1MultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLease4HWAddr1();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// a combination of DUID and IAID.
+TEST_F(MemfileLeaseMgrTest, getLease4HWAddr2) {
+ startBackend(V4);
+ testGetLease4HWAddr2();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address
+TEST_F(MemfileLeaseMgrTest, getLease4HWAddr2MultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLease4HWAddr2();
+}
+
+/// @brief Basic Lease4 Checks
+///
+/// Checks that the addLease, getLease4(by address), getLease4(hwaddr,subnet_id),
+/// updateLease4() and deleteLease can handle NULL client-id.
+/// (client-id is optional and may not be present)
+TEST_F(MemfileLeaseMgrTest, lease4NullClientId) {
+ startBackend(V4);
+ testLease4NullClientId();
+}
+
+/// @brief Basic Lease4 Checks
+TEST_F(MemfileLeaseMgrTest, lease4NullClientIdMultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testLease4NullClientId();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address & Subnet ID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// a combination of hardware address and subnet ID
+TEST_F(MemfileLeaseMgrTest, DISABLED_getLease4HwaddrSubnetId) {
+ /// @todo: fails on memfile. It's probably a memfile bug.
+ startBackend(V4);
+ testGetLease4HWAddrSubnetId();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address & Subnet ID
+TEST_F(MemfileLeaseMgrTest, DISABLED_getLease4HwaddrSubnetIdMultiThread) {
+ /// @todo: fails on memfile. It's probably a memfile bug.
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLease4HWAddrSubnetId();
+}
+
+/// @brief Check GetLease4 methods - access by Client ID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// the Client ID.
+TEST_F(MemfileLeaseMgrTest, getLease4ClientId2) {
+ startBackend(V4);
+ testGetLease4ClientId2();
+}
+
+/// @brief Check GetLease4 methods - access by Client ID
+TEST_F(MemfileLeaseMgrTest, getLease4ClientId2MultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLease4ClientId2();
+}
+
+/// @brief Get Lease4 by client ID
+///
+/// Check that the system can cope with a client ID of any size.
+TEST_F(MemfileLeaseMgrTest, getLease4ClientIdSize) {
+ startBackend(V4);
+ testGetLease4ClientIdSize();
+}
+
+/// @brief Get Lease4 by client ID
+TEST_F(MemfileLeaseMgrTest, getLease4ClientIdSizeMultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLease4ClientIdSize();
+}
+
+/// @brief Check GetLease4 methods - access by Client ID & Subnet ID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// a combination of client and subnet IDs.
+TEST_F(MemfileLeaseMgrTest, getLease4ClientIdSubnetId) {
+ startBackend(V4);
+ testGetLease4ClientIdSubnetId();
+}
+
+/// @brief Check GetLease4 methods - access by Client ID & Subnet ID
+TEST_F(MemfileLeaseMgrTest, getLease4ClientIdSubnetIdMultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLease4ClientIdSubnetId();
+}
+
+/// @brief This test checks that all IPv4 leases for a specified subnet id are returned.
+TEST_F(MemfileLeaseMgrTest, getLeases4SubnetId) {
+ startBackend(V4);
+ testGetLeases4SubnetId();
+}
+
+/// @brief This test checks that all IPv4 leases for a specified subnet id are returned.
+TEST_F(MemfileLeaseMgrTest, getLeases4SubnetIdMultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLeases4SubnetId();
+}
+
+/// @brief This test checks that all IPv4 leases with a specified hostname are returned.
+TEST_F(MemfileLeaseMgrTest, getLeases4Hostname) {
+ startBackend(V4);
+ testGetLeases4Hostname();
+}
+
+/// @brief This test checks that all IPv4 leases with a specified hostname are returned.
+TEST_F(MemfileLeaseMgrTest, getLeases4HostnameMultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLeases4Hostname();
+}
+
+/// @brief This test checks that all IPv4 leases are returned.
+TEST_F(MemfileLeaseMgrTest, getLeases4) {
+ startBackend(V4);
+ testGetLeases4();
+}
+
+/// @brief This test checks that all IPv4 leases are returned.
+TEST_F(MemfileLeaseMgrTest, getLeases4MultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLeases4();
+}
+
+/// @brief Test that a range of IPv4 leases is returned with paging.
+TEST_F(MemfileLeaseMgrTest, getLeases4Paged) {
+ startBackend(V4);
+ testGetLeases4Paged();
+}
+
+/// @brief Test that a range of IPv4 leases is returned with paging.
+TEST_F(MemfileLeaseMgrTest, getLeases4PagedMultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLeases4Paged();
+}
+
+/// @brief This test checks that all IPv6 leases for a specified subnet id are returned.
+TEST_F(MemfileLeaseMgrTest, getLeases6SubnetId) {
+ startBackend(V6);
+ testGetLeases6SubnetId();
+}
+
+/// @brief This test checks that all IPv6 leases for a specified subnet id are returned.
+TEST_F(MemfileLeaseMgrTest, getLeases6SubnetIdMultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLeases6SubnetId();
+}
+
+/// @brief This test checks that all IPv6 leases with a specified hostname are returned.
+TEST_F(MemfileLeaseMgrTest, getLeases6Hostname) {
+ startBackend(V6);
+ testGetLeases6Hostname();
+}
+
+/// @brief This test checks that all IPv6 leases with a specified hostname are returned.
+TEST_F(MemfileLeaseMgrTest, getLeases6HostnameMultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLeases6Hostname();
+}
+
+/// @brief This test adds 3 leases and verifies fetch by DUID.
+/// Verifies retrieval of non existant DUID fails
+TEST_F(MemfileLeaseMgrTest, getLeases6Duid) {
+ startBackend(V6);
+ testGetLeases6Duid();
+}
+
+/// @brief This test adds 3 leases and verifies fetch by DUID.
+TEST_F(MemfileLeaseMgrTest, getLeases6DuidMultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLeases6Duid();
+}
+
+/// @brief This test checks that all IPv6 leases are returned.
+TEST_F(MemfileLeaseMgrTest, getLeases6) {
+ startBackend(V6);
+ testGetLeases6();
+}
+
+/// @brief This test checks that all IPv6 leases are returned.
+TEST_F(MemfileLeaseMgrTest, getLeases6MultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLeases6();
+}
+
+/// @brief Test that a range of IPv6 leases is returned with paging.
+TEST_F(MemfileLeaseMgrTest, getLeases6Paged) {
+ startBackend(V6);
+ testGetLeases6Paged();
+}
+
+/// @brief Test that a range of IPv6 leases is returned with paging.
+TEST_F(MemfileLeaseMgrTest, getLeases6PagedMultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLeases6Paged();
+}
+
+/// @brief Basic Lease6 Checks
+///
+/// Checks that the addLease, getLease6 (by address) and deleteLease (with an
+/// IPv6 address) works.
+TEST_F(MemfileLeaseMgrTest, basicLease6) {
+ startBackend(V6);
+ testBasicLease6();
+}
+
+/// @brief Basic Lease6 Checks
+TEST_F(MemfileLeaseMgrTest, basicLease6MultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testBasicLease6();
+}
+
+/// @brief Check GetLease6 methods - access by DUID/IAID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// a combination of DUID and IAID.
+/// @todo: test disabled, because Memfile_LeaseMgr::getLeases6(Lease::Type,
+/// const DUID& duid, uint32_t iaid) const is not implemented yet.
+TEST_F(MemfileLeaseMgrTest, getLeases6DuidIaid) {
+ startBackend(V6);
+ testGetLeases6DuidIaid();
+}
+
+/// @brief Check GetLease6 methods - access by DUID/IAID
+TEST_F(MemfileLeaseMgrTest, getLeases6DuidIaidMultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLeases6DuidIaid();
+}
+
+/// @brief Check that the system can cope with a DUID of allowed size.
+TEST_F(MemfileLeaseMgrTest, getLeases6DuidSize) {
+ startBackend(V6);
+ testGetLeases6DuidSize();
+}
+
+/// @brief Check that the system can cope with a DUID of allowed size.
+TEST_F(MemfileLeaseMgrTest, getLeases6DuidSizeMultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLeases6DuidSize();
+}
+
+/// @brief Check that the expired DHCPv4 leases can be retrieved.
+///
+/// This test adds a number of leases to the lease database and marks
+/// some of them as expired. Then it queries for expired leases and checks
+/// whether only expired leases are returned, and that they are returned in
+/// the order from most to least expired. It also checks that the lease
+/// which is marked as 'reclaimed' is not returned.
+TEST_F(MemfileLeaseMgrTest, getExpiredLeases4) {
+ startBackend(V4);
+ testGetExpiredLeases4();
+}
+
+/// @brief Check that the expired DHCPv4 leases can be retrieved.
+TEST_F(MemfileLeaseMgrTest, getExpiredLeases4MultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetExpiredLeases4();
+}
+
+/// @brief Check that the expired DHCPv6 leases can be retrieved.
+///
+/// This test adds a number of leases to the lease database and marks
+/// some of them as expired. Then it queries for expired leases and checks
+/// whether only expired leases are returned, and that they are returned in
+/// the order from most to least expired. It also checks that the lease
+/// which is marked as 'reclaimed' is not returned.
+TEST_F(MemfileLeaseMgrTest, getExpiredLeases6) {
+ startBackend(V6);
+ testGetExpiredLeases6();
+}
+
+/// @brief Check that the expired DHCPv6 leases can be retrieved.
+TEST_F(MemfileLeaseMgrTest, getExpiredLeases6MultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetExpiredLeases6();
+}
+
+/// @brief Check that expired reclaimed DHCPv6 leases are removed.
+TEST_F(MemfileLeaseMgrTest, deleteExpiredReclaimedLeases6) {
+ startBackend(V6);
+ testDeleteExpiredReclaimedLeases6();
+}
+
+/// @brief Check that expired reclaimed DHCPv6 leases are removed.
+TEST_F(MemfileLeaseMgrTest, deleteExpiredReclaimedLeases6MultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testDeleteExpiredReclaimedLeases6();
+}
+
+/// @brief Check that expired reclaimed DHCPv4 leases are removed.
+TEST_F(MemfileLeaseMgrTest, deleteExpiredReclaimedLeases4) {
+ startBackend(V4);
+ testDeleteExpiredReclaimedLeases4();
+}
+
+/// @brief Check that expired reclaimed DHCPv4 leases are removed.
+TEST_F(MemfileLeaseMgrTest, deleteExpiredReclaimedLeases4MultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testDeleteExpiredReclaimedLeases4();
+}
+
+/// @brief Check that getLease6 methods discriminate by lease type.
+///
+/// Adds six leases, two per lease type all with the same duid and iad but
+/// with alternating subnet_ids.
+/// It then verifies that all of getLeases6() method variants correctly
+/// discriminate between the leases based on lease type alone.
+/// @todo: Disabled, because type parameter in Memfile_LeaseMgr::getLease6
+/// (Lease::Type, const isc::asiolink::IOAddress& addr) const is not used.
+TEST_F(MemfileLeaseMgrTest, lease6LeaseTypeCheck) {
+ startBackend(V6);
+ testLease6LeaseTypeCheck();
+}
+
+/// @brief Check that getLease6 methods discriminate by lease type.
+TEST_F(MemfileLeaseMgrTest, lease6LeaseTypeCheckMultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testLease6LeaseTypeCheck();
+}
+
+/// @brief Check GetLease6 methods - access by DUID/IAID/SubnetID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// a combination of DIUID and IAID.
+TEST_F(MemfileLeaseMgrTest, getLease6DuidIaidSubnetId) {
+ startBackend(V6);
+ testGetLease6DuidIaidSubnetId();
+}
+
+/// @brief Check GetLease6 methods - access by DUID/IAID/SubnetID
+TEST_F(MemfileLeaseMgrTest, getLease6DuidIaidSubnetIdMultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLease6DuidIaidSubnetId();
+}
+
+/// @brief Checks that getLease6(type, duid, iaid, subnet-id) works with different
+/// DUID sizes
+TEST_F(MemfileLeaseMgrTest, getLease6DuidIaidSubnetIdSize) {
+ startBackend(V6);
+ testGetLease6DuidIaidSubnetIdSize();
+}
+
+/// @brief Checks that getLease6(type, duid, iaid, subnet-id) works with different
+/// DUID sizes
+TEST_F(MemfileLeaseMgrTest, getLease6DuidIaidSubnetIdSizeMultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetLease6DuidIaidSubnetIdSize();
+}
+
+/// @brief Lease4 update tests
+///
+/// Checks that we are able to update a lease in the database.
+/// @todo: Disabled, because memfile does not throw when lease is updated.
+/// We should reconsider if lease{4,6} structures should have a limit
+/// implemented in them.
+TEST_F(MemfileLeaseMgrTest, DISABLED_updateLease4) {
+ startBackend(V4);
+ testUpdateLease4();
+}
+
+/// @brief Lease4 update tests
+TEST_F(MemfileLeaseMgrTest, DISABLED_updateLease4MultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testUpdateLease4();
+}
+
+/// @brief Lease6 update tests
+///
+/// Checks that we are able to update a lease in the database.
+/// @todo: Disabled, because memfile does not throw when lease is updated.
+/// We should reconsider if lease{4,6} structures should have a limit
+/// implemented in them.
+TEST_F(MemfileLeaseMgrTest, DISABLED_updateLease6) {
+ startBackend(V6);
+ testUpdateLease6();
+}
+
+/// @brief Lease6 update tests
+TEST_F(MemfileLeaseMgrTest, DISABLED_updateLease6MultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testUpdateLease6();
+}
+
+/// @brief DHCPv4 Lease recreation tests
+///
+/// Checks that the lease can be created, deleted and recreated with
+/// different parameters. It also checks that the re-created lease is
+/// correctly stored in the lease database.
+TEST_F(MemfileLeaseMgrTest, testRecreateLease4) {
+ startBackend(V4);
+ testRecreateLease4();
+}
+
+/// @brief DHCPv4 Lease recreation tests
+TEST_F(MemfileLeaseMgrTest, testRecreateLease4MultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testRecreateLease4();
+}
+
+/// @brief DHCPv6 Lease recreation tests
+///
+/// Checks that the lease can be created, deleted and recreated with
+/// different parameters. It also checks that the re-created lease is
+/// correctly stored in the lease database.
+TEST_F(MemfileLeaseMgrTest, testRecreateLease6) {
+ startBackend(V6);
+ testRecreateLease6();
+}
+
+/// @brief DHCPv6 Lease recreation tests
+TEST_F(MemfileLeaseMgrTest, testRecreateLease6MultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testRecreateLease6();
+}
+
+// The following tests are not applicable for memfile. When adding
+// new tests to the list here, make sure to provide brief explanation
+// why they are not applicable:
+//
+// testGetLease4HWAddrSubnetIdSize() - memfile just keeps Lease structure
+// and does not do any checks of HWAddr content
+
+/// @brief Checks that null DUID is not allowed.
+/// Test is disabled as Memfile does not currently defend against a null DUID.
+TEST_F(MemfileLeaseMgrTest, DISABLED_nullDuid) {
+ // Create leases, although we need only one.
+ vector<Lease6Ptr> leases = createLeases6();
+
+ leases[1]->duid_.reset();
+ ASSERT_THROW(lmptr_->addLease(leases[1]), DbOperationError);
+}
+
+/// @brief Checks that null DUID is not allowed.
+TEST_F(MemfileLeaseMgrTest, DISABLED_nullDuidMultiThread) {
+ MultiThreadingMgr::instance().setMode(true);
+ // Create leases, although we need only one.
+ vector<Lease6Ptr> leases = createLeases6();
+
+ leases[1]->duid_.reset();
+ ASSERT_THROW(lmptr_->addLease(leases[1]), DbOperationError);
+}
+
+/// @brief Tests whether memfile can store and retrieve hardware addresses
+TEST_F(MemfileLeaseMgrTest, testLease6Mac) {
+ startBackend(V6);
+ testLease6MAC();
+}
+
+/// @brief Tests whether memfile can store and retrieve hardware addresses
+TEST_F(MemfileLeaseMgrTest, testLease6MacMultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testLease6MAC();
+}
+
+/// @brief Check that memfile reports version correctly.
+TEST_F(MemfileLeaseMgrTest, versionCheck) {
+ // Check that V4 backend reports versions correctly.
+ startBackend(V4);
+ testVersion(Memfile_LeaseMgr::MAJOR_VERSION_V4,
+ Memfile_LeaseMgr::MINOR_VERSION_V4);
+ LeaseMgrFactory::destroy();
+
+ // Check that V6 backends reports them ok, too.
+ startBackend(V6);
+ testVersion(Memfile_LeaseMgr::MAJOR_VERSION_V6,
+ Memfile_LeaseMgr::MINOR_VERSION_V6);
+ LeaseMgrFactory::destroy();
+}
+
+/// @brief Checks that declined IPv4 leases can be returned correctly.
+TEST_F(MemfileLeaseMgrTest, getDeclined4) {
+ startBackend(V4);
+ testGetDeclinedLeases4();
+}
+
+/// @brief Checks that declined IPv4 leases can be returned correctly.
+TEST_F(MemfileLeaseMgrTest, getDeclined4MultiThread) {
+ startBackend(V4);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetDeclinedLeases4();
+}
+
+/// @brief Checks that declined IPv6 leases can be returned correctly.
+TEST_F(MemfileLeaseMgrTest, getDeclined6) {
+ startBackend(V6);
+ testGetDeclinedLeases6();
+}
+
+/// @brief Checks that declined IPv6 leases can be returned correctly.
+TEST_F(MemfileLeaseMgrTest, getDeclined6MultiThread) {
+ startBackend(V6);
+ MultiThreadingMgr::instance().setMode(true);
+ testGetDeclinedLeases6();
+}
+
+/// @brief This test checks that the backend reads DHCPv4 lease data from multiple
+/// files.
+TEST_F(MemfileLeaseMgrTest, load4MultipleLeaseFiles) {
+ LeaseFileIO io2(getLeaseFilePath("leasefile4_0.csv.2"));
+ io2.writeFile("address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname,state,user_context\n"
+ "192.0.2.2,02:02:02:02:02:02,,200,200,8,1,1,,1,\n"
+ "192.0.2.11,bb:bb:bb:bb:bb:bb,,200,200,8,1,1,,1,\n");
+
+ LeaseFileIO io1(getLeaseFilePath("leasefile4_0.csv.1"));
+ io1.writeFile("address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname,state,user_context\n"
+ "192.0.2.1,01:01:01:01:01:01,,200,200,8,1,1,,1,\n"
+ "192.0.2.11,bb:bb:bb:bb:bb:bb,,200,400,8,1,1,,1,\n"
+ "192.0.2.12,cc:cc:cc:cc:cc:cc,,200,200,8,1,1,,1,\n");
+
+ LeaseFileIO io(getLeaseFilePath("leasefile4_0.csv"));
+ io.writeFile("address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname,state,user_context\n"
+ "192.0.2.10,0a:0a:0a:0a:0a:0a,,200,200,8,1,1,,1,\n"
+ "192.0.2.12,cc:cc:cc:cc:cc:cc,,200,400,8,1,1,,1,\n");
+
+ startBackend(V4);
+
+ // This lease only exists in the second file and the cltt should
+ // be 0.
+ Lease4Ptr lease = lmptr_->getLease4(IOAddress("192.0.2.1"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+
+ // This lease only exists in the first file and the cltt should
+ // be 0.
+ lease = lmptr_->getLease4(IOAddress("192.0.2.2"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+
+ // This lease only exists in the third file and the cltt should
+ // be 0.
+ lease = lmptr_->getLease4(IOAddress("192.0.2.10"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+
+ // This lease exists in the first and second file and the cltt
+ // should be calculated using the expiration time and the
+ // valid lifetime from the second file.
+ lease = lmptr_->getLease4(IOAddress("192.0.2.11"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(200, lease->cltt_);
+
+ // This lease exists in the second and third file and the cltt
+ // should be calculated using the expiration time and the
+ // valid lifetime from the third file.
+ lease = lmptr_->getLease4(IOAddress("192.0.2.12"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(200, lease->cltt_);
+}
+
+/// @brief This test checks that the lease database backend loads the file with
+/// the .completed postfix instead of files with postfixes .1 and .2 if
+/// the file with .completed postfix exists.
+TEST_F(MemfileLeaseMgrTest, load4CompletedFile) {
+ LeaseFileIO io2(getLeaseFilePath("leasefile4_0.csv.2"));
+ io2.writeFile("address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname,state,user_context\n"
+ "192.0.2.2,02:02:02:02:02:02,,200,200,8,1,1,,1,\n"
+ "192.0.2.11,bb:bb:bb:bb:bb:bb,,200,200,8,1,1,,1,\n");
+
+ LeaseFileIO io1(getLeaseFilePath("leasefile4_0.csv.1"));
+ io1.writeFile("address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname,state,user_context\n"
+ "192.0.2.1,01:01:01:01:01:01,,200,200,8,1,1,,1,\n"
+ "192.0.2.11,bb:bb:bb:bb:bb:bb,,200,400,8,1,1,,1,\n"
+ "192.0.2.12,cc:cc:cc:cc:cc:cc,,200,200,8,1,1,,1,\n");
+
+ LeaseFileIO io(getLeaseFilePath("leasefile4_0.csv"));
+ io.writeFile("address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname,state,user_context\n"
+ "192.0.2.10,0a:0a:0a:0a:0a:0a,,200,200,8,1,1,,1,\n"
+ "192.0.2.12,cc:cc:cc:cc:cc:cc,,200,400,8,1,1,,1,\n");
+
+ LeaseFileIO ioc(getLeaseFilePath("leasefile4_0.csv.completed"));
+ ioc.writeFile("address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname,state,user_context\n"
+ "192.0.2.13,ff:ff:ff:ff:ff:ff,,200,200,8,1,1,,1,\n");
+
+ startBackend(V4);
+
+ // We expect that this file only holds leases that belong to the
+ // lease file or to the file with .completed postfix.
+ Lease4Ptr lease = lmptr_->getLease4(IOAddress("192.0.2.10"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+
+ lease = lmptr_->getLease4(IOAddress("192.0.2.12"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(200, lease->cltt_);
+
+ // This lease is in the .completed file.
+ lease = lmptr_->getLease4(IOAddress("192.0.2.13"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+
+ // Leases from the .1 and .2 files should not be loaded.
+ EXPECT_FALSE(lmptr_->getLease4(IOAddress("192.0.2.11")));
+ EXPECT_FALSE(lmptr_->getLease4(IOAddress("192.0.2.1")));
+}
+
+/// @brief This test checks that backend constructor refuses to load leases from the
+/// lease files if the LFC is in progress.
+TEST_F(MemfileLeaseMgrTest, load4LFCInProgress) {
+ // Create the backend configuration.
+ DatabaseConnection::ParameterMap pmap;
+ pmap["type"] = "memfile";
+ pmap["universe"] = "4";
+ pmap["name"] = getLeaseFilePath("leasefile4_0.csv");
+ pmap["lfc-interval"] = "1";
+
+ // Create a pid file holding the PID of the current process. Choosing the
+ // pid of the current process guarantees that when the backend starts up
+ // the process is alive.
+ PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(pmap["name"], Memfile_LeaseMgr::FILE_PID));
+ pid_file.write();
+
+ // There is a pid file and the process which pid is in the file is
+ // running, so the backend should refuse to start.
+ boost::scoped_ptr<NakedMemfileLeaseMgr> lease_mgr;
+ ASSERT_THROW(lease_mgr.reset(new NakedMemfileLeaseMgr(pmap)),
+ DbOpenError);
+
+ // Remove the pid file, and retry. The backend should be created.
+ pid_file.deleteFile();
+ ASSERT_NO_THROW(lease_mgr.reset(new NakedMemfileLeaseMgr(pmap)));
+}
+
+/// @brief This test checks that the backend reads DHCPv6 lease data from multiple
+/// files.
+TEST_F(MemfileLeaseMgrTest, load6MultipleLeaseFiles) {
+ LeaseFileIO io2(getLeaseFilePath("leasefile6_0.csv.2"));
+ io2.writeFile("address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context,hwtype,hwaddr_source\n"
+ "2001:db8:1::1,01:01:01:01:01:01:01:01:01:01:01:01:01,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::2,02:02:02:02:02:02:02:02:02:02:02:02:02,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n");
+
+ LeaseFileIO io1(getLeaseFilePath("leasefile6_0.csv.1"));
+ io1.writeFile("address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context,hwtype,hwaddr_source\n"
+ "2001:db8:1::3,03:03:03:03:03:03:03:03:03:03:03:03:03,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::2,02:02:02:02:02:02:02:02:02:02:02:02:02,"
+ "300,800,8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::4,04:04:04:04:04:04:04:04:04:04:04:04:04,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n");
+
+ LeaseFileIO io(getLeaseFilePath("leasefile6_0.csv"));
+ io.writeFile("address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context,hwtype,hwaddr_source\n"
+ "2001:db8:1::4,04:04:04:04:04:04:04:04:04:04:04:04:04,"
+ "400,1000,8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::5,05:05:05:05:05:05:05:05:05:05:05:05:05,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n");
+
+ startBackend(V6);
+
+ // This lease only exists in the first file and the cltt should be 0.
+ Lease6Ptr lease = lmptr_->getLease6(Lease::TYPE_NA,
+ IOAddress("2001:db8:1::1"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+
+ // This lease exists in the first and second file and the cltt should
+ // be calculated using the expiration time and the valid lifetime
+ // from the second file.
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::2"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(500, lease->cltt_);
+
+ // This lease only exists in the second file and the cltt should be 0.
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::3"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+
+ // This lease exists in the second and third file and the cltt should
+ // be calculated using the expiration time and the valid lifetime
+ // from the third file.
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::4"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(600, lease->cltt_);
+
+ // This lease only exists in the third file and the cltt should be 0.
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::5"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+}
+
+/// @brief This test checks that the backend reads DHCPv6 lease data from the
+/// leasefile without the postfix and the file with a .1 postfix when
+/// the file with the .2 postfix is missing.
+TEST_F(MemfileLeaseMgrTest, load6MultipleNoSecondFile) {
+ LeaseFileIO io1(getLeaseFilePath("leasefile6_0.csv.1"));
+ io1.writeFile("address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context,hwtype,hwaddr_source\n"
+ "2001:db8:1::3,03:03:03:03:03:03:03:03:03:03:03:03:03,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::2,02:02:02:02:02:02:02:02:02:02:02:02:02,"
+ "300,800,8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::4,04:04:04:04:04:04:04:04:04:04:04:04:04,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n");
+
+ LeaseFileIO io(getLeaseFilePath("leasefile6_0.csv"));
+ io.writeFile("address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context,hwtype,hwaddr_source\n"
+ "2001:db8:1::4,04:04:04:04:04:04:04:04:04:04:04:04:04,"
+ "400,1000,8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::5,05:05:05:05:05:05:05:05:05:05:05:05:05,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n");
+
+ startBackend(V6);
+
+ // Check that leases from the leasefile6_0 and leasefile6_0.1 have
+ // been loaded.
+ Lease6Ptr lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::2"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(500, lease->cltt_);
+
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::3"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::4"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(600, lease->cltt_);
+
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::5"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+
+ // Make sure that a lease which is not in those files is not loaded.
+ EXPECT_FALSE(lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1")));
+}
+
+/// @brief This test checks that the backend reads DHCPv6 lease data from the
+/// leasefile without the postfix and the file with a .2 postfix when
+/// the file with the .1 postfix is missing.
+TEST_F(MemfileLeaseMgrTest, load6MultipleNoFirstFile) {
+ LeaseFileIO io2(getLeaseFilePath("leasefile6_0.csv.2"));
+ io2.writeFile("address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context,hwtype,hwaddr_source\n"
+ "2001:db8:1::1,01:01:01:01:01:01:01:01:01:01:01:01:01,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::2,02:02:02:02:02:02:02:02:02:02:02:02:02,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n");
+
+ LeaseFileIO io(getLeaseFilePath("leasefile6_0.csv"));
+ io.writeFile("address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context,hwtype,hwaddr_source\n"
+ "2001:db8:1::4,04:04:04:04:04:04:04:04:04:04:04:04:04,"
+ "400,1000,8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::5,05:05:05:05:05:05:05:05:05:05:05:05:05,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n");
+
+ startBackend(V6);
+
+ // Verify that leases which belong to the leasefile6_0.csv and
+ // leasefile6_0.2 are loaded.
+ Lease6Ptr lease = lmptr_->getLease6(Lease::TYPE_NA,
+ IOAddress("2001:db8:1::1"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::2"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::4"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(600, lease->cltt_);
+
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::5"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+
+ // A lease which doesn't belong to these files should not be loaded.
+ EXPECT_FALSE(lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::3")));
+}
+
+/// @brief This test checks that the lease database backend loads the file with
+/// the .completed postfix instead of files with postfixes .1 and .2 if
+/// the file with .completed postfix exists.
+TEST_F(MemfileLeaseMgrTest, load6CompletedFile) {
+ LeaseFileIO io2(getLeaseFilePath("leasefile6_0.csv.2"));
+ io2.writeFile("address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context,hwtype,hwaddr_source\n"
+ "2001:db8:1::1,01:01:01:01:01:01:01:01:01:01:01:01:01,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::2,02:02:02:02:02:02:02:02:02:02:02:02:02,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n");
+
+ LeaseFileIO io1(getLeaseFilePath("leasefile6_0.csv.1"));
+ io1.writeFile("address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context,hwtype,hwaddr_source\n"
+ "2001:db8:1::3,03:03:03:03:03:03:03:03:03:03:03:03:03,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::2,02:02:02:02:02:02:02:02:02:02:02:02:02,"
+ "300,800,8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::4,04:04:04:04:04:04:04:04:04:04:04:04:04,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n");
+
+ LeaseFileIO io(getLeaseFilePath("leasefile6_0.csv"));
+ io.writeFile("address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context,hwtype,hwaddr_source\n"
+ "2001:db8:1::4,04:04:04:04:04:04:04:04:04:04:04:04:04,"
+ "400,1000,8,100,0,7,0,1,1,,,1,,,\n"
+ "2001:db8:1::5,05:05:05:05:05:05:05:05:05:05:05:05:05,"
+ "200,200,8,100,0,7,0,1,1,,,1,,,\n");
+
+ LeaseFileIO ioc(getLeaseFilePath("leasefile6_0.csv.completed"));
+ ioc.writeFile("address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context,hwtype,hwaddr_source\n"
+ "2001:db8:1::125,ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff,"
+ "400,1000,8,100,0,7,0,1,1,,,1,,,\n");
+
+ startBackend(V6);
+
+ // We expect that this file only holds leases that belong to the
+ // lease file or to the file with .completed postfix.
+ Lease6Ptr lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::4"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(600, lease->cltt_);
+
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::5"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(0, lease->cltt_);
+
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::125"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(600, lease->cltt_);
+
+ // Leases from the .1 and .2 files should not be loaded.
+ EXPECT_FALSE(lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1")));
+ EXPECT_FALSE(lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::2")));
+ EXPECT_FALSE(lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::3")));
+}
+
+/// @brief This test checks that backend constructor refuses to load leases from the
+/// lease files if the LFC is in progress.
+TEST_F(MemfileLeaseMgrTest, load6LFCInProgress) {
+ // Create the backend configuration.
+ DatabaseConnection::ParameterMap pmap;
+ pmap["type"] = "memfile";
+ pmap["universe"] = "6";
+ pmap["name"] = getLeaseFilePath("leasefile6_0.csv");
+ pmap["lfc-interval"] = "1";
+
+ // Create a pid file holding the PID of the current process. Choosing the
+ // pid of the current process guarantees that when the backend starts up
+ // the process is alive.
+ PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(pmap["name"], Memfile_LeaseMgr::FILE_PID));
+ pid_file.write();
+
+ // There is a pid file and the process which pid is in the file is
+ // running, so the backend should refuse to start.
+ boost::scoped_ptr<NakedMemfileLeaseMgr> lease_mgr;
+ ASSERT_THROW(lease_mgr.reset(new NakedMemfileLeaseMgr(pmap)),
+ DbOpenError);
+
+ // Remove the pid file, and retry. The backend should be created.
+ pid_file.deleteFile();
+ ASSERT_NO_THROW(lease_mgr.reset(new NakedMemfileLeaseMgr(pmap)));
+}
+
+/// @brief Verifies that LFC is automatically run during MemfileLeasemMgr construction
+/// when the lease file(s) being loaded need to be upgraded.
+TEST_F(MemfileLeaseMgrTest, leaseUpgrade4) {
+ // Create header strings for each schema
+ std::string header_1_0 =
+ "address,hwaddr,client_id,valid_lifetime,expire,"
+ "subnet_id,fqdn_fwd,fqdn_rev,hostname\n";
+
+ std::string header_2_0 =
+ "address,hwaddr,client_id,valid_lifetime,expire,"
+ "subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context\n";
+
+ // Create 1.0 Schema current lease file with two entries for
+ // the same lease
+ std::string current_file_contents = header_1_0 +
+ "192.0.2.2,02:02:02:02:02:02,,200,200,8,1,1,\n"
+ "192.0.2.2,02:02:02:02:02:02,,200,800,8,1,1,\n";
+ LeaseFileIO current_file(getLeaseFilePath("leasefile4_0.csv"));
+ current_file.writeFile(current_file_contents);
+
+ // Create 1.0 Schema previous lease file, with two entries for
+ // a another lease
+ std::string previous_file_contents = header_1_0 +
+ "192.0.2.3,03:03:03:03:03:03,,200,200,8,1,1,\n"
+ "192.0.2.3,03:03:03:03:03:03,,200,800,8,1,1,\n";
+ LeaseFileIO previous_file(getLeaseFilePath("leasefile4_0.csv.2"));
+ previous_file.writeFile(previous_file_contents);
+
+ // Create the backend.
+ DatabaseConnection::ParameterMap pmap;
+ pmap["type"] = "memfile";
+ pmap["universe"] = "4";
+ pmap["name"] = getLeaseFilePath("leasefile4_0.csv");
+ pmap["lfc-interval"] = "0";
+ boost::scoped_ptr<NakedMemfileLeaseMgr> lease_mgr(new NakedMemfileLeaseMgr(pmap));
+
+ // Since lease files are loaded during lease manager
+ // constructor, LFC should get launched automatically.
+ // The new lease file should be 2.0 schema and have no entries
+ ASSERT_TRUE(current_file.exists());
+ EXPECT_EQ(header_2_0, current_file.readFile());
+
+ // Wait for the LFC process to complete and
+ // make sure it has returned an exit status of 0.
+ ASSERT_TRUE(waitForProcess(*lease_mgr, 2));
+
+ ASSERT_EQ(0, lease_mgr->getLFCExitStatus())
+ << "Executing the LFC process failed: make sure that"
+ " the kea-lfc program has been compiled.";
+
+ // The LFC should have created a 2.0 schema completion file with the
+ // one entry for each lease and moved it to leasefile4_0.csv.2
+ LeaseFileIO input_file(getLeaseFilePath("leasefile4_0.csv.2"), false);
+ ASSERT_TRUE(input_file.exists());
+
+ // Verify cleaned, converted contents
+ std::string result_file_contents = header_2_0 +
+ "192.0.2.2,02:02:02:02:02:02,,200,800,8,1,1,,0,\n"
+ "192.0.2.3,03:03:03:03:03:03,,200,800,8,1,1,,0,\n";
+ EXPECT_EQ(result_file_contents, input_file.readFile());
+}
+
+/// @brief Verifies that LFC is automatically run during MemfileLeasemMgr construction
+/// when the lease file(s) being loaded need to be upgraded.
+TEST_F(MemfileLeaseMgrTest, leaseUpgrade6) {
+ // Create header strings for all schemas.
+ std::string header_1_0 =
+ "address,duid,valid_lifetime,expire,subnet_id,"
+ "pref_lifetime,lease_type,iaid,prefix_len,fqdn_fwd,"
+ "fqdn_rev,hostname\n";
+
+ std::string header_2_0 =
+ "address,duid,valid_lifetime,expire,subnet_id,"
+ "pref_lifetime,lease_type,iaid,prefix_len,fqdn_fwd,"
+ "fqdn_rev,hostname,hwaddr\n";
+
+ std::string header_3_0 =
+ "address,duid,valid_lifetime,expire,subnet_id,"
+ "pref_lifetime,lease_type,iaid,prefix_len,fqdn_fwd,"
+ "fqdn_rev,hostname,hwaddr,state,user_context\n";
+
+ std::string header_4_0 =
+ "address,duid,valid_lifetime,expire,subnet_id,"
+ "pref_lifetime,lease_type,iaid,prefix_len,fqdn_fwd,"
+ "fqdn_rev,hostname,hwaddr,state,user_context,"
+ "hwtype,hwaddr_source\n";
+
+ // The current lease file is schema 1.0 and has two entries for
+ // the same lease
+ std::string current_file_contents = header_1_0 +
+ "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,200,200,"
+ "8,100,0,7,0,1,1,,\n"
+ "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,200,800,"
+ "8,100,0,7,0,1,1,,\n";
+ LeaseFileIO current_file(getLeaseFilePath("leasefile6_0.csv"));
+ current_file.writeFile(current_file_contents);
+
+ // The previous lease file is schema 2.0 and has two entries for
+ // a different lease
+ std::string previous_file_contents = header_2_0 +
+ "2001:db8:1::2,01:01:01:01:01:01:01:01:01:01:01:01:01,200,200,"
+ "8,100,0,7,0,1,1,,11:22:33:44:55\n"
+ "2001:db8:1::2,01:01:01:01:01:01:01:01:01:01:01:01:01,200,800,"
+ "8,100,0,7,0,1,1,,11:22:33:44:55\n";
+ LeaseFileIO previous_file(getLeaseFilePath("leasefile6_0.csv.2"));
+ previous_file.writeFile(previous_file_contents);
+
+ // Create the backend.
+ DatabaseConnection::ParameterMap pmap;
+ pmap["type"] = "memfile";
+ pmap["universe"] = "6";
+ pmap["name"] = getLeaseFilePath("leasefile6_0.csv");
+ pmap["lfc-interval"] = "0";
+ boost::scoped_ptr<NakedMemfileLeaseMgr> lease_mgr(new NakedMemfileLeaseMgr(pmap));
+
+ // Since lease files are loaded during lease manager
+ // constructor, LFC should get launched automatically.
+ // The new lease file should been 4.0 and contain no leases.
+ ASSERT_TRUE(current_file.exists());
+ EXPECT_EQ(header_4_0, current_file.readFile());
+
+ // Wait for the LFC process to complete and
+ // make sure it has returned an exit status of 0.
+ ASSERT_TRUE(waitForProcess(*lease_mgr, 2));
+
+ ASSERT_EQ(0, lease_mgr->getLFCExitStatus())
+ << "Executing the LFC process failed: make sure that"
+ " the kea-lfc program has been compiled.";
+
+ // The LFC should have created a 4.0 schema cleaned file with one entry
+ // for each lease as leasefile6_0.csv.2
+ LeaseFileIO input_file(getLeaseFilePath("leasefile6_0.csv.2"), false);
+ ASSERT_TRUE(input_file.exists());
+
+ // Verify cleaned, converted contents
+ std::string result_file_contents = header_4_0 +
+ "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,200,800,"
+ "8,100,0,7,0,1,1,,,0,,,\n"
+ "2001:db8:1::2,01:01:01:01:01:01:01:01:01:01:01:01:01,200,800,"
+ "8,100,0,7,0,1,1,,11:22:33:44:55,0,,1,0\n";
+ EXPECT_EQ(result_file_contents, input_file.readFile());
+}
+
+/// @brief This test verifies that the indexes of the container holding
+/// DHCPv4 leases are updated correctly when a lease is updated.
+TEST_F(MemfileLeaseMgrTest, lease4ContainerIndexUpdate) {
+
+ const uint32_t seed = 12345678; // Used to initialize the random generator
+ const uint32_t leases_cnt = 100; // Number of leases generated per round.
+ const uint32_t updates_cnt = 5; // Number of times existing leases are updated
+
+ const string leasefile(getLeaseFilePath("leasefile4_0.csv"));
+
+ // Parameters for the lease file. Make sure the leases are persistent, so they
+ // are written to disk.
+ DatabaseConnection::ParameterMap pmap;
+ pmap["universe"] = "4";
+ pmap["name"] = leasefile;
+ pmap["persist"] = "true";
+ pmap["lfc-interval"] = "0";
+
+ srand(seed);
+
+ IOAddress addr("10.0.0.1"); // Let's generate leases sequentially
+
+ // Recreate Memfile_LeaseMgr.
+ LeaseMgrFactory::destroy();
+ ASSERT_NO_THROW(lmptr_ = new Memfile_LeaseMgr(pmap));
+
+ // We will store addresses here, so it will be easier to randomly
+ // pick a lease.
+ std::vector<IOAddress> lease_addresses;
+
+ // Generate random leases. We remember their addresses in
+ // lease_addresses.
+ for (uint32_t i = 0; i < leases_cnt; ++i) {
+ Lease4Ptr lease = initiateRandomLease4(addr);
+ lease_addresses.push_back(addr);
+ ASSERT_NO_THROW(lmptr_->addLease(lease));
+ addr = IOAddress::increase(addr);
+ }
+
+ // Check that we inserted correct number of leases.
+ ASSERT_EQ(leases_cnt, lease_addresses.size());
+
+ // Now, conduct updates. We call initiateRandomLease4(), so most
+ // of the fields are randomly changed. The only constant field
+ // is the address.
+ for (uint32_t i = 0; i < updates_cnt; ++i) {
+ uint32_t offset = random() % lease_addresses.size();
+ Lease4Ptr existing(lmptr_->getLease4(lease_addresses[offset]));
+ Lease4Ptr updated(initiateRandomLease4(lease_addresses[offset]));
+
+ // Update a lease with new data but preserve lease address.
+ // This update should also cause lease container indexes to
+ // be updated.
+ ASSERT_NO_THROW(lmptr_->updateLease4(updated))
+ << "Attempt " << i << " out of " << updates_cnt
+ << ":Failed to update lease for address "
+ << lease_addresses[offset];
+ }
+
+ // Re-create lease manager to cause it to reload leases
+ // from a lease file. We want to make sure that lease
+ // container is rebuilt correctly and the indexes are
+ // consistent with lease information held.
+ ASSERT_NO_THROW({
+ LeaseMgrFactory::destroy();
+ lmptr_ = new Memfile_LeaseMgr(pmap);
+ });
+
+ // Ok, let's check if the leases are really accessible.
+ // First, build an array of leases. Get them by address.
+ // This should work in general, as we haven't updated the addresses.
+ std::vector<Lease4Ptr> leases;
+ for (uint32_t i = 0; i < lease_addresses.size(); ++i) {
+ Lease4Ptr from_mgr = lmptr_->getLease4(lease_addresses[i]);
+ ASSERT_TRUE(from_mgr) << "Lease for address " << lease_addresses[i].toText()
+ << " not found";
+ leases.push_back(from_mgr);
+ }
+
+ ASSERT_EQ(leases_cnt, leases.size());
+
+ // Now do the actual checks.
+ for (uint32_t i = 0; i < leases.size(); ++i) {
+ Lease4Ptr tested = leases[i];
+
+ // Get the lease by different access patterns.
+ // In properly working lease manager all queries should return
+ // exactly the same lease.
+
+ std::string error_desc = " which indicates that the lease indexes were"
+ " not updated correctly when the lease was updated.";
+
+ // Retrieve lease by address.
+ Lease4Ptr lease_by_address = lmptr_->getLease4(tested->addr_);
+ ASSERT_TRUE(lease_by_address)
+ << "Lease " << tested->addr_.toText()
+ << " not found by getLease4(addr)"
+ << error_desc;
+ detailCompareLease(tested, lease_by_address);
+
+ // Retrieve lease by HW address and subnet id.
+ Lease4Ptr lease_by_hwaddr_subnet = lmptr_->getLease4(*tested->hwaddr_,
+ tested->subnet_id_);
+ ASSERT_TRUE(lease_by_hwaddr_subnet)
+ << "Lease " << tested->addr_.toText()
+ << " not found by getLease4(hwaddr, subnet_id)"
+ << error_desc;
+ detailCompareLease(tested, lease_by_hwaddr_subnet);
+
+ // Retrieve lease by client identifier and subnet id.
+ Lease4Ptr lease_by_clientid_subnet = lmptr_->getLease4(*tested->client_id_,
+ tested->subnet_id_);
+ ASSERT_TRUE(lease_by_clientid_subnet)
+ << "Lease " << tested->addr_.toText()
+ << " not found by getLease4(clientid, subnet_id)"
+ << error_desc;
+ detailCompareLease(tested, lease_by_clientid_subnet);
+
+ // Retrieve lease by HW address.
+ Lease4Collection leases_by_hwaddr = lmptr_->getLease4(*tested->hwaddr_);
+ ASSERT_EQ(1, leases_by_hwaddr.size());
+ detailCompareLease(tested, leases_by_hwaddr[0]);
+
+ // Retrieve lease by client identifier.
+ Lease4Collection leases_by_client_id = lmptr_->getLease4(*tested->client_id_);
+ ASSERT_EQ(1, leases_by_client_id.size());
+ detailCompareLease(tested, leases_by_client_id[0]);
+ }
+}
+
+/// @brief This test verifies that the indexes of the container holding
+/// DHCPv4 leases are updated correctly when a lease is updated.
+TEST_F(MemfileLeaseMgrTest, lease6ContainerIndexUpdate) {
+
+ const uint32_t seed = 12345678; // Used to initialize the random generator
+ const uint32_t leases_cnt = 100; // Number of leases generated per round.
+ const uint32_t updates_cnt = 5; // Number of times existing leases are updated
+
+ const string leasefile(getLeaseFilePath("leasefile6_0.csv"));
+
+ // Parameters for the lease file. Make sure the leases are persistent, so they
+ // are written to disk.
+ DatabaseConnection::ParameterMap pmap;
+ pmap["universe"] = "6";
+ pmap["name"] = leasefile;
+ pmap["persist"] = "true";
+ pmap["lfc-interval"] = "0";
+
+ srand(seed);
+
+ IOAddress addr("2001:db8:1::1"); // Let's generate leases sequentially
+
+ // Recreate Memfile_LeaseMgr.
+ LeaseMgrFactory::destroy();
+ ASSERT_NO_THROW(lmptr_ = new Memfile_LeaseMgr(pmap));
+
+ // We will store addresses here, so it will be easier to randomly
+ // pick a lease.
+ std::vector<IOAddress> lease_addresses;
+
+ // Generate random leases. We remember their addresses in
+ // lease_addresses.
+ for (uint32_t i = 0; i < leases_cnt; ++i) {
+ Lease6Ptr lease = initiateRandomLease6(addr);
+ lease_addresses.push_back(addr);
+ ASSERT_NO_THROW(lmptr_->addLease(lease));
+ addr = IOAddress::increase(addr);
+ }
+
+ // Check that we inserted correct number of leases.
+ ASSERT_EQ(leases_cnt, lease_addresses.size());
+
+ // Now, conduct updates. We call initiateRandomLease6(), so most
+ // of the fields are randomly changed. The only constant field
+ // is the address.
+ for (uint32_t i = 0; i < updates_cnt; ++i) {
+ uint32_t offset = random() % lease_addresses.size();
+ Lease6Ptr existing(lmptr_->getLease6(Lease::TYPE_NA,
+ lease_addresses[offset]));
+ Lease6Ptr updated(initiateRandomLease6(lease_addresses[offset]));
+
+ // Update a lease with new data but preserve lease address.
+ // This update should also cause lease container indexes to
+ // be updated.
+ ASSERT_NO_THROW(lmptr_->updateLease6(updated))
+ << "Attempt " << i << " out of " << updates_cnt
+ << ":Failed to update lease for address "
+ << lease_addresses[offset];
+ }
+
+ // Re-create lease manager to cause it to reload leases
+ // from a lease file. We want to make sure that lease
+ // container is rebuilt correctly and the indexes are
+ // consistent with lease information held.
+ ASSERT_NO_THROW({
+ LeaseMgrFactory::destroy();
+ lmptr_ = new Memfile_LeaseMgr(pmap);
+ });
+
+ // Ok, let's check if the leases are really accessible.
+ // First, build an array of leases. Get them by address.
+ // This should work in general, as we haven't updated the addresses.
+ std::vector<Lease6Ptr> leases;
+ for (uint32_t i = 0; i < lease_addresses.size(); ++i) {
+ Lease6Ptr from_mgr = lmptr_->getLease6(Lease::TYPE_NA,
+ lease_addresses[i]);
+ ASSERT_TRUE(from_mgr) << "Lease for address " << lease_addresses[i].toText()
+ << " not found";
+ leases.push_back(from_mgr);
+ }
+
+ ASSERT_EQ(leases_cnt, leases.size());
+
+ // Now do the actual checks.
+ for (uint32_t i = 0; i < leases.size(); ++i) {
+ Lease6Ptr tested = leases[i];
+
+ // Get the lease by different access patterns.
+ // In properly working lease manager all queries should return
+ // exactly the same lease.
+
+ std::string error_desc = " which indicates that the lease indexes were"
+ " not updated correctly when the lease was updated.";
+
+ // Retrieve lease by address.
+ Lease6Ptr lease_by_address = lmptr_->getLease6(Lease::TYPE_NA,
+ tested->addr_);
+ ASSERT_TRUE(lease_by_address)
+ << "Lease " << tested->addr_.toText()
+ << " not found by getLease6(addr)"
+ << error_desc;
+ detailCompareLease(tested, lease_by_address);
+
+ // Retrieve lease by type, DUID, IAID.
+ Lease6Collection leases_by_duid_iaid = lmptr_->getLeases6(tested->type_,
+ *tested->duid_,
+ tested->iaid_);
+ ASSERT_EQ(1, leases_by_duid_iaid.size());
+ ASSERT_TRUE(leases_by_duid_iaid[0])
+ << "Lease " << tested->addr_.toText()
+ << " not found by getLease6(type, duid, iaid)"
+ << error_desc;
+ detailCompareLease(tested, leases_by_duid_iaid[0]);
+
+ // Retrieve lease by type, DUID, IAID, subnet identifier.
+ Lease6Collection leases_by_duid_iaid_subnet =
+ lmptr_->getLeases6(tested->type_, *tested->duid_,
+ tested->iaid_, tested->subnet_id_);
+ ASSERT_EQ(1, leases_by_duid_iaid_subnet.size());
+ ASSERT_TRUE(leases_by_duid_iaid_subnet[0])
+ << "Lease " << tested->addr_.toText()
+ << " not found by getLease6(type, duid, iaid, subnet_id)"
+ << error_desc;
+ detailCompareLease(tested, leases_by_duid_iaid_subnet[0]);
+ }
+}
+
+/// @brief Verifies that IPv4 lease statistics can be recalculated.
+TEST_F(MemfileLeaseMgrTest, recountLeaseStats4) {
+ startBackend(V4);
+ testRecountLeaseStats4();
+}
+
+/// @brief Verifies that IPv6 lease statistics can be recalculated.
+TEST_F(MemfileLeaseMgrTest, recountLeaseStats6) {
+ startBackend(V6);
+ testRecountLeaseStats6();
+}
+
+/// @brief Tests that leases from specific subnet can be removed.
+TEST_F(MemfileLeaseMgrTest, wipeLeases4) {
+ startBackend(V4);
+ testWipeLeases4();
+}
+
+/// @brief Tests that leases from specific subnet can be removed.
+TEST_F(MemfileLeaseMgrTest, wipeLeases6) {
+ startBackend(V6);
+ testWipeLeases6();
+}
+
+/// @brief Tests v4 lease stats query variants.
+TEST_F(MemfileLeaseMgrTest, leaseStatsQuery4) {
+ startBackend(V4);
+ testLeaseStatsQuery4();
+}
+
+/// @brief Tests v6 lease stats query variants.
+TEST_F(MemfileLeaseMgrTest, leaseStatsQuery6) {
+ startBackend(V6);
+ testLeaseStatsQuery6();
+}
+
+/// @brief Tests v4 lease stats to be attributed to the wrong subnet.
+TEST_F(MemfileLeaseMgrTest, leaseStatsQueryAttribution4) {
+ startBackend(V4);
+ testLeaseStatsQueryAttribution4();
+}
+
+/// @brief Tests v6 lease stats to be attributed to the wrong subnet.
+TEST_F(MemfileLeaseMgrTest, leaseStatsQueryAttribution6) {
+ startBackend(V6);
+ testLeaseStatsQueryAttribution6();
+}
+
+TEST_F(MemfileLeaseMgrTest, checkVersion4) {
+ // Create the backend.
+ DatabaseConnection::ParameterMap parameters;
+ parameters["type"] = "memfile";
+ parameters["universe"] = "4";
+ parameters["name"] = getLeaseFilePath("leasefile4_0.csv");
+ std::unique_ptr<NakedMemfileLeaseMgr> lease_mgr;
+ ASSERT_NO_THROW_LOG(lease_mgr.reset(new NakedMemfileLeaseMgr(parameters)));
+
+ // Get the backend version.
+ auto const& backend_version(lease_mgr->getVersion());
+ std::stringstream s;
+ s << backend_version.first << '.' << backend_version.second;
+
+ // Get the CSV version.
+ CSVLeaseFile4 f(getLeaseFilePath("leasefile4_0.csv"));
+
+ // They should match.
+ EXPECT_EQ(s.str(), f.getSchemaVersion());
+
+ // DBVersion too.
+ EXPECT_EQ("Memfile backend " + s.str(),
+ lease_mgr->getDBVersion(Memfile_LeaseMgr::V4));
+}
+
+TEST_F(MemfileLeaseMgrTest, checkVersion6) {
+ // Create the backend.
+ DatabaseConnection::ParameterMap parameters;
+ parameters["type"] = "memfile";
+ parameters["universe"] = "6";
+ parameters["name"] = getLeaseFilePath("leasefile6_0.csv");
+ std::unique_ptr<NakedMemfileLeaseMgr> lease_mgr;
+ ASSERT_NO_THROW_LOG(lease_mgr.reset(new NakedMemfileLeaseMgr(parameters)));
+
+ // Get the backend version.
+ auto const& backend_version(lease_mgr->getVersion());
+ std::stringstream s;
+ s << backend_version.first << '.' << backend_version.second;
+
+ // Get the CSV version.
+ CSVLeaseFile6 f(getLeaseFilePath("leasefile6_0.csv"));
+
+ // They should match.
+ EXPECT_EQ(s.str(), f.getSchemaVersion());
+
+ // DBVersion too.
+ EXPECT_EQ("Memfile backend " + s.str(),
+ lease_mgr->getDBVersion(Memfile_LeaseMgr::V6));
+}
+
+/// @brief Checks that complex user context can be read in v4.
+TEST_F(MemfileLeaseMgrTest, v4UserContext) {
+ // Add some leases to the CSV file.
+ LeaseFileIO io(getLeaseFilePath("leasefile4_0.csv"));
+ io.writeFile(
+ "address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+ "fqdn_fwd,fqdn_rev,hostname,state,user_context\n"
+
+ "192.0.0.1,01:01:01:01:01:01,,100,100,1,1,1,,1,"
+ "{}\n"
+
+ "192.0.0.2,02:02:02:02:02:02,,200,400,1,1,1,,0,"
+ "{ \"comment\": \"this lease is for the kitchen computer\"}\n"
+
+ // The next lines have escaped commas in user context.
+
+ "192.0.0.4,04:04:04:04:04:04,,400,1600,1,1,1,,0,"
+ "{ \"comment\": \"this lease is for the mainframe computer\"&#x2c"
+ " \"comment2\": \"don't release it\" }\n"
+
+ "192.0.0.8,08:08:08:08:08:08,,800,6400,1,1,1,,0,"
+ "{ \"a\": \"b\"&#x2c \"c\": { \"d\": 1&#x2c\"e\": 2 } }\n"
+ );
+
+ startBackend(V4);
+
+ // Check the lease with no key-value pairs in the user context.
+ Lease4Ptr lease(lmptr_->getLease4(IOAddress("192.0.0.1")));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(
+ lease->toText(),
+ "Address: 192.0.0.1\n"
+ "Valid life: 100\n"
+ "Cltt: 0\n"
+ "Hardware addr: 01:01:01:01:01:01\n"
+ "Client id: (none)\n"
+ "Subnet ID: 1\n"
+ "State: declined\n"
+ "User context: { }\n"
+ );
+
+ // Check the lease with one key-value pair in the user context.
+ lease = lmptr_->getLease4(IOAddress("192.0.0.2"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(
+ lease->toText(),
+ "Address: 192.0.0.2\n"
+ "Valid life: 200\n"
+ "Cltt: 200\n"
+ "Hardware addr: 02:02:02:02:02:02\n"
+ "Client id: (none)\n"
+ "Subnet ID: 1\n"
+ "State: default\n"
+ "User context: { \"comment\": \"this lease is for the kitchen computer\" }\n"
+ );
+
+ // Check the lease with two key-value pairs in the user context.
+ lease = lmptr_->getLease4(IOAddress("192.0.0.4"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(
+ lease->toText(),
+ "Address: 192.0.0.4\n"
+ "Valid life: 400\n"
+ "Cltt: 1200\n"
+ "Hardware addr: 04:04:04:04:04:04\n"
+ "Client id: (none)\n"
+ "Subnet ID: 1\n"
+ "State: default\n"
+ "User context: "
+ "{ \"comment\": \"this lease is for the mainframe computer\", "
+ "\"comment2\": \"don't release it\" }\n"
+ );
+
+ // Check the lease with nested key-value pairs in the user context.
+ lease = lmptr_->getLease4(IOAddress("192.0.0.8"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(
+ lease->toText(),
+ "Address: 192.0.0.8\n"
+ "Valid life: 800\n"
+ "Cltt: 5600\n"
+ "Hardware addr: 08:08:08:08:08:08\n"
+ "Client id: (none)\n"
+ "Subnet ID: 1\n"
+ "State: default\n"
+ "User context: { \"a\": \"b\", \"c\": { \"d\": 1, \"e\": 2 } }\n"
+ );
+}
+
+/// @brief Checks that complex user context can be read in v4.
+TEST_F(MemfileLeaseMgrTest, v6UserContext) {
+ // Add some leases to the CSV file.
+ LeaseFileIO io(getLeaseFilePath("leasefile6_0.csv"));
+ io.writeFile(
+ "address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context,hwtype,hwaddr_source\n"
+
+ "2001:db8:1::1,01:01:01:01:01:01:01:01:01:01:01:01:01,"
+ "400,1000,8,100,0,7,0,1,1,,,1,"
+ "{},,\n"
+
+ "2001:db8:1::2,02:02:02:02:02:02:02:02:02:02:02:02:02,"
+ "200,200,8,100,0,7,0,1,1,,,1,"
+ "{ \"comment\": \"this lease is for the kitchen computer\"},,\n"
+
+ // The next lines have escaped commas in user context.
+
+ "2001:db8:1::4,04:04:04:04:04:04:04:04:04:04:04:04:04,"
+ "200,200,8,100,0,7,0,1,1,,,1,"
+ "{ \"comment\": \"this lease is for the mainframe computer\"&#x2c"
+ " \"comment2\": \"don't release it\" },,\n"
+
+ "2001:db8:1::8,08:08:08:08:08:08:08:08:08:08:08:08:08,"
+ "200,200,8,100,0,7,0,1,1,,,1,"
+ "{ \"a\": \"b\"&#x2c \"c\": { \"d\": 1&#x2c\"e\": 2 } }\n"
+ );
+
+ startBackend(V6);
+
+ // Check the lease with no key-value pairs in the user context.
+ Lease6Ptr lease(
+ lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1")));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(
+ lease->toText(),
+ "Type: IA_NA(0)\n"
+ "Address: 2001:db8:1::1\n"
+ "Prefix length: 0\n"
+ "IAID: 7\n"
+ "Pref life: 100\n"
+ "Valid life: 400\n"
+ "Cltt: 600\n"
+ "DUID: 01:01:01:01:01:01:01:01:01:01:01:01:01\n"
+ "Hardware addr: (none)\n"
+ "Subnet ID: 8\n"
+ "State: declined\n"
+ "User context: { }\n"
+ );
+
+ // Check the lease with one key-value pair in the user context.
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::2"));
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(
+ lease->toText(),
+ "Type: IA_NA(0)\n"
+ "Address: 2001:db8:1::2\n"
+ "Prefix length: 0\n"
+ "IAID: 7\n"
+ "Pref life: 100\n"
+ "Valid life: 200\n"
+ "Cltt: 0\n"
+ "DUID: 02:02:02:02:02:02:02:02:02:02:02:02:02\n"
+ "Hardware addr: (none)\n"
+ "Subnet ID: 8\n"
+ "State: declined\n"
+ "User context: { \"comment\": \"this lease is for the kitchen computer\" }\n"
+ );
+
+ // Check the lease with two key-value pairs in the user context.
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::4"));
+ EXPECT_TRUE(lease);
+ EXPECT_EQ(
+ lease->toText(),
+ "Type: IA_NA(0)\n"
+ "Address: 2001:db8:1::4\n"
+ "Prefix length: 0\n"
+ "IAID: 7\n"
+ "Pref life: 100\n"
+ "Valid life: 200\n"
+ "Cltt: 0\n"
+ "DUID: 04:04:04:04:04:04:04:04:04:04:04:04:04\n"
+ "Hardware addr: (none)\n"
+ "Subnet ID: 8\n"
+ "State: declined\n"
+ "User context: { \"comment\": \"this lease is for the mainframe computer\","
+ " \"comment2\": \"don't release it\" }\n"
+ );
+
+ // Check the lease with nested key-value pairs in the user context.
+ lease = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::8"));
+ EXPECT_TRUE(lease);
+ EXPECT_EQ(
+ lease->toText(),
+ "Type: IA_NA(0)\n"
+ "Address: 2001:db8:1::8\n"
+ "Prefix length: 0\n"
+ "IAID: 7\n"
+ "Pref life: 100\n"
+ "Valid life: 200\n"
+ "Cltt: 0\n"
+ "DUID: 08:08:08:08:08:08:08:08:08:08:08:08:08\n"
+ "Hardware addr: (none)\n"
+ "Subnet ID: 8\n"
+ "State: declined\n"
+ "User context: { \"a\": \"b\", \"c\": { \"d\": 1, \"e\": 2 } }\n"
+ );
+}
+
+/// @brief Checks that various hardware information is correctly imported from a
+/// CSV file.
+TEST_F(MemfileLeaseMgrTest, testHWAddr) {
+ // Add some leases to the CSV file.
+ LeaseFileIO io(getLeaseFilePath("leasefile6_0.csv"));
+ std::stringstream contents;
+ contents << "address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+ "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,"
+ "hwaddr,state,user_context,hwtype,hwaddr_source\n";
+
+ // Create combinations with all valid values except for 255 which is an
+ // unused value for both hwtype and hwaddr_source, but lease construction
+ // doesn't complain in that case either.
+ std::vector<uint16_t> const hwtypes{0, 1, 6, 8, 255};
+ std::vector<uint32_t> const hwsources{
+ 0xffffffff, 0x00000000, 0x00000001, 0x00000002, 0x00000004, 0x00000008,
+ 0x00000010, 0x00000020, 0x00000040, 0x00000080, 0x000000ff};
+ int i = 0;
+ for (uint16_t hwtype : hwtypes) {
+ for (uint32_t hwsource : hwsources) {
+ std::stringstream hex;
+ hex << std::hex << std::setw(2) << std::setfill('0') << i;
+ contents << "2001:db8:1::" << hex.str()
+ << ",00:00:00:00:00:" << hex.str()
+ << std::dec << ",7200,8000,1,3600,0,1,128,0,0,,ff:ff:ff:ff:ff:"
+ << hex.str() << ",1,," << hwtype << "," << hwsource << "\n";
+ ++i;
+ }
+ }
+
+ io.writeFile(contents.str());
+ startBackend(V6);
+
+ // Check leases.
+ i = 0;
+ for (uint16_t hwtype : hwtypes) {
+ for (uint32_t hwsource : hwsources) {
+ std::stringstream hex;
+ hex << std::hex << std::setw(2) << std::setfill('0') << i;
+ IOAddress address("2001:db8:1::" + hex.str());
+ Lease6Ptr lease(lmptr_->getLease6(Lease::TYPE_NA, address.toText()));
+
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(lease->toText(),
+ "Type: IA_NA(0)\n"
+ "Address: " + address.toText() + "\n"
+ "Prefix length: 128\n"
+ "IAID: 1\n"
+ "Pref life: 3600\n"
+ "Valid life: 7200\n"
+ "Cltt: 800\n"
+ "DUID: 00:00:00:00:00:" + hex.str() + "\n"
+ "Hardware addr: ff:ff:ff:ff:ff:" + hex.str() + "\n"
+ "Subnet ID: 1\n"
+ "State: declined\n");
+ ASSERT_TRUE(lease->hwaddr_);
+ EXPECT_EQ(lease->hwaddr_->htype_, hwtype);
+ EXPECT_EQ(lease->hwaddr_->source_, hwsource);
+ ++i;
+ }
+ }
+}
+
+// Verifies that isJsonSupported() returns true for Memfile.
+TEST_F(MemfileLeaseMgrTest, isJsonSupported4) {
+ startBackend(V4);
+ bool json_supported;
+ ASSERT_NO_THROW_LOG(json_supported = LeaseMgrFactory::instance().isJsonSupported());
+ ASSERT_TRUE(json_supported);
+}
+
+// Verifies that isJsonSupported() returns true for Memfile.
+TEST_F(MemfileLeaseMgrTest, isJsonSupported6) {
+ startBackend(V6);
+ bool json_supported;
+ ASSERT_NO_THROW_LOG(json_supported = LeaseMgrFactory::instance().isJsonSupported());
+ ASSERT_TRUE(json_supported);
+}
+
+// Verifies that v4 class lease counts are correctly adjusted
+// when leases have class lists.
+TEST_F(MemfileLeaseMgrTest, classLeaseCount4) {
+ startBackend(V4);
+ testClassLeaseCount4();
+}
+
+// Verifies that v6 IA_NA class lease counts are correctly adjusted
+// when leases have class lists.
+TEST_F(MemfileLeaseMgrTest, classLeaseCount6_NA) {
+ startBackend(V6);
+ testClassLeaseCount6(Lease::TYPE_NA);
+}
+
+// Verifies that v6 IA_PD class lease counts are correctly adjusted
+// when leases have class lists.
+TEST_F(MemfileLeaseMgrTest, classLeaseCount6_PD) {
+ startBackend(V6);
+ testClassLeaseCount6(Lease::TYPE_PD);
+}
+
+// brief Checks that a null user context allows allocation.
+TEST_F(MemfileLeaseMgrTest, checkLimitsNull4) {
+ startBackend(V4);
+ std::string text;
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits4(nullptr));
+ EXPECT_TRUE(text.empty());
+}
+
+// brief Checks that a null user context allows allocation.
+TEST_F(MemfileLeaseMgrTest, checkLimitsNull6) {
+ startBackend(V6);
+ std::string text;
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits6(nullptr));
+ EXPECT_TRUE(text.empty());
+}
+
+// Checks a few v4 lease limit checking scenarios.
+TEST_F(MemfileLeaseMgrTest, checkLimits4) {
+ startBackend(V4);
+ testLeaseLimits4();
+}
+
+// Checks a few v6 lease limit checking scenarios.
+TEST_F(MemfileLeaseMgrTest, checkLimits6) {
+ startBackend(V6);
+ testLeaseLimits6();
+}
+
+// Verifies that v4 class lease counts can be recounted.
+TEST_F(MemfileLeaseMgrTest, classLeaseRecount4) {
+ startBackend(V4);
+
+ // Create a subnet
+ CfgSubnets4Ptr cfg = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
+ Subnet4Ptr subnet;
+ Pool4Ptr pool;
+
+ subnet.reset(new Subnet4(IOAddress("192.0.1.0"), 24, 1, 2, 3, 777));
+ pool.reset(new Pool4(IOAddress("192.0.1.0"), 24));
+ subnet->addPool(pool);
+ cfg->add(subnet);
+
+ // We need a Memfile_LeaseMgr pointer to access recount and clear functions.
+ Memfile_LeaseMgr* memfile_mgr = dynamic_cast<Memfile_LeaseMgr*>(lmptr_);
+ ASSERT_TRUE(memfile_mgr);
+
+ // Verify class lease counts are zero.
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("water"));
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("melon"));
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("slice"));
+
+ // Structure that describes a recipe for a lease.
+ struct Recipe {
+ std::string address_;
+ uint32_t state_;
+ std::list<ClientClass> classes_;
+ };
+
+ // List of lease recipes.
+ std::list<Recipe> recipes{
+ { "192.0.1.100", Lease::STATE_DEFAULT, {"water", "slice"} },
+ { "192.0.1.101", Lease::STATE_DEFAULT, {"melon"} },
+ { "192.0.1.102", Lease::STATE_DEFAULT, {"melon", "slice"} }
+ };
+
+ // Bake all the leases.
+ for ( auto recipe : recipes ) {
+ ElementPtr ctx = makeContextWithClasses(recipe.classes_);
+ ASSERT_TRUE(makeLease4(recipe.address_, 777, recipe.state_, ctx));
+ }
+
+ // Verify counts are as expected.
+ EXPECT_EQ(1, memfile_mgr->getClassLeaseCount("water"));
+ EXPECT_EQ(2, memfile_mgr->getClassLeaseCount("melon"));
+ EXPECT_EQ(2, memfile_mgr->getClassLeaseCount("slice"));
+
+ // Clear counts
+ ASSERT_NO_THROW_LOG(memfile_mgr->clearClassLeaseCounts());
+
+ // Verify counts are zero.
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("water"));
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("melon"));
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("slice"));
+
+ // Recount
+ ASSERT_NO_THROW_LOG(memfile_mgr->recountClassLeases4());
+
+ // Verify counts are recounted correctly.
+ EXPECT_EQ(1, memfile_mgr->getClassLeaseCount("water"));
+ EXPECT_EQ(2, memfile_mgr->getClassLeaseCount("melon"));
+ EXPECT_EQ(2, memfile_mgr->getClassLeaseCount("slice"));
+}
+
+// Verifies that v6 class lease counts can be recounted.
+TEST_F(MemfileLeaseMgrTest, classLeaseRecount6) {
+ startBackend(V6);
+
+ // Create a subnet
+ CfgSubnets6Ptr cfg = CfgMgr::instance().getStagingCfg()->getCfgSubnets6();
+ Subnet6Ptr subnet;
+ Pool6Ptr pool;
+
+ subnet.reset(new Subnet6(IOAddress("3001:1::"), 64, 1, 2, 3, 4, 777));
+ pool.reset(new Pool6(Lease::TYPE_NA, IOAddress("3001:1::"),
+ IOAddress("3001:1::FF")));
+ subnet->addPool(pool);
+
+ pool.reset(new Pool6(Lease::TYPE_PD, IOAddress("3001:1:2::"), 96, 112));
+ subnet->addPool(pool);
+ cfg->add(subnet);
+
+ // We need a Memfile_LeaseMgr pointer to access recount and clear functions.
+ Memfile_LeaseMgr* memfile_mgr = dynamic_cast<Memfile_LeaseMgr*>(lmptr_);
+ ASSERT_TRUE(memfile_mgr);
+
+ // Verify class lease counts are zero.
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("water", Lease::TYPE_NA));
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("melon", Lease::TYPE_NA));
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("slice", Lease::TYPE_NA));
+
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("grapes", Lease::TYPE_PD));
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("wrath", Lease::TYPE_PD));
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("slice", Lease::TYPE_PD));
+
+ // Structure that describes a recipe for a lease.
+ struct Recipe {
+ Lease::Type ltype_;
+ std::string address_;
+ uint32_t prefix_len_;
+ uint32_t state_;
+ std::list<ClientClass> classes_;
+ };
+
+ // List of lease recipes.
+ std::list<Recipe> recipes{
+ { Lease::TYPE_NA, "3001::1", 0, Lease::STATE_DEFAULT, {"water", "slice"} },
+ { Lease::TYPE_NA, "3001::2", 0, Lease::STATE_DEFAULT, {"melon"} },
+ { Lease::TYPE_NA, "3001::3", 0, Lease::STATE_DEFAULT, {"melon", "slice"} },
+
+ { Lease::TYPE_PD, "3001:1:2:0100::", 112, Lease::STATE_DEFAULT, {"grapes", "slice"} },
+ { Lease::TYPE_PD, "3001:1:2:0200::", 112, Lease::STATE_DEFAULT, {"wrath"} },
+ { Lease::TYPE_PD, "3001:1:2:0300::", 112, Lease::STATE_DEFAULT, {"wrath", "slice"} },
+ };
+
+ // Bake all the leases.
+ for ( auto recipe : recipes ) {
+ ElementPtr ctx = makeContextWithClasses(recipe.classes_);
+ ASSERT_TRUE(makeLease6(recipe.ltype_, recipe.address_, recipe.prefix_len_, 777, recipe.state_, ctx));
+ }
+
+ // Verify counts are as expected.
+ EXPECT_EQ(1, memfile_mgr->getClassLeaseCount("water", Lease::TYPE_NA));
+ EXPECT_EQ(2, memfile_mgr->getClassLeaseCount("melon", Lease::TYPE_NA));
+ EXPECT_EQ(2, memfile_mgr->getClassLeaseCount("slice", Lease::TYPE_NA));
+
+ EXPECT_EQ(1, memfile_mgr->getClassLeaseCount("grapes", Lease::TYPE_PD));
+ EXPECT_EQ(2, memfile_mgr->getClassLeaseCount("wrath", Lease::TYPE_PD));
+ EXPECT_EQ(2, memfile_mgr->getClassLeaseCount("slice", Lease::TYPE_PD));
+
+ // Clear counts
+ ASSERT_NO_THROW_LOG(memfile_mgr->clearClassLeaseCounts());
+
+ // Verify counts are zero.
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("water", Lease::TYPE_NA));
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("melon", Lease::TYPE_NA));
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("slice", Lease::TYPE_NA));
+
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("grapes", Lease::TYPE_PD));
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("wrath", Lease::TYPE_PD));
+ EXPECT_EQ(0, memfile_mgr->getClassLeaseCount("slice", Lease::TYPE_PD));
+
+ // Recount
+ ASSERT_NO_THROW_LOG(memfile_mgr->recountClassLeases6());
+
+ // Verify counts are recounted correctly.
+ EXPECT_EQ(1, memfile_mgr->getClassLeaseCount("water", Lease::TYPE_NA));
+ EXPECT_EQ(2, memfile_mgr->getClassLeaseCount("melon", Lease::TYPE_NA));
+ EXPECT_EQ(2, memfile_mgr->getClassLeaseCount("slice", Lease::TYPE_NA));
+
+ EXPECT_EQ(1, memfile_mgr->getClassLeaseCount("grapes", Lease::TYPE_PD));
+ EXPECT_EQ(2, memfile_mgr->getClassLeaseCount("wrath", Lease::TYPE_PD));
+ EXPECT_EQ(2, memfile_mgr->getClassLeaseCount("slice", Lease::TYPE_PD));
+}
+
+} // namespace
diff --git a/src/lib/dhcpsrv/tests/multi_threading_config_parser_unittest.cc b/src/lib/dhcpsrv/tests/multi_threading_config_parser_unittest.cc
new file mode 100644
index 0000000..2272ccb
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/multi_threading_config_parser_unittest.cc
@@ -0,0 +1,192 @@
+// Copyright (C) 2020-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <cc/data.h>
+#include <dhcpsrv/parsers/multi_threading_config_parser.h>
+#include <dhcpsrv/cfg_multi_threading.h>
+#include <util/multi_threading_mgr.h>
+#include <testutils/test_to_element.h>
+
+#include <gtest/gtest.h>
+
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::test;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Test fixture class for @c MultiThreadingConfigParser
+class MultiThreadingConfigParserTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor
+ MultiThreadingConfigParserTest() = default;
+
+ /// @brief Destructor
+ virtual ~MultiThreadingConfigParserTest() = default;
+
+protected:
+
+ /// @brief Setup for each test.
+ virtual void SetUp();
+
+ /// @brief Cleans up after each test.
+ virtual void TearDown();
+};
+
+void
+MultiThreadingConfigParserTest::SetUp() {
+ MultiThreadingMgr::instance().setMode(false);
+}
+
+void
+MultiThreadingConfigParserTest::TearDown() {
+ MultiThreadingMgr::instance().setMode(false);
+}
+
+// Verifies that MultiThreadingConfigParser handles
+// expected valid content
+TEST_F(MultiThreadingConfigParserTest, validContent) {
+ struct Scenario {
+ std::string description_;
+ std::string json_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "enable-multi-threading, without thread-pool-size or packet-queue-size",
+ "{ \n"
+ " \"enable-multi-threading\": true \n"
+ "} \n"
+ },
+ {
+ "enable-multi-threading disabled",
+ "{ \n"
+ " \"enable-multi-threading\": false \n"
+ "} \n"
+ },
+ {
+ "enable-multi-threading, with thread-pool-size and packet-queue-size",
+ "{ \n"
+ " \"enable-multi-threading\": true, \n"
+ " \"thread-pool-size\": 4, \n"
+ " \"packet-queue-size\": 64 \n"
+ "} \n"
+ }
+ };
+
+ // Iterate over the valid scenarios and verify they succeed.
+ ConstElementPtr config_elems;
+ ConstElementPtr multi_threading_config;
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+ {
+ SrvConfig srv_config;
+ // Construct the config JSON
+ ASSERT_NO_THROW(config_elems = Element::fromJSON(scenario.json_))
+ << "invalid JSON, test is broken";
+
+ // Parsing config should succeed.
+ MultiThreadingConfigParser parser;
+ try {
+ parser.parse(srv_config, config_elems);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "parser threw an exception: " << ex.what();
+ }
+
+ multi_threading_config = srv_config.getDHCPMultiThreading();
+ // Verify the resultant configuration.
+ ASSERT_TRUE(multi_threading_config);
+
+ bool enabled = false;
+ uint32_t thread_count = 0;
+ uint32_t queue_size = 0;
+
+ CfgMultiThreading::extract(multi_threading_config, enabled,
+ thread_count, queue_size);
+
+ EXPECT_EQ(MultiThreadingMgr::instance().getMode(), enabled);
+
+ EXPECT_TRUE(multi_threading_config->equals(*config_elems));
+ }
+ }
+}
+
+// Verifies that MultiThreadingConfigParser correctly catches
+// invalid content
+TEST_F(MultiThreadingConfigParserTest, invalidContent) {
+ struct Scenario {
+ std::string description_;
+ std::string json_;
+ };
+
+ std::vector<Scenario> scenarios = {
+ {
+ "enable-multi-threading not boolean",
+ "{ \n"
+ " \"enable-multi-threading\": \"always\" \n"
+ "} \n"
+ },
+ {
+ "thread-pool-size not integer",
+ "{ \n"
+ " \"thread-pool-size\": true \n"
+ "} \n"
+ },
+ {
+ "thread-pool-size negative",
+ "{ \n"
+ " \"thread-pool-size\": -1 \n"
+ "} \n"
+ },
+ {
+ "thread-pool-size too large",
+ "{ \n"
+ " \"thread-pool-size\": 200000 \n"
+ "} \n"
+ },
+ {
+ "packet-queue-size not integer",
+ "{ \n"
+ " \"packet-queue-size\": true \n"
+ "} \n"
+ },
+ {
+ "packet-queue-size-size negative",
+ "{ \n"
+ " \"packet-queue-size\": -1 \n"
+ "} \n"
+ },
+ {
+ "packet-queue-size too large",
+ "{ \n"
+ " \"packet-queue-size\": 200000 \n"
+ "} \n"
+ }
+ };
+
+ // Iterate over the valid scenarios and verify they succeed.
+ ConstElementPtr config_elems;
+ ConstElementPtr queue_control;
+ for (auto scenario : scenarios) {
+ SCOPED_TRACE(scenario.description_);
+ {
+ SrvConfig srv_config;
+ // Construct the config JSON
+ ASSERT_NO_THROW(config_elems = Element::fromJSON(scenario.json_))
+ << "invalid JSON, test is broken";
+
+ // Parsing config into a queue control should succeed.
+ MultiThreadingConfigParser parser;
+ EXPECT_THROW(parser.parse(srv_config, config_elems), DhcpConfigError);
+ }
+ }
+}
+
+} // namespace
diff --git a/src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc b/src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc
new file mode 100644
index 0000000..73f602a
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc
@@ -0,0 +1,1706 @@
+// Copyright (C) 2015-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <dhcpsrv/testutils/test_utils.h>
+#include <exceptions/exceptions.h>
+#include <dhcpsrv/host.h>
+#include <dhcpsrv/mysql_host_data_source.h>
+#include <dhcpsrv/testutils/generic_host_data_source_unittest.h>
+#include <dhcpsrv/testutils/host_data_source_utils.h>
+#include <dhcpsrv/host_mgr.h>
+#include <dhcpsrv/host_data_source_factory.h>
+#include <mysql/mysql_connection.h>
+#include <mysql/testutils/mysql_schema.h>
+#include <testutils/multi_threading_utils.h>
+#include <util/multi_threading_mgr.h>
+
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <utility>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::db;
+using namespace isc::db::test;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::data;
+using namespace isc::test;
+using namespace isc::util;
+using namespace std;
+
+namespace {
+
+class MySqlHostDataSourceTest : public GenericHostDataSourceTest {
+public:
+ /// @brief Clears the database and opens connection to it.
+ void initializeTest() {
+ // Ensure we have the proper schema with no transient data.
+ createMySQLSchema();
+
+ // Connect to the database
+ try {
+ HostMgr::create();
+ HostMgr::addBackend(validMySQLConnectionString());
+ } catch (...) {
+ std::cerr << "*** ERROR: unable to open database. The test\n"
+ "*** environment is broken and must be fixed before\n"
+ "*** the MySQL tests will run correctly.\n"
+ "*** The reason for the problem is described in the\n"
+ "*** accompanying exception output.\n";
+ throw;
+ }
+
+ hdsptr_ = HostMgr::instance().getHostDataSource();
+ hdsptr_->setIPReservationsUnique(true);
+
+ MultiThreadingMgr::instance().setMode(false);
+ }
+
+ /// @brief Destroys the HDS and the schema.
+ void destroyTest() {
+ try {
+ hdsptr_->rollback();
+ } catch (...) {
+ // Rollback may fail if backend is in read only mode. That's ok.
+ }
+ HostMgr::delAllBackends();
+ hdsptr_.reset();
+ // If data wipe enabled, delete transient data otherwise destroy the schema
+ destroyMySQLSchema();
+ }
+
+ /// @brief Constructor
+ ///
+ /// Deletes everything from the database and opens it.
+ MySqlHostDataSourceTest() {
+ initializeTest();
+ }
+
+ /// @brief Destructor
+ ///
+ /// Rolls back all pending transactions. The deletion of hdsptr_ will close
+ /// the database. Then reopen it and delete everything created by the test.
+ virtual ~MySqlHostDataSourceTest() {
+ destroyTest();
+ }
+
+ /// @brief Reopen the database
+ ///
+ /// Closes the database and re-open it. Anything committed should be
+ /// visible.
+ ///
+ /// Parameter is ignored for MySQL backend as the v4 and v6 hosts share
+ /// the same database.
+ void reopen(Universe) {
+ HostMgr::create();
+ HostMgr::addBackend(validMySQLConnectionString());
+ hdsptr_ = HostMgr::instance().getHostDataSource();
+ }
+
+ /// @brief returns number of rows in a table
+ ///
+ /// Note: This method uses its own connection. It will not work if your test
+ /// uses transactions.
+ ///
+ /// @param name of the table
+ /// @return number of rows currently present in the table
+ int countRowsInTable(const std::string& table) {
+ string query = "SELECT * FROM " + table;
+
+ DatabaseConnection::ParameterMap params;
+ params["name"] = "keatest";
+ params["user"] = "keatest";
+ params["password"] = "keatest";
+
+ MySqlConnection conn(params);
+ conn.openDatabase();
+
+ int status = MysqlQuery(conn.mysql_, query.c_str());
+ if (status != 0) {
+ isc_throw(DbOperationError, "Query failed: " << mysql_error(conn.mysql_));
+ }
+
+ MYSQL_RES * res = mysql_store_result(conn.mysql_);
+ int numrows = static_cast<int>(mysql_num_rows(res));
+ mysql_free_result(res);
+
+ return (numrows);
+ }
+
+ /// @brief Returns number of IPv4 options currently stored in DB.
+ virtual int countDBOptions4() {
+ return (countRowsInTable("dhcp4_options"));
+ }
+
+ /// @brief Returns number of IPv6 options currently stored in DB.
+ virtual int countDBOptions6() {
+ return (countRowsInTable("dhcp6_options"));
+ }
+
+ /// @brief Returns number of IPv6 reservations currently stored in DB.
+ virtual int countDBReservations6() {
+ return (countRowsInTable("ipv6_reservations"));
+ }
+
+};
+
+/// @brief Check that database can be opened
+///
+/// This test checks if the MySqlHostDataSource can be instantiated. This happens
+/// only if the database can be opened. Note that this is not part of the
+/// MySqlHostMgr test fixture set. This test checks that the database can be
+/// opened: the fixtures assume that and check basic operations.
+TEST(MySqlHostDataSource, OpenDatabase) {
+ // Schema needs to be created for the test to work.
+ destroyMySQLSchema();
+ createMySQLSchema();
+
+ // Check that host manager opens the database correctly and tidy up. If it
+ // fails, print the error message.
+ try {
+ HostMgr::create();
+ EXPECT_NO_THROW(HostMgr::addBackend(validMySQLConnectionString()));
+ HostMgr::delBackend("mysql");
+ } catch (const isc::Exception& ex) {
+ FAIL() << "*** ERROR: unable to open database, reason:\n"
+ << " " << ex.what() << "\n"
+ << "*** The test environment is broken and must be fixed\n"
+ << "*** before the MySQL tests will run correctly.\n";
+ }
+
+ // Check that host manager opens the database correctly with a longer
+ // timeout. If it fails, print the error message.
+ try {
+ string connection_string = validMySQLConnectionString() + string(" ") +
+ string(VALID_TIMEOUT);
+ HostMgr::create();
+ EXPECT_NO_THROW(HostMgr::addBackend(connection_string));
+ HostMgr::delBackend("mysql");
+ } catch (const isc::Exception& ex) {
+ FAIL() << "*** ERROR: unable to open database, reason:\n"
+ << " " << ex.what() << "\n"
+ << "*** The test environment is broken and must be fixed\n"
+ << "*** before the MySQL tests will run correctly.\n";
+ }
+
+ // Check that attempting to get an instance of the host data source when
+ // none is set returns empty pointer.
+ EXPECT_FALSE(HostMgr::instance().getHostDataSource());
+
+ // Check that wrong specification of backend throws an exception.
+ // (This is really a check on HostDataSourceFactory, but is convenient to
+ // perform here.)
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ NULL, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ InvalidParameter);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ INVALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)),
+ InvalidType);
+
+ // Check that invalid login data causes an exception.
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, INVALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, INVALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, INVALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD, INVALID_TIMEOUT_1)),
+ DbInvalidTimeout);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD, INVALID_TIMEOUT_2)),
+ DbInvalidTimeout);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD,
+ VALID_TIMEOUT, INVALID_READONLY_DB)), DbInvalidReadOnly);
+
+ // Check for missing parameters
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, NULL, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ NoDatabaseName);
+
+ // Tidy up after the test
+ destroyMySQLSchema();
+}
+
+/// @brief Check that database can be opened with Multi-Threading
+///
+/// This test checks if the MySqlHostDataSource can be instantiated. This happens
+/// only if the database can be opened. Note that this is not part of the
+/// MySqlHostMgr test fixture set. This test checks that the database can be
+/// opened: the fixtures assume that and check basic operations.
+TEST(MySqlHostDataSource, OpenDatabaseMultiThreading) {
+ // Enable Multi-Threading.
+ MultiThreadingTest mt(true);
+
+ // Schema needs to be created for the test to work.
+ destroyMySQLSchema();
+ createMySQLSchema();
+
+ // Check that host manager opens the database correctly and tidy up. If it
+ // fails, print the error message.
+ try {
+ HostMgr::create();
+ EXPECT_NO_THROW(HostMgr::addBackend(validMySQLConnectionString()));
+ HostMgr::delBackend("mysql");
+ } catch (const isc::Exception& ex) {
+ FAIL() << "*** ERROR: unable to open database, reason:\n"
+ << " " << ex.what() << "\n"
+ << "*** The test environment is broken and must be fixed\n"
+ << "*** before the MySQL tests will run correctly.\n";
+ }
+
+ // Check that host manager opens the database correctly with a longer
+ // timeout. If it fails, print the error message.
+ try {
+ string connection_string = validMySQLConnectionString() + string(" ") +
+ string(VALID_TIMEOUT);
+ HostMgr::create();
+ EXPECT_NO_THROW(HostMgr::addBackend(connection_string));
+ HostMgr::delBackend("mysql");
+ } catch (const isc::Exception& ex) {
+ FAIL() << "*** ERROR: unable to open database, reason:\n"
+ << " " << ex.what() << "\n"
+ << "*** The test environment is broken and must be fixed\n"
+ << "*** before the MySQL tests will run correctly.\n";
+ }
+
+ // Check that attempting to get an instance of the host data source when
+ // none is set returns empty pointer.
+ EXPECT_FALSE(HostMgr::instance().getHostDataSource());
+
+ // Check that wrong specification of backend throws an exception.
+ // (This is really a check on HostDataSourceFactory, but is convenient to
+ // perform here.)
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ NULL, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ InvalidParameter);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ INVALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)),
+ InvalidType);
+
+ // Check that invalid login data causes an exception.
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, INVALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, INVALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, INVALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD, INVALID_TIMEOUT_1)),
+ DbInvalidTimeout);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD, INVALID_TIMEOUT_2)),
+ DbInvalidTimeout);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD,
+ VALID_TIMEOUT, INVALID_READONLY_DB)), DbInvalidReadOnly);
+
+ // Check for missing parameters
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, NULL, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ NoDatabaseName);
+
+ // Tidy up after the test
+ destroyMySQLSchema();
+}
+
+/// @brief Flag used to detect calls to db_lost_callback function
+bool callback_called = false;
+
+/// @brief Callback function used in open database testing
+bool db_lost_callback(ReconnectCtlPtr /* db_conn_retry */) {
+ return (callback_called = true);
+}
+
+/// @brief Make sure open failures do NOT invoke db lost callback
+/// The db lost callback should only be invoked after successfully
+/// opening the DB and then subsequently losing it. Failing to
+/// open should be handled directly by the application layer.
+/// There is simply no good way to break the connection in a
+/// unit test environment. So testing the callback invocation
+/// in a unit test is next to impossible. That has to be done
+/// as a system test.
+TEST(MySqlHostDataSource, NoCallbackOnOpenFail) {
+ // Schema needs to be created for the test to work.
+ destroyMySQLSchema();
+ createMySQLSchema();
+
+ callback_called = false;
+ DatabaseConnection::db_lost_callback_ = db_lost_callback;
+ HostMgr::create();
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, INVALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+
+ EXPECT_FALSE(callback_called);
+ destroyMySQLSchema();
+}
+
+/// @brief Make sure open failures do NOT invoke db lost callback
+/// The db lost callback should only be invoked after successfully
+/// opening the DB and then subsequently losing it. Failing to
+/// open should be handled directly by the application layer.
+/// There is simply no good way to break the connection in a
+/// unit test environment. So testing the callback invocation
+/// in a unit test is next to impossible. That has to be done
+/// as a system test.
+TEST(MySqlHostDataSource, NoCallbackOnOpenFailMultiThreading) {
+ // Enable Multi-Threading.
+ MultiThreadingTest mt(true);
+
+ // Schema needs to be created for the test to work.
+ destroyMySQLSchema();
+ createMySQLSchema();
+
+ callback_called = false;
+ DatabaseConnection::db_lost_callback_ = db_lost_callback;
+ HostMgr::create();
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, INVALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+
+ EXPECT_FALSE(callback_called);
+ destroyMySQLSchema();
+}
+
+/// @brief Check conversion functions
+///
+/// The server works using cltt and valid_filetime. In the database, the
+/// information is stored as expire_time and valid-lifetime, which are
+/// related by
+///
+/// expire_time = cltt + valid_lifetime
+///
+/// This test checks that the conversion is correct. It does not check that the
+/// data is entered into the database correctly, only that the MYSQL_TIME
+/// structure used for the entry is correctly set up.
+TEST(MySqlConnection, checkTimeConversion) {
+ const time_t cltt = time(NULL);
+ const uint32_t valid_lft = 86400; // 1 day
+ struct tm tm_expire;
+ MYSQL_TIME mysql_expire;
+
+ // Work out what the broken-down time will be for one day
+ // after the current time.
+ time_t expire_time = cltt + valid_lft;
+ (void) localtime_r(&expire_time, &tm_expire);
+
+ // Convert to the database time
+ MySqlConnection::convertToDatabaseTime(cltt, valid_lft, mysql_expire);
+
+ // Are the times the same?
+ EXPECT_EQ(tm_expire.tm_year + 1900, mysql_expire.year);
+ EXPECT_EQ(tm_expire.tm_mon + 1, mysql_expire.month);
+ EXPECT_EQ(tm_expire.tm_mday, mysql_expire.day);
+ EXPECT_EQ(tm_expire.tm_hour, mysql_expire.hour);
+ EXPECT_EQ(tm_expire.tm_min, mysql_expire.minute);
+ EXPECT_EQ(tm_expire.tm_sec, mysql_expire.second);
+ EXPECT_EQ(0, mysql_expire.second_part);
+ EXPECT_EQ(0, mysql_expire.neg);
+
+ // Convert back
+ time_t converted_cltt = 0;
+ MySqlConnection::convertFromDatabaseTime(mysql_expire, valid_lft, converted_cltt);
+ EXPECT_EQ(cltt, converted_cltt);
+}
+
+/// @brief This test verifies that database backend can operate in Read-Only mode.
+TEST_F(MySqlHostDataSourceTest, testReadOnlyDatabase) {
+ testReadOnlyDatabase(MYSQL_VALID_TYPE);
+}
+
+/// @brief This test verifies that database backend can operate in Read-Only mode.
+TEST_F(MySqlHostDataSourceTest, testReadOnlyDatabaseMultiThreading) {
+ MultiThreadingTest mt(true);
+ testReadOnlyDatabase(MYSQL_VALID_TYPE);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by IPv4
+/// address. Host uses hw address as identifier.
+TEST_F(MySqlHostDataSourceTest, basic4HWAddr) {
+ testBasic4(Host::IDENT_HWADDR);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by IPv4
+/// address. Host uses hw address as identifier.
+TEST_F(MySqlHostDataSourceTest, basic4HWAddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testBasic4(Host::IDENT_HWADDR);
+}
+
+/// @brief Verifies that IPv4 host reservation with options can have the global
+/// subnet id value
+TEST_F(MySqlHostDataSourceTest, globalSubnetId4) {
+ testGlobalSubnetId4();
+}
+
+/// @brief Verifies that IPv4 host reservation with options can have the global
+/// subnet id value
+TEST_F(MySqlHostDataSourceTest, globalSubnetId4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGlobalSubnetId4();
+}
+
+/// @brief Verifies that IPv6 host reservation with options can have the global
+/// subnet id value
+TEST_F(MySqlHostDataSourceTest, globalSubnetId6) {
+ testGlobalSubnetId6();
+}
+
+/// @brief Verifies that IPv6 host reservation with options can have the global
+/// subnet id value
+TEST_F(MySqlHostDataSourceTest, globalSubnetId6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGlobalSubnetId6();
+}
+
+/// @brief Verifies that IPv4 host reservation with options can have a max value
+/// for dhcp4_subnet id
+TEST_F(MySqlHostDataSourceTest, maxSubnetId4) {
+ testMaxSubnetId4();
+}
+
+/// @brief Verifies that IPv4 host reservation with options can have a max value
+/// for dhcp4_subnet id
+TEST_F(MySqlHostDataSourceTest, maxSubnetId4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMaxSubnetId4();
+}
+
+/// @brief Verifies that IPv6 host reservation with options can have a max value
+/// for dhcp6_subnet id
+TEST_F(MySqlHostDataSourceTest, maxSubnetId6) {
+ testMaxSubnetId6();
+}
+
+/// @brief Verifies that IPv6 host reservation with options can have a max value
+/// for dhcp6_subnet id
+TEST_F(MySqlHostDataSourceTest, maxSubnetId6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMaxSubnetId6();
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+TEST_F(MySqlHostDataSourceTest, getAll4BySubnet) {
+ testGetAll4();
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+TEST_F(MySqlHostDataSourceTest, getAll4BySubnetMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetAll4();
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+TEST_F(MySqlHostDataSourceTest, getAll6BySubnet) {
+ testGetAll6();
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+TEST_F(MySqlHostDataSourceTest, getAll6BySubnetMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetAll6();
+}
+
+/// @brief Verifies that host reservations with the same hostname can be retrieved
+TEST_F(MySqlHostDataSourceTest, getAllbyHostname) {
+ testGetAllbyHostname();
+}
+
+/// @brief Verifies that host reservations with the same hostname can be retrieved
+TEST_F(MySqlHostDataSourceTest, getAllbyHostnameMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetAllbyHostname();
+}
+
+/// @brief Verifies that IPv4 host reservations with the same hostname and in
+/// the same subnet can be retrieved
+TEST_F(MySqlHostDataSourceTest, getAllbyHostnameSubnet4) {
+ testGetAllbyHostnameSubnet4();
+}
+
+/// @brief Verifies that IPv4 host reservations with the same hostname and in
+/// the same subnet can be retrieved
+TEST_F(MySqlHostDataSourceTest, getAllbyHostnameSubnet4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetAllbyHostnameSubnet4();
+}
+
+/// @brief Verifies that IPv6 host reservations with the same hostname and in
+/// the same subnet can be retrieved
+TEST_F(MySqlHostDataSourceTest, getAllbyHostnameSubnet6) {
+ testGetAllbyHostnameSubnet6();
+}
+
+/// @brief Verifies that IPv6 host reservations with the same hostname and in
+/// the same subnet can be retrieved
+TEST_F(MySqlHostDataSourceTest, getAllbyHostnameSubnet6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetAllbyHostnameSubnet6();
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+/// by pages.
+TEST_F(MySqlHostDataSourceTest, getPage4) {
+ testGetPage4();
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+/// by pages.
+TEST_F(MySqlHostDataSourceTest, getPage4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPage4();
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+/// by pages.
+TEST_F(MySqlHostDataSourceTest, getPage6) {
+ testGetPage6();
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+/// by pages.
+TEST_F(MySqlHostDataSourceTest, getPage6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPage6();
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+/// by pages without truncation from the limit.
+TEST_F(MySqlHostDataSourceTest, getPageLimit4) {
+ testGetPageLimit4(Host::IDENT_DUID);
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+/// by pages without truncation from the limit.
+TEST_F(MySqlHostDataSourceTest, getPageLimit4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPageLimit4(Host::IDENT_DUID);
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+/// by pages without truncation from the limit.
+TEST_F(MySqlHostDataSourceTest, getPageLimit6) {
+ testGetPageLimit6(Host::IDENT_HWADDR);
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+/// by pages without truncation from the limit.
+TEST_F(MySqlHostDataSourceTest, getPageLimit6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPageLimit6(Host::IDENT_HWADDR);
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+/// by pages even with multiple subnets.
+TEST_F(MySqlHostDataSourceTest, getPage4Subnets) {
+ testGetPage4Subnets();
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+/// by pages even with multiple subnets.
+TEST_F(MySqlHostDataSourceTest, getPage4SubnetsMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPage4Subnets();
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+/// by pages even with multiple subnets.
+TEST_F(MySqlHostDataSourceTest, getPage6Subnets) {
+ testGetPage6Subnets();
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+/// by pages even with multiple subnets.
+TEST_F(MySqlHostDataSourceTest, getPage6SubnetsMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPage6Subnets();
+}
+
+// Verifies that all IPv4 host reservations can be retrieved by pages.
+TEST_F(MySqlHostDataSourceTest, getPage4All) {
+ testGetPage4All();
+}
+
+// Verifies that all IPv4 host reservations can be retrieved by pages.
+TEST_F(MySqlHostDataSourceTest, getPage4AllMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPage4All();
+}
+
+// Verifies that all IPv6 host reservations can be retrieved by pages.
+TEST_F(MySqlHostDataSourceTest, getPage6All) {
+ testGetPage6All();
+}
+
+// Verifies that all IPv6 host reservations can be retrieved by pages.
+TEST_F(MySqlHostDataSourceTest, getPage6AllMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPage6All();
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by IPv4
+/// address. Host uses client-id (DUID) as identifier.
+TEST_F(MySqlHostDataSourceTest, basic4ClientId) {
+ testBasic4(Host::IDENT_DUID);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by IPv4
+/// address. Host uses client-id (DUID) as identifier.
+TEST_F(MySqlHostDataSourceTest, basic4ClientIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testBasic4(Host::IDENT_DUID);
+}
+
+/// @brief Test verifies that multiple hosts can be added and later retrieved by their
+/// reserved IPv4 address. This test uses HW addresses as identifiers.
+TEST_F(MySqlHostDataSourceTest, getByIPv4HWaddr) {
+ testGetByIPv4(Host::IDENT_HWADDR);
+}
+
+/// @brief Test verifies that multiple hosts can be added and later retrieved by their
+/// reserved IPv4 address. This test uses HW addresses as identifiers.
+TEST_F(MySqlHostDataSourceTest, getByIPv4HWaddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetByIPv4(Host::IDENT_HWADDR);
+}
+
+/// @brief Test verifies that multiple hosts can be added and later retrieved by their
+/// reserved IPv4 address. This test uses client-id (DUID) as identifiers.
+TEST_F(MySqlHostDataSourceTest, getByIPv4ClientId) {
+ testGetByIPv4(Host::IDENT_DUID);
+}
+
+/// @brief Test verifies that multiple hosts can be added and later retrieved by their
+/// reserved IPv4 address. This test uses client-id (DUID) as identifiers.
+TEST_F(MySqlHostDataSourceTest, getByIPv4ClientIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetByIPv4(Host::IDENT_DUID);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// hardware address.
+TEST_F(MySqlHostDataSourceTest, get4ByHWaddr) {
+ testGet4ByIdentifier(Host::IDENT_HWADDR);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// hardware address.
+TEST_F(MySqlHostDataSourceTest, get4ByHWaddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGet4ByIdentifier(Host::IDENT_HWADDR);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// DUID.
+TEST_F(MySqlHostDataSourceTest, get4ByDUID) {
+ testGet4ByIdentifier(Host::IDENT_DUID);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// DUID.
+TEST_F(MySqlHostDataSourceTest, get4ByDUIDMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGet4ByIdentifier(Host::IDENT_DUID);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// circuit id.
+TEST_F(MySqlHostDataSourceTest, get4ByCircuitId) {
+ testGet4ByIdentifier(Host::IDENT_CIRCUIT_ID);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// circuit id.
+TEST_F(MySqlHostDataSourceTest, get4ByCircuitIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGet4ByIdentifier(Host::IDENT_CIRCUIT_ID);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// client-id.
+TEST_F(MySqlHostDataSourceTest, get4ByClientId) {
+ testGet4ByIdentifier(Host::IDENT_CLIENT_ID);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// client-id.
+TEST_F(MySqlHostDataSourceTest, get4ByClientIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGet4ByIdentifier(Host::IDENT_CLIENT_ID);
+}
+
+/// @brief Test verifies if hardware address and client identifier are not confused.
+TEST_F(MySqlHostDataSourceTest, hwaddrNotClientId1) {
+ testHWAddrNotClientId();
+}
+
+/// @brief Test verifies if hardware address and client identifier are not confused.
+TEST_F(MySqlHostDataSourceTest, hwaddrNotClientId1MultiThreading) {
+ MultiThreadingTest mt(true);
+ testHWAddrNotClientId();
+}
+
+/// @brief Test verifies if hardware address and client identifier are not confused.
+TEST_F(MySqlHostDataSourceTest, hwaddrNotClientId2) {
+ testClientIdNotHWAddr();
+}
+
+/// @brief Test verifies if hardware address and client identifier are not confused.
+TEST_F(MySqlHostDataSourceTest, hwaddrNotClientId2MultiThreading) {
+ MultiThreadingTest mt(true);
+ testClientIdNotHWAddr();
+}
+
+/// @brief Test verifies if a host with FQDN hostname can be stored and later retrieved.
+TEST_F(MySqlHostDataSourceTest, hostnameFQDN) {
+ testHostname("foo.example.org", 1);
+}
+
+/// @brief Test verifies if a host with FQDN hostname can be stored and later retrieved.
+TEST_F(MySqlHostDataSourceTest, hostnameFQDNMultiThreading) {
+ MultiThreadingTest mt(true);
+ testHostname("foo.example.org", 1);
+}
+
+/// @brief Test verifies if 100 hosts with unique FQDN hostnames can be stored and later
+/// retrieved.
+TEST_F(MySqlHostDataSourceTest, hostnameFQDN100) {
+ testHostname("foo.example.org", 100);
+}
+
+/// @brief Test verifies if 100 hosts with unique FQDN hostnames can be stored and later
+/// retrieved.
+TEST_F(MySqlHostDataSourceTest, hostnameFQDN100MultiThreading) {
+ MultiThreadingTest mt(true);
+ testHostname("foo.example.org", 100);
+}
+
+/// @brief Test verifies if a host without any hostname specified can be stored and later
+/// retrieved.
+TEST_F(MySqlHostDataSourceTest, noHostname) {
+ testHostname("", 1);
+}
+
+/// @brief Test verifies if a host without any hostname specified can be stored and later
+/// retrieved.
+TEST_F(MySqlHostDataSourceTest, noHostnameMultiThreading) {
+ MultiThreadingTest mt(true);
+ testHostname("", 1);
+}
+
+/// @brief Test verifies if a host with user context can be stored and later retrieved.
+TEST_F(MySqlHostDataSourceTest, usercontext) {
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testUserContext(Element::fromJSON(comment));
+}
+
+/// @brief Test verifies if a host with user context can be stored and later retrieved.
+TEST_F(MySqlHostDataSourceTest, usercontextMultiThreading) {
+ MultiThreadingTest mt(true);
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testUserContext(Element::fromJSON(comment));
+}
+
+/// @brief Test verifies if the hardware or client-id query can match hardware address.
+TEST_F(MySqlHostDataSourceTest, DISABLED_hwaddrOrClientId1) {
+ /// @todo: The logic behind ::get4(subnet_id, hwaddr, duid) call needs to
+ /// be discussed.
+ ///
+ /// @todo: Add host reservation with hardware address X, try to retrieve
+ /// host for hardware address X or client identifier Y, verify that the
+ /// reservation is returned.
+}
+
+/// @brief Test verifies if the hardware or client-id query can match hardware address.
+TEST_F(MySqlHostDataSourceTest, DISABLED_hwaddrOrClientId1MultiThreading) {
+ MultiThreadingTest mt(true);
+ /// @todo: The logic behind ::get4(subnet_id, hwaddr, duid) call needs to
+ /// be discussed.
+ ///
+ /// @todo: Add host reservation with hardware address X, try to retrieve
+ /// host for hardware address X or client identifier Y, verify that the
+ /// reservation is returned.
+}
+
+/// @brief Test verifies if the hardware or client-id query can match client-id.
+TEST_F(MySqlHostDataSourceTest, DISABLED_hwaddrOrClientId2) {
+ /// @todo: The logic behind ::get4(subnet_id, hwaddr, duid) call needs to
+ /// be discussed.
+ ///
+ /// @todo: Add host reservation with client identifier Y, try to retrieve
+ /// host for hardware address X or client identifier Y, verify that the
+ /// reservation is returned.
+}
+
+/// @brief Test verifies if the hardware or client-id query can match client-id.
+TEST_F(MySqlHostDataSourceTest, DISABLED_hwaddrOrClientId2MultiThreading) {
+ MultiThreadingTest mt(true);
+ /// @todo: The logic behind ::get4(subnet_id, hwaddr, duid) call needs to
+ /// be discussed.
+ ///
+ /// @todo: Add host reservation with client identifier Y, try to retrieve
+ /// host for hardware address X or client identifier Y, verify that the
+ /// reservation is returned.
+}
+
+/// @brief Test verifies that host with IPv6 address and DUID can be added and
+/// later retrieved by IPv6 address.
+TEST_F(MySqlHostDataSourceTest, get6AddrWithDuid) {
+ testGetByIPv6(Host::IDENT_DUID, false);
+}
+
+/// @brief Test verifies that host with IPv6 address and DUID can be added and
+/// later retrieved by IPv6 address.
+TEST_F(MySqlHostDataSourceTest, get6AddrWithDuidMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetByIPv6(Host::IDENT_DUID, false);
+}
+
+/// @brief Test verifies that host with IPv6 address and HWAddr can be added and
+/// later retrieved by IPv6 address.
+TEST_F(MySqlHostDataSourceTest, get6AddrWithHWAddr) {
+ testGetByIPv6(Host::IDENT_HWADDR, false);
+}
+
+/// @brief Test verifies that host with IPv6 address and HWAddr can be added and
+/// later retrieved by IPv6 address.
+TEST_F(MySqlHostDataSourceTest, get6AddrWithHWAddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetByIPv6(Host::IDENT_HWADDR, false);
+}
+
+/// @brief Test verifies that host with IPv6 prefix and DUID can be added and
+/// later retrieved by IPv6 prefix.
+TEST_F(MySqlHostDataSourceTest, get6PrefixWithDuid) {
+ testGetByIPv6(Host::IDENT_DUID, true);
+}
+
+/// @brief Test verifies that host with IPv6 prefix and DUID can be added and
+/// later retrieved by IPv6 prefix.
+TEST_F(MySqlHostDataSourceTest, get6PrefixWithDuidMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetByIPv6(Host::IDENT_DUID, true);
+}
+
+/// @brief Test verifies that host with IPv6 prefix and HWAddr can be added and
+/// later retrieved by IPv6 prefix.
+TEST_F(MySqlHostDataSourceTest, get6PrefixWithHWaddr) {
+ testGetByIPv6(Host::IDENT_HWADDR, true);
+}
+
+/// @brief Test verifies that host with IPv6 prefix and HWAddr can be added and
+/// later retrieved by IPv6 prefix.
+TEST_F(MySqlHostDataSourceTest, get6PrefixWithHWaddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetByIPv6(Host::IDENT_HWADDR, true);
+}
+
+/// @brief Test verifies that host with IPv6 prefix reservation can be retrieved
+/// by subnet id and prefix value.
+TEST_F(MySqlHostDataSourceTest, get6SubnetPrefix) {
+ testGetBySubnetIPv6();
+}
+
+/// @brief Test verifies that host with IPv6 prefix reservation can be retrieved
+/// by subnet id and prefix value.
+TEST_F(MySqlHostDataSourceTest, get6SubnetPrefixMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetBySubnetIPv6();
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// hardware address.
+TEST_F(MySqlHostDataSourceTest, get6ByHWaddr) {
+ testGet6ByHWAddr();
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// hardware address.
+TEST_F(MySqlHostDataSourceTest, get6ByHWaddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGet6ByHWAddr();
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// client identifier.
+TEST_F(MySqlHostDataSourceTest, get6ByClientId) {
+ testGet6ByClientId();
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// client identifier.
+TEST_F(MySqlHostDataSourceTest, get6ByClientIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGet6ByClientId();
+}
+
+/// @brief Test verifies if a host reservation can be stored with both IPv6 address and
+/// prefix.
+TEST_F(MySqlHostDataSourceTest, addr6AndPrefix) {
+ testAddr6AndPrefix();
+}
+
+/// @brief Test verifies if a host reservation can be stored with both IPv6 address and
+/// prefix.
+TEST_F(MySqlHostDataSourceTest, addr6AndPrefixMultiThreading) {
+ MultiThreadingTest mt(true);
+ testAddr6AndPrefix();
+}
+
+/// @brief Tests if host with multiple IPv6 reservations can be added and then
+/// retrieved correctly. Test checks reservations comparing.
+TEST_F(MySqlHostDataSourceTest, multipleReservations) {
+ testMultipleReservations();
+}
+
+/// @brief Tests if host with multiple IPv6 reservations can be added and then
+/// retrieved correctly. Test checks reservations comparing.
+TEST_F(MySqlHostDataSourceTest, multipleReservationsMultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleReservations();
+}
+
+/// @brief Tests if compareIPv6Reservations() method treats same pool of reservations
+/// but added in different order as equal.
+TEST_F(MySqlHostDataSourceTest, multipleReservationsDifferentOrder) {
+ testMultipleReservationsDifferentOrder();
+}
+
+/// @brief Tests if compareIPv6Reservations() method treats same pool of reservations
+/// but added in different order as equal.
+TEST_F(MySqlHostDataSourceTest, multipleReservationsDifferentOrderMultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleReservationsDifferentOrder();
+}
+
+/// @brief Test that multiple client classes for IPv4 can be inserted and
+/// retrieved for a given host reservation.
+TEST_F(MySqlHostDataSourceTest, multipleClientClasses4) {
+ testMultipleClientClasses4();
+}
+
+/// @brief Test that multiple client classes for IPv4 can be inserted and
+/// retrieved for a given host reservation.
+TEST_F(MySqlHostDataSourceTest, multipleClientClasses4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleClientClasses4();
+}
+
+/// @brief Test that multiple client classes for IPv6 can be inserted and
+/// retrieved for a given host reservation.
+TEST_F(MySqlHostDataSourceTest, multipleClientClasses6) {
+ testMultipleClientClasses6();
+}
+
+/// @brief Test that multiple client classes for IPv6 can be inserted and
+/// retrieved for a given host reservation.
+TEST_F(MySqlHostDataSourceTest, multipleClientClasses6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleClientClasses6();
+}
+
+/// @brief Test that multiple client classes for both IPv4 and IPv6 can
+/// be inserted and retrieved for a given host reservation.
+TEST_F(MySqlHostDataSourceTest, multipleClientClassesBoth) {
+ testMultipleClientClassesBoth();
+}
+
+/// @brief Test that multiple client classes for both IPv4 and IPv6 can
+/// be inserted and retrieved for a given host reservation.
+TEST_F(MySqlHostDataSourceTest, multipleClientClassesBothMultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleClientClassesBoth();
+}
+
+/// @brief Test if the same host can have reservations in different subnets (with the
+/// same hardware address). The test logic is as follows:
+/// Insert 10 host reservations for a given physical host (the same
+/// hardware address), but for different subnets (different subnet-ids).
+/// Make sure that getAll() returns them all correctly.
+TEST_F(MySqlHostDataSourceTest, multipleSubnetsHWAddr) {
+ testMultipleSubnets(10, Host::IDENT_HWADDR);
+}
+
+/// @brief Test if the same host can have reservations in different subnets (with the
+/// same hardware address). The test logic is as follows:
+/// Insert 10 host reservations for a given physical host (the same
+/// hardware address), but for different subnets (different subnet-ids).
+/// Make sure that getAll() returns them all correctly.
+TEST_F(MySqlHostDataSourceTest, multipleSubnetsHWAddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleSubnets(10, Host::IDENT_HWADDR);
+}
+
+/// @brief Test if the same host can have reservations in different subnets (with the
+/// same client identifier). The test logic is as follows:
+///
+/// Insert 10 host reservations for a given physical host (the same
+/// client-identifier), but for different subnets (different subnet-ids).
+/// Make sure that getAll() returns them correctly.
+TEST_F(MySqlHostDataSourceTest, multipleSubnetsClientId) {
+ testMultipleSubnets(10, Host::IDENT_DUID);
+}
+
+/// @brief Test if the same host can have reservations in different subnets (with the
+/// same client identifier). The test logic is as follows:
+///
+/// Insert 10 host reservations for a given physical host (the same
+/// client-identifier), but for different subnets (different subnet-ids).
+/// Make sure that getAll() returns them correctly.
+TEST_F(MySqlHostDataSourceTest, multipleSubnetsClientIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleSubnets(10, Host::IDENT_DUID);
+}
+
+/// @brief Test if host reservations made for different IPv6 subnets are handled correctly.
+/// The test logic is as follows:
+///
+/// Insert 10 host reservations for different subnets. Make sure that
+/// get6(subnet-id, ...) calls return correct reservation.
+TEST_F(MySqlHostDataSourceTest, subnetId6) {
+ testSubnetId6(10, Host::IDENT_HWADDR);
+}
+
+/// @brief Test if host reservations made for different IPv6 subnets are handled correctly.
+/// The test logic is as follows:
+///
+/// Insert 10 host reservations for different subnets. Make sure that
+/// get6(subnet-id, ...) calls return correct reservation.
+TEST_F(MySqlHostDataSourceTest, subnetId6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testSubnetId6(10, Host::IDENT_HWADDR);
+}
+
+/// @brief Test if the duplicate host instances can't be inserted. The test logic is as
+/// follows: try to add multiple instances of the same host reservation and
+/// verify that the second and following attempts will throw exceptions.
+/// Hosts with same DUID.
+TEST_F(MySqlHostDataSourceTest, addDuplicate6WithDUID) {
+ testAddDuplicate6WithSameDUID();
+}
+
+/// @brief Test if the duplicate host instances can't be inserted. The test logic is as
+/// follows: try to add multiple instances of the same host reservation and
+/// verify that the second and following attempts will throw exceptions.
+/// Hosts with same DUID.
+TEST_F(MySqlHostDataSourceTest, addDuplicate6WithDUIDMultiThreading) {
+ MultiThreadingTest mt(true);
+ testAddDuplicate6WithSameDUID();
+}
+
+/// @brief Test if the duplicate host instances can't be inserted. The test logic is as
+/// follows: try to add multiple instances of the same host reservation and
+/// verify that the second and following attempts will throw exceptions.
+/// Hosts with same HWAddr.
+TEST_F(MySqlHostDataSourceTest, addDuplicate6WithHWAddr) {
+ testAddDuplicate6WithSameHWAddr();
+}
+
+/// @brief Test if the duplicate host instances can't be inserted. The test logic is as
+/// follows: try to add multiple instances of the same host reservation and
+/// verify that the second and following attempts will throw exceptions.
+/// Hosts with same HWAddr.
+TEST_F(MySqlHostDataSourceTest, addDuplicate6WithHWAddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testAddDuplicate6WithSameHWAddr();
+}
+
+/// @brief Test if the same IPv6 reservation can't be inserted multiple times.
+TEST_F(MySqlHostDataSourceTest, addDuplicateIPv6) {
+ testAddDuplicateIPv6();
+}
+
+/// @brief Test if the same IPv6 reservation can't be inserted multiple times.
+TEST_F(MySqlHostDataSourceTest, addDuplicateIPv6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testAddDuplicateIPv6();
+}
+
+/// @brief Test if the host reservation for the same IPv6 address can be inserted
+/// multiple times when allowed by the configuration and when the host identifier
+/// is different.
+TEST_F(MySqlHostDataSourceTest, allowDuplicateIPv6) {
+ testAllowDuplicateIPv6();
+}
+
+/// @brief Test if the host reservation for the same IPv6 address can be inserted
+/// multiple times when allowed by the configuration and when the host identifier
+/// is different.
+TEST_F(MySqlHostDataSourceTest, allowDuplicateIPv6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testAllowDuplicateIPv6();
+}
+
+/// @brief Test if the duplicate IPv4 host instances can't be inserted. The test logic is as
+/// follows: try to add multiple instances of the same host reservation and
+/// verify that the second and following attempts will throw exceptions.
+TEST_F(MySqlHostDataSourceTest, addDuplicateIPv4) {
+ testAddDuplicateIPv4();
+}
+
+/// @brief Test if the duplicate IPv4 host instances can't be inserted. The test logic is as
+/// follows: try to add multiple instances of the same host reservation and
+/// verify that the second and following attempts will throw exceptions.
+TEST_F(MySqlHostDataSourceTest, addDuplicateIPv4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testAddDuplicateIPv4();
+}
+
+/// @brief Test if the host reservation for the same IPv4 address can be inserted
+/// multiple times when allowed by the configuration and when the host identifier
+/// is different.
+TEST_F(MySqlHostDataSourceTest, allowDuplicateIPv4) {
+ testAllowDuplicateIPv4();
+}
+
+/// @brief Test if the host reservation for the same IPv4 address can be inserted
+/// multiple times when allowed by the configuration and when the host identifier
+/// is different.
+TEST_F(MySqlHostDataSourceTest, allowDuplicateIPv4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testAllowDuplicateIPv4();
+}
+
+/// @brief This test verifies that DHCPv4 options can be inserted in a binary format
+/// and retrieved from the MySQL host database.
+TEST_F(MySqlHostDataSourceTest, optionsReservations4) {
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations4(false, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv4 options can be inserted in a binary format
+/// and retrieved from the MySQL host database.
+TEST_F(MySqlHostDataSourceTest, optionsReservations4MultiThreading) {
+ MultiThreadingTest mt(true);
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations4(false, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv6 options can be inserted in a binary format
+/// and retrieved from the MySQL host database.
+TEST_F(MySqlHostDataSourceTest, optionsReservations6) {
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations6(false, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv6 options can be inserted in a binary format
+/// and retrieved from the MySQL host database.
+TEST_F(MySqlHostDataSourceTest, optionsReservations6MultiThreading) {
+ MultiThreadingTest mt(true);
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations6(false, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv4 and DHCPv6 options can be inserted in a
+/// binary format and retrieved with a single query to the database.
+TEST_F(MySqlHostDataSourceTest, optionsReservations46) {
+ testOptionsReservations46(false);
+}
+
+/// @brief This test verifies that DHCPv4 and DHCPv6 options can be inserted in a
+/// binary format and retrieved with a single query to the database.
+TEST_F(MySqlHostDataSourceTest, optionsReservations46MultiThreading) {
+ MultiThreadingTest mt(true);
+ testOptionsReservations46(false);
+}
+
+/// @brief This test verifies that DHCPv4 options can be inserted in a textual format
+/// and retrieved from the MySQL host database.
+TEST_F(MySqlHostDataSourceTest, formattedOptionsReservations4) {
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations4(true, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv4 options can be inserted in a textual format
+/// and retrieved from the MySQL host database.
+TEST_F(MySqlHostDataSourceTest, formattedOptionsReservations4MultiThreading) {
+ MultiThreadingTest mt(true);
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations4(true, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv6 options can be inserted in a textual format
+/// and retrieved from the MySQL host database.
+TEST_F(MySqlHostDataSourceTest, formattedOptionsReservations6) {
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations6(true, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv6 options can be inserted in a textual format
+/// and retrieved from the MySQL host database.
+TEST_F(MySqlHostDataSourceTest, formattedOptionsReservations6MultiThreading) {
+ MultiThreadingTest mt(true);
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations6(true, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv4 and DHCPv6 options can be inserted in a
+/// textual format and retrieved with a single query to the database.
+TEST_F(MySqlHostDataSourceTest, formattedOptionsReservations46) {
+ testOptionsReservations46(true);
+}
+
+/// @brief This test verifies that DHCPv4 and DHCPv6 options can be inserted in a
+/// textual format and retrieved with a single query to the database.
+TEST_F(MySqlHostDataSourceTest, formattedOptionsReservations46MultiThreading) {
+ MultiThreadingTest mt(true);
+ testOptionsReservations46(true);
+}
+
+/// @brief This test checks transactional insertion of the host information
+/// into the database. The failure to insert host information at
+/// any stage should cause the whole transaction to be rolled back.
+TEST_F(MySqlHostDataSourceTest, testAddRollback) {
+ // Make sure we have the pointer to the host data source.
+ ASSERT_TRUE(hdsptr_);
+
+ // To test the transaction rollback mechanism we need to cause the
+ // insertion of host information to fail at some stage. The 'hosts'
+ // table should be updated correctly but the failure should occur
+ // when inserting reservations or options. The simplest way to
+ // achieve that is to simply drop one of the tables. To do so, we
+ // connect to the database and issue a DROP query.
+ DatabaseConnection::ParameterMap params;
+ params["name"] = "keatest";
+ params["user"] = "keatest";
+ params["password"] = "keatest";
+ MySqlConnection conn(params);
+ ASSERT_NO_THROW(conn.openDatabase());
+
+ int status = MysqlQuery(conn.mysql_,
+ "DROP TABLE IF EXISTS ipv6_reservations");
+ ASSERT_EQ(0, status) << mysql_error(conn.mysql_);
+
+ // Create a host with a reservation.
+ HostPtr host = HostDataSourceUtils::initializeHost6("2001:db8:1::1",
+ Host::IDENT_HWADDR, false);
+ // Let's assign some DHCPv4 subnet to the host, because we will use the
+ // DHCPv4 subnet to try to retrieve the host after failed insertion.
+ host->setIPv4SubnetID(SubnetID(4));
+
+ // There is no ipv6_reservations table, so the insertion should fail.
+ ASSERT_THROW(hdsptr_->add(host), DbOperationError);
+
+ // Even though we have created a DHCPv6 host, we can't use get6()
+ // method to retrieve the host from the database, because the
+ // query would expect that the ipv6_reservations table is present.
+ // Therefore, the query would fail. Instead, we use the get4 method
+ // which uses the same client identifier, but doesn't attempt to
+ // retrieve the data from ipv6_reservations table. The query should
+ // pass but return no host because the (insert) transaction is expected
+ // to be rolled back.
+ ConstHostPtr from_hds = hdsptr_->get4(host->getIPv4SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ EXPECT_FALSE(from_hds);
+}
+
+/// @brief This test checks transactional insertion of the host information
+/// into the database. The failure to insert host information at
+/// any stage should cause the whole transaction to be rolled back.
+TEST_F(MySqlHostDataSourceTest, testAddRollbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ // Make sure we have the pointer to the host data source.
+ ASSERT_TRUE(hdsptr_);
+
+ // To test the transaction rollback mechanism we need to cause the
+ // insertion of host information to fail at some stage. The 'hosts'
+ // table should be updated correctly but the failure should occur
+ // when inserting reservations or options. The simplest way to
+ // achieve that is to simply drop one of the tables. To do so, we
+ // connect to the database and issue a DROP query.
+ DatabaseConnection::ParameterMap params;
+ params["name"] = "keatest";
+ params["user"] = "keatest";
+ params["password"] = "keatest";
+ MySqlConnection conn(params);
+ ASSERT_NO_THROW(conn.openDatabase());
+
+ int status = MysqlQuery(conn.mysql_,
+ "DROP TABLE IF EXISTS ipv6_reservations");
+ ASSERT_EQ(0, status) << mysql_error(conn.mysql_);
+
+ // Create a host with a reservation.
+ HostPtr host = HostDataSourceUtils::initializeHost6("2001:db8:1::1",
+ Host::IDENT_HWADDR, false);
+ // Let's assign some DHCPv4 subnet to the host, because we will use the
+ // DHCPv4 subnet to try to retrieve the host after failed insertion.
+ host->setIPv4SubnetID(SubnetID(4));
+
+ // There is no ipv6_reservations table, so the insertion should fail.
+ ASSERT_THROW(hdsptr_->add(host), DbOperationError);
+
+ // Even though we have created a DHCPv6 host, we can't use get6()
+ // method to retrieve the host from the database, because the
+ // query would expect that the ipv6_reservations table is present.
+ // Therefore, the query would fail. Instead, we use the get4 method
+ // which uses the same client identifier, but doesn't attempt to
+ // retrieve the data from ipv6_reservations table. The query should
+ // pass but return no host because the (insert) transaction is expected
+ // to be rolled back.
+ ConstHostPtr from_hds = hdsptr_->get4(host->getIPv4SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ EXPECT_FALSE(from_hds);
+}
+
+/// @brief This test checks that siaddr, sname, file fields can be retrieved
+/// from a database for a host.
+TEST_F(MySqlHostDataSourceTest, messageFields) {
+ testMessageFields4();
+}
+
+/// @brief This test checks that siaddr, sname, file fields can be retrieved
+/// from a database for a host.
+TEST_F(MySqlHostDataSourceTest, messageFieldsMultiThreading) {
+ MultiThreadingTest mt(true);
+ testMessageFields4();
+}
+
+/// @brief Check that delete(subnet-id, addr4) works.
+TEST_F(MySqlHostDataSourceTest, deleteByAddr4) {
+ testDeleteByAddr4();
+}
+
+/// @brief Check that delete(subnet-id, addr4) works.
+TEST_F(MySqlHostDataSourceTest, deleteByAddr4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testDeleteByAddr4();
+}
+
+/// @brief Check that delete(subnet4-id, identifier-type, identifier) works.
+TEST_F(MySqlHostDataSourceTest, deleteById4) {
+ testDeleteById4();
+}
+
+/// @brief Check that delete(subnet4-id, identifier-type, identifier) works.
+TEST_F(MySqlHostDataSourceTest, deleteById4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testDeleteById4();
+}
+
+/// @brief Check that delete(subnet4-id, identifier-type, identifier) works,
+/// even when options are present.
+TEST_F(MySqlHostDataSourceTest, deleteById4Options) {
+ testDeleteById4Options();
+}
+
+/// @brief Check that delete(subnet4-id, identifier-type, identifier) works,
+/// even when options are present.
+TEST_F(MySqlHostDataSourceTest, deleteById4OptionsMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDeleteById4Options();
+}
+
+/// @brief Check that delete(subnet6-id, identifier-type, identifier) works.
+TEST_F(MySqlHostDataSourceTest, deleteById6) {
+ testDeleteById6();
+}
+
+/// @brief Check that delete(subnet6-id, identifier-type, identifier) works.
+TEST_F(MySqlHostDataSourceTest, deleteById6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testDeleteById6();
+}
+
+/// @brief Check that delete(subnet6-id, identifier-type, identifier) works,
+/// even when options are present.
+TEST_F(MySqlHostDataSourceTest, deleteById6Options) {
+ testDeleteById6Options();
+}
+
+/// @brief Check that delete(subnet6-id, identifier-type, identifier) works,
+/// even when options are present.
+TEST_F(MySqlHostDataSourceTest, deleteById6OptionsMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDeleteById6Options();
+}
+
+/// @brief Tests that multiple reservations without IPv4 addresses can be
+/// specified within a subnet.
+TEST_F(MySqlHostDataSourceTest, testMultipleHostsNoAddress4) {
+ testMultipleHostsNoAddress4();
+}
+
+/// @brief Tests that multiple reservations without IPv4 addresses can be
+/// specified within a subnet.
+TEST_F(MySqlHostDataSourceTest, testMultipleHostsNoAddress4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleHostsNoAddress4();
+}
+
+/// @brief Tests that multiple hosts can be specified within an IPv6 subnet.
+TEST_F(MySqlHostDataSourceTest, testMultipleHosts6) {
+ testMultipleHosts6();
+}
+
+/// @brief Tests that multiple hosts can be specified within an IPv6 subnet.
+TEST_F(MySqlHostDataSourceTest, testMultipleHosts6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleHosts6();
+}
+
+/// @brief Test fixture class for validating @c HostMgr using
+/// MySQL as alternate host data source.
+class MySQLHostMgrTest : public HostMgrTest {
+protected:
+
+ /// @brief Build MySQL schema for a test.
+ virtual void SetUp();
+
+ /// @brief Rollback and drop MySQL schema after the test.
+ virtual void TearDown();
+};
+
+void
+MySQLHostMgrTest::SetUp() {
+ HostMgrTest::SetUp();
+
+ // Ensure we have the proper schema with no transient data.
+ db::test::createMySQLSchema();
+
+ // Connect to the database
+ try {
+ HostMgr::addBackend(db::test::validMySQLConnectionString());
+ } catch (...) {
+ std::cerr << "*** ERROR: unable to open database. The test\n"
+ "*** environment is broken and must be fixed before\n"
+ "*** the MySQL tests will run correctly.\n"
+ "*** The reason for the problem is described in the\n"
+ "*** accompanying exception output.\n";
+ throw;
+ }
+}
+
+void
+MySQLHostMgrTest::TearDown() {
+ try {
+ HostMgr::instance().getHostDataSource()->rollback();
+ } catch(...) {
+ // we don't care if we aren't in a transaction.
+ }
+
+ HostMgr::delBackend("mysql");
+ // If data wipe enabled, delete transient data otherwise destroy the schema
+ db::test::destroyMySQLSchema();
+}
+
+/// @brief Test fixture class for validating @c HostMgr using
+/// MySQL as alternate host data source and MySQL connectivity loss.
+class MySQLHostMgrDbLostCallbackTest : public HostMgrDbLostCallbackTest {
+public:
+ virtual void destroySchema() {
+ // If data wipe enabled, delete transient data otherwise destroy the schema
+ db::test::destroyMySQLSchema();
+ }
+
+ virtual void createSchema() {
+ // Ensure we have the proper schema with no transient data.
+ db::test::createMySQLSchema();
+ }
+
+ virtual std::string validConnectString() {
+ return (db::test::validMySQLConnectionString());
+ }
+
+ virtual std::string invalidConnectString() {
+ return (connectionString(MYSQL_VALID_TYPE, INVALID_NAME, VALID_HOST,
+ VALID_USER, VALID_PASSWORD));
+ }
+};
+
+// This test verifies that reservations for a particular client can
+// be retrieved from the configuration file and a database simultaneously.
+TEST_F(MySQLHostMgrTest, getAll) {
+ testGetAll(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that reservations for a particular subnet can
+// be retrieved from the configuration file and a database simultaneously.
+TEST_F(MySQLHostMgrTest, getAll4BySubnet) {
+ testGetAll4BySubnet(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that reservations for a particular subnet can
+// be retrieved from the configuration file and a database simultaneously.
+TEST_F(MySQLHostMgrTest, getAll6BySubnet) {
+ testGetAll6BySubnet(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that HostMgr returns all reservations for the specified
+// IPv4 subnet and reserved address.
+TEST_F(MySQLHostMgrTest, getAll4BySubnetIP) {
+ testGetAll4BySubnetIP(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that HostMgr returns all reservations for the specified
+// IPv6 subnet and reserved address.
+TEST_F(MySQLHostMgrTest, getAll6BySubnetIP) {
+ testGetAll6BySubnetIP(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that reservations for a particular hostname can be
+// retrieved from the configuration file and a database simultaneously.
+TEST_F(MySQLHostMgrTest, getAllbyHostname) {
+ testGetAllbyHostname(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that reservations for a particular hostname and
+// DHCPv4 subnet can be retrieved from the configuration file and a
+// database simultaneously.
+TEST_F(MySQLHostMgrTest, getAllbyHostnameSubnet4) {
+ testGetAllbyHostnameSubnet4(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that reservations for a particular hostname and
+// DHCPv6 subnet can be retrieved from the configuration file and a
+// database simultaneously.
+TEST_F(MySQLHostMgrTest, getAllbyHostnameSubnet6) {
+ testGetAllbyHostnameSubnet6(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that reservations for a particular subnet can
+// be retrieved by pages from the configuration file and a database
+// simultaneously.
+TEST_F(MySQLHostMgrTest, getPage4) {
+ testGetPage4(true);
+}
+
+// This test verifies that all v4 reservations be retrieved by pages
+// from the configuration file and a database simultaneously.
+TEST_F(MySQLHostMgrTest, getPage4All) {
+ testGetPage4All(true);
+}
+
+// This test verifies that reservations for a particular subnet can
+// be retrieved by pages from the configuration file and a database
+// simultaneously.
+TEST_F(MySQLHostMgrTest, getPage6) {
+ testGetPage6(true);
+}
+
+// This test verifies that all v6 reservations be retrieved by pages
+// from the configuration file and a database simultaneously.
+TEST_F(MySQLHostMgrTest, getPage6All) {
+ testGetPage6All(true);
+}
+
+// This test verifies that IPv4 reservations for a particular client can
+// be retrieved from the configuration file and a database simultaneously.
+TEST_F(MySQLHostMgrTest, getAll4) {
+ testGetAll4(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that the IPv4 reservation can be retrieved from a
+// database.
+TEST_F(MySQLHostMgrTest, get4) {
+ testGet4(HostMgr::instance());
+}
+
+// This test verifies that the IPv6 reservation can be retrieved from a
+// database.
+TEST_F(MySQLHostMgrTest, get6) {
+ testGet6(HostMgr::instance());
+}
+
+// This test verifies that the IPv6 prefix reservation can be retrieved
+// from a configuration file and a database.
+TEST_F(MySQLHostMgrTest, get6ByPrefix) {
+ testGet6ByPrefix(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that it is possible to control whether the reserved
+// IP addresses are unique or non unique via the HostMgr.
+TEST_F(MySQLHostMgrTest, setIPReservationsUnique) {
+ EXPECT_TRUE(HostMgr::instance().setIPReservationsUnique(true));
+ EXPECT_TRUE(HostMgr::instance().setIPReservationsUnique(false));
+}
+
+/// @brief Verifies that db lost callback is not invoked on an open failure
+TEST_F(MySQLHostMgrDbLostCallbackTest, testNoCallbackOnOpenFailure) {
+ MultiThreadingTest mt(false);
+ testNoCallbackOnOpenFailure();
+}
+
+/// @brief Verifies that db lost callback is not invoked on an open failure
+TEST_F(MySQLHostMgrDbLostCallbackTest, testNoCallbackOnOpenFailureMultiThreading) {
+ MultiThreadingTest mt(true);
+ testNoCallbackOnOpenFailure();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySQLHostMgrDbLostCallbackTest, testDbLostAndRecoveredCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndRecoveredCallback();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySQLHostMgrDbLostCallbackTest, testDbLostAndRecoveredCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndRecoveredCallback();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySQLHostMgrDbLostCallbackTest, testDbLostAndFailedCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndFailedCallback();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySQLHostMgrDbLostCallbackTest, testDbLostAndFailedCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndFailedCallback();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySQLHostMgrDbLostCallbackTest, testDbLostAndRecoveredAfterTimeoutCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndRecoveredAfterTimeoutCallback();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySQLHostMgrDbLostCallbackTest, testDbLostAndRecoveredAfterTimeoutCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndRecoveredAfterTimeoutCallback();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySQLHostMgrDbLostCallbackTest, testDbLostAndFailedAfterTimeoutCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndFailedAfterTimeoutCallback();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySQLHostMgrDbLostCallbackTest, testDbLostAndFailedAfterTimeoutCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndFailedAfterTimeoutCallback();
+}
+
+} // namespace
diff --git a/src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc b/src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc
new file mode 100644
index 0000000..9383db8
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc
@@ -0,0 +1,1161 @@
+// Copyright (C) 2012-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/mysql_lease_mgr.h>
+#include <dhcpsrv/testutils/test_utils.h>
+#include <dhcpsrv/tests/generic_lease_mgr_unittest.h>
+#include <exceptions/exceptions.h>
+#include <mysql/mysql_connection.h>
+#include <mysql/testutils/mysql_schema.h>
+#include <testutils/gtest_utils.h>
+#include <testutils/multi_threading_utils.h>
+#include <util/multi_threading_mgr.h>
+
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <utility>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::db;
+using namespace isc::db::test;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::test;
+using namespace isc::util;
+using namespace std;
+
+namespace {
+
+
+/// @brief Test fixture class for testing MySQL Lease Manager
+///
+/// Opens the database prior to each test and closes it afterwards.
+/// All pending transactions are deleted prior to closure.
+
+class MySqlLeaseMgrTest : public GenericLeaseMgrTest {
+public:
+ /// @brief Clears the database and opens connection to it.
+ void initializeTest() {
+ // Ensure we have the proper schema with no transient data.
+ createMySQLSchema();
+
+ // Connect to the database
+ try {
+ LeaseMgrFactory::create(validMySQLConnectionString());
+ } catch (...) {
+ std::cerr << "*** ERROR: unable to open database. The test\n"
+ "*** environment is broken and must be fixed before\n"
+ "*** the MySQL tests will run correctly.\n"
+ "*** The reason for the problem is described in the\n"
+ "*** accompanying exception output.\n";
+ throw;
+ }
+
+ lmptr_ = &(LeaseMgrFactory::instance());
+
+ MultiThreadingMgr::instance().setMode(false);
+ }
+
+ /// @brief Destroys the LM and the schema.
+ void destroyTest() {
+ LeaseMgrFactory::destroy();
+ // If data wipe enabled, delete transient data otherwise destroy the schema
+ destroyMySQLSchema();
+
+ // Disable Multi-Threading.
+ MultiThreadingMgr::instance().setMode(false);
+ }
+
+ /// @brief Constructor
+ ///
+ /// Deletes everything from the database and opens it.
+ MySqlLeaseMgrTest() {
+ initializeTest();
+ }
+
+ /// @brief Destructor
+ ///
+ /// Rolls back all pending transactions. The deletion of lmptr_ will close
+ /// the database. Then reopen it and delete everything created by the test.
+ virtual ~MySqlLeaseMgrTest() {
+ destroyTest();
+ }
+
+ /// @brief Reopen the database
+ ///
+ /// Closes the database and re-open it. Anything committed should be
+ /// visible.
+ ///
+ /// Parameter is ignored for MySQL backend as the v4 and v6 leases share
+ /// the same database.
+ void reopen(Universe) {
+ LeaseMgrFactory::destroy();
+ LeaseMgrFactory::create(validMySQLConnectionString());
+ lmptr_ = &(LeaseMgrFactory::instance());
+ }
+};
+
+/// @brief Check that database can be opened
+///
+/// This test checks if the MySqlLeaseMgr can be instantiated. This happens
+/// only if the database can be opened. Note that this is not part of the
+/// MySqlLeaseMgr test fixture set. This test checks that the database can be
+/// opened: the fixtures assume that and check basic operations.
+TEST(MySqlOpenTest, OpenDatabase) {
+ // Explicitly disable Multi-Threading.
+ MultiThreadingMgr::instance().setMode(false);
+
+ // Schema needs to be created for the test to work.
+ createMySQLSchema(true);
+
+ // Check that lease manager opens the database correctly and tidy up. If it
+ // fails, print the error message.
+ try {
+ LeaseMgrFactory::create(validMySQLConnectionString());
+ EXPECT_NO_THROW((void)LeaseMgrFactory::instance());
+ LeaseMgrFactory::destroy();
+ } catch (const isc::Exception& ex) {
+ FAIL() << "*** ERROR: unable to open database, reason:\n"
+ << " " << ex.what() << "\n"
+ << "*** The test environment is broken and must be fixed\n"
+ << "*** before the MySQL tests will run correctly.\n";
+ }
+
+ // Check that lease manager opens the database correctly with a longer
+ // timeout. If it fails, print the error message.
+ try {
+ string connection_string = validMySQLConnectionString() + string(" ") +
+ string(VALID_TIMEOUT);
+ LeaseMgrFactory::create(connection_string);
+ EXPECT_NO_THROW((void) LeaseMgrFactory::instance());
+ LeaseMgrFactory::destroy();
+ } catch (const isc::Exception& ex) {
+ FAIL() << "*** ERROR: unable to open database, reason:\n"
+ << " " << ex.what() << "\n"
+ << "*** The test environment is broken and must be fixed\n"
+ << "*** before the MySQL tests will run correctly.\n";
+ }
+
+ // Check that attempting to get an instance of the lease manager when
+ // none is set throws an exception.
+ EXPECT_THROW(LeaseMgrFactory::instance(), NoLeaseManager);
+
+ // Check that wrong specification of backend throws an exception.
+ // (This is really a check on LeaseMgrFactory, but is convenient to
+ // perform here.)
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ NULL, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ InvalidParameter);
+
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ INVALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)),
+ InvalidType);
+
+ // Check that invalid login data causes an exception.
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ MYSQL_VALID_TYPE, INVALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+
+#ifndef OS_OSX
+ // Under MacOS, connecting with an invalid host can cause a TCP/IP socket
+ // to be orphaned and never closed. This can interfere with subsequent tests
+ // which attempt to locate and manipulate MySQL client socket descriptor.
+ // In the interests of progress, we'll just avoid this test.
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, INVALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+#endif
+
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, INVALID_PASSWORD)),
+ DbOpenError);
+
+ // Check for invalid timeouts
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD, INVALID_TIMEOUT_1)),
+ DbInvalidTimeout);
+
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD, INVALID_TIMEOUT_2)),
+ DbInvalidTimeout);
+
+ // Check for missing parameters
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ MYSQL_VALID_TYPE, NULL, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ NoDatabaseName);
+
+ // Tidy up after the test
+ destroyMySQLSchema(true);
+ LeaseMgrFactory::destroy();
+}
+
+/// @brief Check that database can be opened with Multi-Threading
+TEST(MySqlOpenTest, OpenDatabaseMultiThreading) {
+ // Enable Multi-Threading.
+ MultiThreadingTest mt(true);
+
+ // Schema needs to be created for the test to work.
+ createMySQLSchema(true);
+
+ // Check that lease manager opens the database correctly and tidy up. If it
+ // fails, print the error message.
+ try {
+ LeaseMgrFactory::create(validMySQLConnectionString());
+ EXPECT_NO_THROW((void)LeaseMgrFactory::instance());
+ LeaseMgrFactory::destroy();
+ } catch (const isc::Exception& ex) {
+ FAIL() << "*** ERROR: unable to open database, reason:\n"
+ << " " << ex.what() << "\n"
+ << "*** The test environment is broken and must be fixed\n"
+ << "*** before the MySQL tests will run correctly.\n";
+ }
+
+ // Tidy up after the test
+ destroyMySQLSchema(true);
+ LeaseMgrFactory::destroy();
+}
+
+/// @brief Check the getType() method
+///
+/// getType() returns a string giving the type of the backend, which should
+/// always be "mysql".
+TEST_F(MySqlLeaseMgrTest, getType) {
+ EXPECT_EQ(std::string("mysql"), lmptr_->getType());
+}
+
+/// @brief Check conversion functions
+///
+/// The server works using cltt and valid_filetime. In the database, the
+/// information is stored as expire_time and valid-lifetime, which are
+/// related by
+///
+/// expire_time = cltt + valid_lifetime
+///
+/// This test checks that the conversion is correct. It does not check that the
+/// data is entered into the database correctly, only that the MYSQL_TIME
+/// structure used for the entry is correctly set up.
+TEST_F(MySqlLeaseMgrTest, checkTimeConversion) {
+ const time_t cltt = time(NULL);
+ const uint32_t valid_lft = 86400; // 1 day
+ struct tm tm_expire;
+ MYSQL_TIME mysql_expire;
+
+ // Work out what the broken-down time will be for one day
+ // after the current time.
+ time_t expire_time = cltt + valid_lft;
+ (void) localtime_r(&expire_time, &tm_expire);
+
+ // Convert to the database time
+ MySqlConnection::convertToDatabaseTime(cltt, valid_lft, mysql_expire);
+
+ // Are the times the same?
+ EXPECT_EQ(tm_expire.tm_year + 1900, mysql_expire.year);
+ EXPECT_EQ(tm_expire.tm_mon + 1, mysql_expire.month);
+ EXPECT_EQ(tm_expire.tm_mday, mysql_expire.day);
+ EXPECT_EQ(tm_expire.tm_hour, mysql_expire.hour);
+ EXPECT_EQ(tm_expire.tm_min, mysql_expire.minute);
+ EXPECT_EQ(tm_expire.tm_sec, mysql_expire.second);
+ EXPECT_EQ(0, mysql_expire.second_part);
+ EXPECT_EQ(0, mysql_expire.neg);
+
+ // Convert back
+ time_t converted_cltt = 0;
+ MySqlConnection::convertFromDatabaseTime(mysql_expire, valid_lft, converted_cltt);
+ EXPECT_EQ(cltt, converted_cltt);
+}
+
+/// @brief Check getName() returns correct database name
+TEST_F(MySqlLeaseMgrTest, getName) {
+ EXPECT_EQ(std::string("keatest"), lmptr_->getName());
+}
+
+/// @brief Check that getVersion() returns the expected version
+TEST_F(MySqlLeaseMgrTest, checkVersion) {
+ // Check version
+ pair<uint32_t, uint32_t> version;
+ ASSERT_NO_THROW(version = lmptr_->getVersion());
+ EXPECT_EQ(MYSQL_SCHEMA_VERSION_MAJOR, version.first);
+ EXPECT_EQ(MYSQL_SCHEMA_VERSION_MINOR, version.second);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+/// LEASE4 /////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Basic Lease4 Checks
+///
+/// Checks that the addLease, getLease4 (by address) and deleteLease (with an
+/// IPv4 address) works.
+TEST_F(MySqlLeaseMgrTest, basicLease4) {
+ testBasicLease4();
+}
+
+/// @brief Basic Lease4 Checks
+TEST_F(MySqlLeaseMgrTest, basicLease4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testBasicLease4();
+}
+
+/// @brief Check that Lease4 code safely handles invalid dates.
+TEST_F(MySqlLeaseMgrTest, maxDate4) {
+ testMaxDate4();
+}
+
+/// @brief Check that Lease4 code safely handles invalid dates.
+TEST_F(MySqlLeaseMgrTest, maxDate4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMaxDate4();
+}
+
+/// @brief checks that infinite lifetimes do not overflow.
+TEST_F(MySqlLeaseMgrTest, infiniteLifeTime4) {
+ testInfiniteLifeTime4();
+}
+
+/// @brief Lease4 update tests
+///
+/// Checks that we are able to update a lease in the database.
+TEST_F(MySqlLeaseMgrTest, updateLease4) {
+ testUpdateLease4();
+}
+
+/// @brief Lease4 update tests
+TEST_F(MySqlLeaseMgrTest, updateLease4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testUpdateLease4();
+}
+
+/// @brief Lease4 concurrent update tests
+///
+/// Checks that we are not able to concurrently update a lease in the database.
+TEST_F(MySqlLeaseMgrTest, concurrentUpdateLease4) {
+ testConcurrentUpdateLease4();
+}
+
+/// @brief Lease4 concurrent update tests
+///
+/// Checks that we are not able to concurrently update a lease in the database.
+TEST_F(MySqlLeaseMgrTest, concurrentUpdateLease4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testConcurrentUpdateLease4();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address
+TEST_F(MySqlLeaseMgrTest, getLease4HWAddr1) {
+ testGetLease4HWAddr1();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address
+TEST_F(MySqlLeaseMgrTest, getLease4HWAddr1MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4HWAddr1();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address
+TEST_F(MySqlLeaseMgrTest, getLease4HWAddr2) {
+ testGetLease4HWAddr2();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address
+TEST_F(MySqlLeaseMgrTest, getLease4HWAddr2MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4HWAddr2();
+}
+
+/// @brief Get lease4 by hardware address (2)
+///
+/// Check that the system can cope with getting a hardware address of
+/// any size.
+TEST_F(MySqlLeaseMgrTest, getLease4HWAddrSize) {
+ testGetLease4HWAddrSize();
+}
+
+/// @brief Get lease4 by hardware address (2)
+TEST_F(MySqlLeaseMgrTest, getLease4HWAddrSizeMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4HWAddrSize();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address & Subnet ID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// a combination of hardware address and subnet ID
+TEST_F(MySqlLeaseMgrTest, getLease4HwaddrSubnetId) {
+ testGetLease4HWAddrSubnetId();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address & Subnet ID
+TEST_F(MySqlLeaseMgrTest, getLease4HwaddrSubnetIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4HWAddrSubnetId();
+}
+
+/// @brief Get lease4 by hardware address and subnet ID (2)
+///
+/// Check that the system can cope with getting a hardware address of
+/// any size.
+TEST_F(MySqlLeaseMgrTest, getLease4HWAddrSubnetIdSize) {
+ testGetLease4HWAddrSubnetIdSize();
+}
+
+/// @brief Get lease4 by hardware address and subnet ID (2)
+TEST_F(MySqlLeaseMgrTest, getLease4HWAddrSubnetIdSizeMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4HWAddrSubnetIdSize();
+}
+
+/// @brief This test was derived from memfile.
+TEST_F(MySqlLeaseMgrTest, getLease4ClientId) {
+ testGetLease4ClientId();
+}
+
+/// @brief This test was derived from memfile.
+TEST_F(MySqlLeaseMgrTest, getLease4ClientIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4ClientId();
+}
+
+/// @brief Check GetLease4 methods - access by Client ID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// the Client ID.
+TEST_F(MySqlLeaseMgrTest, getLease4ClientId2) {
+ testGetLease4ClientId2();
+}
+
+/// @brief Check GetLease4 methods - access by Client ID
+TEST_F(MySqlLeaseMgrTest, getLease4ClientId2MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4ClientId2();
+}
+
+/// @brief Get Lease4 by client ID (2)
+///
+/// Check that the system can cope with a client ID of any size.
+TEST_F(MySqlLeaseMgrTest, getLease4ClientIdSize) {
+ testGetLease4ClientIdSize();
+}
+
+/// @brief Get Lease4 by client ID (2)
+TEST_F(MySqlLeaseMgrTest, getLease4ClientIdSizeMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4ClientIdSize();
+}
+
+/// @brief Check GetLease4 methods - access by Client ID & Subnet ID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// a combination of client and subnet IDs.
+TEST_F(MySqlLeaseMgrTest, getLease4ClientIdSubnetId) {
+ testGetLease4ClientIdSubnetId();
+}
+
+/// @brief Check GetLease4 methods - access by Client ID & Subnet ID
+TEST_F(MySqlLeaseMgrTest, getLease4ClientIdSubnetIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4ClientIdSubnetId();
+}
+
+/// @brief This test checks that all IPv4 leases for a specified subnet id are returned.
+TEST_F(MySqlLeaseMgrTest, getLeases4SubnetId) {
+ testGetLeases4SubnetId();
+}
+
+/// @brief This test checks that all IPv4 leases for a specified subnet id are returned.
+TEST_F(MySqlLeaseMgrTest, getLeases4SubnetIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases4SubnetId();
+}
+
+/// @brief This test checks that all IPv4 leases with a specified hostname are returned.
+TEST_F(MySqlLeaseMgrTest, getLeases4Hostname) {
+ testGetLeases4Hostname();
+}
+
+/// @brief This test checks that all IPv4 leases with a specified hostname are returned.
+TEST_F(MySqlLeaseMgrTest, getLeases4HostnameMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases4Hostname();
+}
+
+/// @brief This test checks that all IPv4 leases are returned.
+TEST_F(MySqlLeaseMgrTest, getLeases4) {
+ testGetLeases4();
+}
+
+/// @brief This test checks that all IPv4 leases are returned.
+TEST_F(MySqlLeaseMgrTest, getLeases4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases4();
+}
+
+/// @brief Test that a range of IPv4 leases is returned with paging.
+TEST_F(MySqlLeaseMgrTest, getLeases4Paged) {
+ testGetLeases4Paged();
+}
+
+/// @brief Test that a range of IPv4 leases is returned with paging.
+TEST_F(MySqlLeaseMgrTest, getLeases4PagedMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases4Paged();
+}
+
+/// @brief This test checks that all IPv6 leases for a specified subnet id are returned.
+TEST_F(MySqlLeaseMgrTest, getLeases6SubnetId) {
+ testGetLeases6SubnetId();
+}
+
+/// @brief This test checks that all IPv6 leases for a specified subnet id are returned.
+TEST_F(MySqlLeaseMgrTest, getLeases6SubnetIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases6SubnetId();
+}
+
+/// @brief This test checks that all IPv6 leases with a specified hostname are returned.
+TEST_F(MySqlLeaseMgrTest, getLeases6Hostname) {
+ testGetLeases6Hostname();
+}
+
+/// @brief This test checks that all IPv6 leases with a specified hostname are returned.
+TEST_F(MySqlLeaseMgrTest, getLeases6HostnameMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases6Hostname();
+}
+
+/// @brief This test checks that all IPv6 leases are returned.
+TEST_F(MySqlLeaseMgrTest, getLeases6) {
+ testGetLeases6();
+}
+
+/// @brief This test checks that all IPv6 leases are returned.
+TEST_F(MySqlLeaseMgrTest, getLeases6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases6();
+}
+
+/// @brief Test that a range of IPv6 leases is returned with paging.
+TEST_F(MySqlLeaseMgrTest, getLeases6Paged) {
+ testGetLeases6Paged();
+}
+
+/// @brief Test that a range of IPv6 leases is returned with paging.
+TEST_F(MySqlLeaseMgrTest, getLeases6PagedMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases6Paged();
+}
+
+/// @brief Basic Lease4 Checks
+///
+/// Checks that the addLease, getLease4(by address), getLease4(hwaddr,subnet_id),
+/// updateLease4() and deleteLease can handle NULL client-id.
+/// (client-id is optional and may not be present)
+TEST_F(MySqlLeaseMgrTest, lease4NullClientId) {
+ testLease4NullClientId();
+}
+
+/// @brief Basic Lease4 Checks
+TEST_F(MySqlLeaseMgrTest, lease4NullClientIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testLease4NullClientId();
+}
+
+/// @brief Verify that too long hostname for Lease4 is not accepted.
+///
+/// Checks that the it is not possible to create a lease when the hostname
+/// length exceeds 255 characters.
+TEST_F(MySqlLeaseMgrTest, lease4InvalidHostname) {
+ testLease4InvalidHostname();
+}
+
+/// @brief Verify that too long hostname for Lease4 is not accepted.
+TEST_F(MySqlLeaseMgrTest, lease4InvalidHostnameMultiThreading) {
+ MultiThreadingTest mt(true);
+ testLease4InvalidHostname();
+}
+
+/// @brief Check that the expired DHCPv4 leases can be retrieved.
+///
+/// This test adds a number of leases to the lease database and marks
+/// some of them as expired. Then it queries for expired leases and checks
+/// whether only expired leases are returned, and that they are returned in
+/// the order from most to least expired. It also checks that the lease
+/// which is marked as 'reclaimed' is not returned.
+TEST_F(MySqlLeaseMgrTest, getExpiredLeases4) {
+ testGetExpiredLeases4();
+}
+
+/// @brief Check that the expired DHCPv4 leases can be retrieved.
+TEST_F(MySqlLeaseMgrTest, getExpiredLeases4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetExpiredLeases4();
+}
+
+/// @brief Checks that DHCPv4 leases with infinite valid lifetime
+/// will never expire.
+TEST_F(MySqlLeaseMgrTest, infiniteAreNotExpired4) {
+ testInfiniteAreNotExpired4();
+}
+
+/// @brief Check that expired reclaimed DHCPv4 leases are removed.
+TEST_F(MySqlLeaseMgrTest, deleteExpiredReclaimedLeases4) {
+ testDeleteExpiredReclaimedLeases4();
+}
+
+/// @brief Check that expired reclaimed DHCPv4 leases are removed.
+TEST_F(MySqlLeaseMgrTest, deleteExpiredReclaimedLeases4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testDeleteExpiredReclaimedLeases4();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+/// LEASE6 /////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Test checks whether simple add, get and delete operations
+/// are possible on Lease6
+TEST_F(MySqlLeaseMgrTest, testAddGetDelete6) {
+ testAddGetDelete6();
+}
+
+/// @brief Test checks whether simple add, get and delete operations
+/// are possible on Lease6
+TEST_F(MySqlLeaseMgrTest, testAddGetDelete6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testAddGetDelete6();
+}
+
+/// @brief Basic Lease6 Checks
+///
+/// Checks that the addLease, getLease6 (by address) and deleteLease (with an
+/// IPv6 address) works.
+TEST_F(MySqlLeaseMgrTest, basicLease6) {
+ testBasicLease6();
+}
+
+/// @brief Basic Lease6 Checks
+TEST_F(MySqlLeaseMgrTest, basicLease6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testBasicLease6();
+}
+
+/// @brief Check that Lease6 code safely handles invalid dates.
+TEST_F(MySqlLeaseMgrTest, maxDate6) {
+ testMaxDate6();
+}
+
+/// @brief Check that Lease6 code safely handles invalid dates.
+TEST_F(MySqlLeaseMgrTest, maxDate6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMaxDate6();
+}
+
+/// @brief checks that infinite lifetimes do not overflow.
+TEST_F(MySqlLeaseMgrTest, infiniteLifeTime6) {
+ testInfiniteLifeTime6();
+}
+
+/// @brief Verify that too long hostname for Lease6 is not accepted.
+///
+/// Checks that the it is not possible to create a lease when the hostname
+/// length exceeds 255 characters.
+TEST_F(MySqlLeaseMgrTest, lease6InvalidHostname) {
+ testLease6InvalidHostname();
+}
+
+/// @brief Verify that too long hostname for Lease6 is not accepted.
+TEST_F(MySqlLeaseMgrTest, lease6InvalidHostnameMultiThreading) {
+ MultiThreadingTest mt(true);
+ testLease6InvalidHostname();
+}
+
+/// @brief Verify that large IAID values work correctly.
+///
+/// Adds lease with a large IAID to the database and verifies it can
+/// fetched correctly.
+TEST_F(MySqlLeaseMgrTest, leases6LargeIaidCheck) {
+ testLease6LargeIaidCheck();
+}
+
+/// @brief Verify that large IAID values work correctly.
+TEST_F(MySqlLeaseMgrTest, leases6LargeIaidCheckMultiThreading) {
+ MultiThreadingTest mt(true);
+ testLease6LargeIaidCheck();
+}
+
+/// @brief Check GetLease6 methods - access by DUID/IAID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// a combination of DUID and IAID.
+TEST_F(MySqlLeaseMgrTest, getLeases6DuidIaid) {
+ testGetLeases6DuidIaid();
+}
+
+/// @brief Check GetLease6 methods - access by DUID/IAID
+TEST_F(MySqlLeaseMgrTest, getLeases6DuidIaidMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases6DuidIaid();
+}
+
+/// @brief Check that the system can cope with a DUID of allowed size.
+TEST_F(MySqlLeaseMgrTest, getLeases6DuidSize) {
+ testGetLeases6DuidSize();
+}
+
+/// @brief Check that the system can cope with a DUID of allowed size.
+TEST_F(MySqlLeaseMgrTest, getLeases6DuidSizeMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases6DuidSize();
+}
+
+/// @brief Check that getLease6 methods discriminate by lease type.
+///
+/// Adds six leases, two per lease type all with the same duid and iad but
+/// with alternating subnet_ids.
+/// It then verifies that all of getLeases6() method variants correctly
+/// discriminate between the leases based on lease type alone.
+TEST_F(MySqlLeaseMgrTest, lease6LeaseTypeCheck) {
+ testLease6LeaseTypeCheck();
+}
+
+/// @brief Check that getLease6 methods discriminate by lease type.
+TEST_F(MySqlLeaseMgrTest, lease6LeaseTypeCheckMultiThreading) {
+ MultiThreadingTest mt(true);
+ testLease6LeaseTypeCheck();
+}
+
+/// @brief Check GetLease6 methods - access by DUID/IAID/SubnetID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// a combination of DIUID and IAID.
+TEST_F(MySqlLeaseMgrTest, getLease6DuidIaidSubnetId) {
+ testGetLease6DuidIaidSubnetId();
+}
+
+/// @brief Check GetLease6 methods - access by DUID/IAID/SubnetID
+TEST_F(MySqlLeaseMgrTest, getLease6DuidIaidSubnetIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease6DuidIaidSubnetId();
+}
+
+/// @brief Test checks that getLease6() works with different DUID sizes
+TEST_F(MySqlLeaseMgrTest, getLease6DuidIaidSubnetIdSize) {
+ testGetLease6DuidIaidSubnetIdSize();
+}
+
+/// @brief Test checks that getLease6() works with different DUID sizes
+TEST_F(MySqlLeaseMgrTest, getLease6DuidIaidSubnetIdSizeMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease6DuidIaidSubnetIdSize();
+}
+
+/// @brief check leases could be retrieved by DUID
+///
+/// Create leases, add them to backend and verify if it can be queried
+/// using DUID index
+TEST_F(MySqlLeaseMgrTest, getLeases6Duid) {
+ testGetLeases6Duid();
+}
+
+/// @brief check leases could be retrieved by DUID
+TEST_F(MySqlLeaseMgrTest, getLeases6DuidMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases6Duid();
+}
+
+/// @brief Lease6 update tests
+///
+/// Checks that we are able to update a lease in the database.
+TEST_F(MySqlLeaseMgrTest, updateLease6) {
+ testUpdateLease6();
+}
+
+/// @brief Lease6 update tests
+TEST_F(MySqlLeaseMgrTest, updateLease6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testUpdateLease6();
+}
+
+/// @brief Lease6 concurrent update tests
+///
+/// Checks that we are not able to concurrently update a lease in the database.
+TEST_F(MySqlLeaseMgrTest, concurrentUpdateLease6) {
+ testConcurrentUpdateLease6();
+}
+
+/// @brief Lease6 concurrent update tests
+///
+/// Checks that we are not able to concurrently update a lease in the database.
+TEST_F(MySqlLeaseMgrTest, concurrentUpdateLease6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testConcurrentUpdateLease6();
+}
+
+/// @brief DHCPv4 Lease recreation tests
+///
+/// Checks that the lease can be created, deleted and recreated with
+/// different parameters. It also checks that the re-created lease is
+/// correctly stored in the lease database.
+TEST_F(MySqlLeaseMgrTest, testRecreateLease4) {
+ testRecreateLease4();
+}
+
+/// @brief DHCPv4 Lease recreation tests
+TEST_F(MySqlLeaseMgrTest, testRecreateLease4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testRecreateLease4();
+}
+
+/// @brief DHCPv6 Lease recreation tests
+///
+/// Checks that the lease can be created, deleted and recreated with
+/// different parameters. It also checks that the re-created lease is
+/// correctly stored in the lease database.
+TEST_F(MySqlLeaseMgrTest, testRecreateLease6) {
+ testRecreateLease6();
+}
+
+/// @brief DHCPv6 Lease recreation tests
+TEST_F(MySqlLeaseMgrTest, testRecreateLease6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testRecreateLease6();
+}
+
+/// @brief Checks that null DUID is not allowed.
+TEST_F(MySqlLeaseMgrTest, nullDuid) {
+ testNullDuid();
+}
+
+/// @brief Checks that null DUID is not allowed.
+TEST_F(MySqlLeaseMgrTest, nullDuidMultiThreading) {
+ MultiThreadingTest mt(true);
+ testNullDuid();
+}
+
+/// @brief Tests whether MySQL can store and retrieve hardware addresses
+TEST_F(MySqlLeaseMgrTest, testLease6Mac) {
+ testLease6MAC();
+}
+
+/// @brief Tests whether MySQL can store and retrieve hardware addresses
+TEST_F(MySqlLeaseMgrTest, testLease6MacMultiThreading) {
+ MultiThreadingTest mt(true);
+ testLease6MAC();
+}
+
+/// @brief Tests whether MySQL can store and retrieve hardware addresses
+TEST_F(MySqlLeaseMgrTest, testLease6HWTypeAndSource) {
+ testLease6HWTypeAndSource();
+}
+
+/// @brief Tests whether MySQL can store and retrieve hardware addresses
+TEST_F(MySqlLeaseMgrTest, testLease6HWTypeAndSourceMultiThreading) {
+ MultiThreadingTest mt(true);
+ testLease6HWTypeAndSource();
+}
+
+/// @brief Check that the expired DHCPv6 leases can be retrieved.
+///
+/// This test adds a number of leases to the lease database and marks
+/// some of them as expired. Then it queries for expired leases and checks
+/// whether only expired leases are returned, and that they are returned in
+/// the order from most to least expired. It also checks that the lease
+/// which is marked as 'reclaimed' is not returned.
+TEST_F(MySqlLeaseMgrTest, getExpiredLeases6) {
+ testGetExpiredLeases6();
+}
+
+/// @brief Check that the expired DHCPv6 leases can be retrieved.
+TEST_F(MySqlLeaseMgrTest, getExpiredLeases6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetExpiredLeases6();
+}
+
+/// @brief Checks that DHCPv6 leases with infinite valid lifetime
+/// will never expire.
+TEST_F(MySqlLeaseMgrTest, infiniteAreNotExpired6) {
+ testInfiniteAreNotExpired6();
+}
+
+/// @brief Check that expired reclaimed DHCPv6 leases are removed.
+TEST_F(MySqlLeaseMgrTest, deleteExpiredReclaimedLeases6) {
+ testDeleteExpiredReclaimedLeases6();
+}
+
+/// @brief Check that expired reclaimed DHCPv6 leases are removed.
+TEST_F(MySqlLeaseMgrTest, deleteExpiredReclaimedLeases6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testDeleteExpiredReclaimedLeases6();
+}
+
+/// @brief Verifies that IPv4 lease statistics can be recalculated.
+TEST_F(MySqlLeaseMgrTest, recountLeaseStats4) {
+ testRecountLeaseStats4();
+}
+
+/// @brief Verifies that IPv4 lease statistics can be recalculated.
+TEST_F(MySqlLeaseMgrTest, recountLeaseStats4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testRecountLeaseStats4();
+}
+
+/// @brief Verifies that IPv6 lease statistics can be recalculated.
+TEST_F(MySqlLeaseMgrTest, recountLeaseStats6) {
+ testRecountLeaseStats6();
+}
+
+/// @brief Verifies that IPv6 lease statistics can be recalculated.
+TEST_F(MySqlLeaseMgrTest, recountLeaseStats6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testRecountLeaseStats6();
+}
+
+/// @brief Tests that leases from specific subnet can be removed.
+TEST_F(MySqlLeaseMgrTest, DISABLED_wipeLeases4) {
+ testWipeLeases4();
+}
+
+/// @brief Tests that leases from specific subnet can be removed.
+TEST_F(MySqlLeaseMgrTest, DISABLED_wipeLeases4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testWipeLeases4();
+}
+
+/// @brief Tests that leases from specific subnet can be removed.
+TEST_F(MySqlLeaseMgrTest, DISABLED_wipeLeases6) {
+ testWipeLeases6();
+}
+
+/// @brief Tests that leases from specific subnet can be removed.
+TEST_F(MySqlLeaseMgrTest, DISABLED_wipeLeases6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testWipeLeases6();
+}
+
+/// @brief Test fixture class for validating @c LeaseMgr using
+/// MySQL as back end and MySQL connectivity loss.
+class MySqlLeaseMgrDbLostCallbackTest : public LeaseMgrDbLostCallbackTest {
+public:
+ virtual void destroySchema() {
+ destroyMySQLSchema();
+ }
+
+ virtual void createSchema() {
+ createMySQLSchema();
+ }
+
+ virtual std::string validConnectString() {
+ return (validMySQLConnectionString());
+ }
+
+ virtual std::string invalidConnectString() {
+ return (connectionString(MYSQL_VALID_TYPE, INVALID_NAME, VALID_HOST,
+ VALID_USER, VALID_PASSWORD));
+ }
+};
+
+/// @brief Verifies that db lost callback is not invoked on an open failure
+TEST_F(MySqlLeaseMgrDbLostCallbackTest, testNoCallbackOnOpenFailure) {
+ MultiThreadingTest mt(false);
+ testNoCallbackOnOpenFailure();
+}
+
+/// @brief Verifies that db lost callback is not invoked on an open failure
+TEST_F(MySqlLeaseMgrDbLostCallbackTest, testNoCallbackOnOpenFailureMultiThreading) {
+ MultiThreadingTest mt(true);
+ testNoCallbackOnOpenFailure();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySqlLeaseMgrDbLostCallbackTest, testDbLostAndRecoveredCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndRecoveredCallback();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySqlLeaseMgrDbLostCallbackTest, testDbLostAndRecoveredCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndRecoveredCallback();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySqlLeaseMgrDbLostCallbackTest, testDbLostAndFailedCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndFailedCallback();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySqlLeaseMgrDbLostCallbackTest, testDbLostAndFailedCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndFailedCallback();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySqlLeaseMgrDbLostCallbackTest, testDbLostAndRecoveredAfterTimeoutCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndRecoveredAfterTimeoutCallback();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySqlLeaseMgrDbLostCallbackTest, testDbLostAndRecoveredAfterTimeoutCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndRecoveredAfterTimeoutCallback();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySqlLeaseMgrDbLostCallbackTest, testDbLostAndFailedAfterTimeoutCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndFailedAfterTimeoutCallback();
+}
+
+/// @brief Verifies that loss of connectivity to MySQL is handled correctly.
+TEST_F(MySqlLeaseMgrDbLostCallbackTest, testDbLostAndFailedAfterTimeoutCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndFailedAfterTimeoutCallback();
+}
+
+/// @brief Tests v4 lease stats query variants.
+TEST_F(MySqlLeaseMgrTest, leaseStatsQuery4) {
+ testLeaseStatsQuery4();
+}
+
+/// @brief Tests v4 lease stats query variants.
+TEST_F(MySqlLeaseMgrTest, leaseStatsQuery4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testLeaseStatsQuery4();
+}
+
+/// @brief Tests v6 lease stats query variants.
+TEST_F(MySqlLeaseMgrTest, leaseStatsQuery6) {
+ testLeaseStatsQuery6();
+}
+
+/// @brief Tests v6 lease stats query variants.
+TEST_F(MySqlLeaseMgrTest, leaseStatsQuery6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testLeaseStatsQuery6();
+}
+
+/// @brief Tests v4 lease stats to be attributed to the wrong subnet.
+TEST_F(MySqlLeaseMgrTest, leaseStatsQueryAttribution4) {
+ testLeaseStatsQueryAttribution4();
+}
+
+/// @brief Tests v4 lease stats to be attributed to the wrong subnet.
+TEST_F(MySqlLeaseMgrTest, leaseStatsQueryAttribution4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testLeaseStatsQueryAttribution4();
+}
+
+/// @brief Tests v6 lease stats to be attributed to the wrong subnet.
+TEST_F(MySqlLeaseMgrTest, leaseStatsQueryAttribution6) {
+ testLeaseStatsQueryAttribution6();
+}
+
+/// @brief Tests v6 lease stats to be attributed to the wrong subnet.
+TEST_F(MySqlLeaseMgrTest, leaseStatsQueryAttribution6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testLeaseStatsQueryAttribution6();
+}
+
+/// @brief Checks that no exceptions are thrown when inquiring about JSON
+/// support and prints an informative message.
+TEST_F(MySqlLeaseMgrTest, isJsonSupported) {
+ bool json_supported;
+ ASSERT_NO_THROW_LOG(json_supported = LeaseMgrFactory::instance().isJsonSupported());
+ std::cout << "JSON support is " << (json_supported ? "" : "not ") <<
+ "enabled in the database." << std::endl;
+}
+
+// Verifies that v4 class lease counts are correctly adjusted
+// when leases have class lists.
+TEST_F(MySqlLeaseMgrTest, classLeaseCount4) {
+ if (!LeaseMgrFactory::instance().isJsonSupported()) {
+ std::cout << "Skipped test because of lack of JSON support in the database." << std::endl;
+ return;
+ }
+
+ testClassLeaseCount4();
+}
+
+// Verifies that v6 IA_NA class lease counts are correctly adjusted
+// when leases have class lists.
+TEST_F(MySqlLeaseMgrTest, classLeaseCount6_NA) {
+ if (!LeaseMgrFactory::instance().isJsonSupported()) {
+ std::cout << "Skipped test because of lack of JSON support in the database." << std::endl;
+ return;
+ }
+
+ testClassLeaseCount6(Lease::TYPE_NA);
+}
+
+// Verifies that v6 IA_PD class lease counts are correctly adjusted
+// when leases have class lists.
+TEST_F(MySqlLeaseMgrTest, classLeaseCount6_PD) {
+ if (!LeaseMgrFactory::instance().isJsonSupported()) {
+ std::cout << "Skipped test because of lack of JSON support in the database." << std::endl;
+ return;
+ }
+
+ testClassLeaseCount6(Lease::TYPE_PD);
+}
+
+/// @brief Checks that a null user context allows allocation.
+TEST_F(MySqlLeaseMgrTest, checkLimitsNull) {
+ std::string text;
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits4(nullptr));
+ EXPECT_TRUE(text.empty());
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits6(nullptr));
+ EXPECT_TRUE(text.empty());
+}
+
+/// @brief Checks a few v4 limit checking scenarios.
+TEST_F(MySqlLeaseMgrTest, checkLimits4) {
+ // Limit checking should be precluded at reconfiguration time on systems
+ // that don't have JSON support in the database. It's fine if it throws.
+ if (!LeaseMgrFactory::instance().isJsonSupported()) {
+ ASSERT_THROW_MSG(LeaseMgrFactory::instance().checkLimits4(
+ isc::data::Element::createMap()), isc::db::DbOperationError,
+ "unable to set up for storing all results for "
+ "<SELECT checkLease4Limits(?)>, reason: FUNCTION "
+ "keatest.JSON_EXTRACT does not exist (error code 1305)");
+ return;
+ }
+
+ // The rest of the checks are only for databases with JSON support.
+ testLeaseLimits4();
+}
+
+/// @brief Checks a few v6 limit checking scenarios.
+TEST_F(MySqlLeaseMgrTest, checkLimits6) {
+ // Limit checking should be precluded at reconfiguration time on systems
+ // that don't have JSON support in the database. It's fine if it throws.
+ if (!LeaseMgrFactory::instance().isJsonSupported()) {
+ ASSERT_THROW_MSG(LeaseMgrFactory::instance().checkLimits6(
+ isc::data::Element::createMap()), isc::db::DbOperationError,
+ "unable to set up for storing all results for "
+ "<SELECT checkLease6Limits(?)>, reason: FUNCTION "
+ "keatest.JSON_EXTRACT does not exist (error code 1305)");
+ return;
+ }
+
+ // The rest of the checks are only for databases with JSON support.
+ testLeaseLimits6();
+}
+
+} // namespace
diff --git a/src/lib/dhcpsrv/tests/ncr_generator_unittest.cc b/src/lib/dhcpsrv/tests/ncr_generator_unittest.cc
new file mode 100644
index 0000000..9590965
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/ncr_generator_unittest.cc
@@ -0,0 +1,691 @@
+// Copyright (C) 2015-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcp/duid.h>
+#include <dhcp_ddns/ncr_msg.h>
+#include <dhcpsrv/ncr_generator.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/d2_client_mgr.h>
+#include <dhcpsrv/lease.h>
+
+#include <gtest/gtest.h>
+#include <ctime>
+#include <functional>
+#include <stdint.h>
+#include <string>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp_ddns;
+namespace ph = std::placeholders;
+
+namespace {
+
+/// @brief Base test fixture class for testing generation of the name
+/// change requests from leases.
+///
+/// @tparam LeasePtrType One of the @c Lease4Ptr or @c Lease6Ptr.
+template<typename LeasePtrType>
+class NCRGeneratorTest : public ::testing::Test {
+public:
+
+ /// @brief Reference to the D2 client manager.
+ D2ClientMgr& d2_mgr_;
+
+ /// @brief Pointer to the lease object used by the tests.
+ LeasePtrType lease_;
+
+ /// @brief Pointer to the lease's subnet
+ SubnetPtr subnet_;
+
+ /// @brief Constructor.
+ NCRGeneratorTest()
+ : d2_mgr_(CfgMgr::instance().getD2ClientMgr()), lease_() {
+ }
+
+ /// @brief Destructor
+ virtual ~NCRGeneratorTest() = default;
+
+ /// @brief Initializes the lease pointer used by the tests and starts D2.
+ ///
+ /// This method initializes the pointer to the lease which will be used
+ /// throughout the tests. Because the lease may be either a v4 or v6 lease
+ /// it calls a virtual function @c initLease, which must be implemented
+ /// in the derived classes as appropriate. Note that lease object can't
+ /// be initialized in the constructor, because it is not allowed to
+ /// call virtual functions in the constructors. Hence, the @c SetUp
+ /// function is needed.
+ virtual void SetUp() {
+ // Base class SetUp.
+ ::testing::Test::SetUp();
+ // Initialize lease_ object.
+ initLease();
+ // Start D2 by default.
+ enableD2();
+ }
+
+ /// @brief Stops D2.
+ virtual void TearDown() {
+ // Stop D2 if running.
+ disableD2();
+ // Base class TearDown.
+ ::testing::Test::TearDown();
+
+ CfgMgr::instance().clear();
+ }
+
+ /// @brief Enables DHCP-DDNS updates.
+ ///
+ /// Replaces the current D2ClientConfiguration with a configuration
+ /// which has updates enabled and the control options set based upon
+ /// the bit mask of options.
+ void enableD2() {
+ D2ClientConfigPtr cfg(new D2ClientConfig());
+ ASSERT_NO_THROW(cfg->enableUpdates(true));
+ ASSERT_NO_THROW(CfgMgr::instance().setD2ClientConfig(cfg));
+ d2_mgr_.startSender(std::bind(&NCRGeneratorTest::d2ErrorHandler, this,
+ ph::_1, ph::_2));
+ }
+
+ /// @brief Disables DHCP-DDNS updates.
+ void disableD2() {
+ d2_mgr_.stopSender();
+ // Default constructor creates a config with DHCP-DDNS updates
+ // disabled.
+ D2ClientConfigPtr cfg(new D2ClientConfig());
+ CfgMgr::instance().setD2ClientConfig(cfg);
+ }
+
+ /// @brief No-op error handler for D2.
+ void d2ErrorHandler(const NameChangeSender::Result, NameChangeRequestPtr&) {
+ // no-op
+ }
+
+ /// @brief Abstract method to initialize @c lease_ object.
+ virtual void initLease() = 0;
+
+ /// @brief Verify that NameChangeRequest holds valid values.
+ ///
+ /// This function picks first NameChangeRequest from the internal server's
+ /// queue and checks that it holds valid parameters. The NameChangeRequest
+ /// is removed from the queue.
+ ///
+ /// @param type An expected type of the NameChangeRequest (Add or Remove).
+ /// @param reverse An expected setting of the reverse update flag.
+ /// @param forward An expected setting of the forward update flag.
+ /// @param addr A string representation of the IPv6 address held in the
+ /// NameChangeRequest.
+ /// @param dhcid An expected DHCID value.
+ /// @note This value is the value that is produced by
+ /// dhcp_ddns::D2Dhcid::createDigest() with the appropriate arguments. This
+ /// method uses encryption tools to produce the value which cannot be
+ /// easily duplicated by hand. It is more or less necessary to generate
+ /// these values programmatically and place them here. Should the
+ /// underlying implementation of createDigest() change these test values
+ /// will likely need to be updated as well.
+ /// @param expires A timestamp when the lease associated with the
+ /// NameChangeRequest expires.
+ /// @param len A valid lifetime of the lease associated with the
+ /// NameChangeRequest.
+ /// @param fqdn The expected string value of the FQDN, if blank the
+ /// check is skipped
+ void verifyNameChangeRequest(const isc::dhcp_ddns::NameChangeType type,
+ const bool reverse, const bool forward,
+ const std::string& addr,
+ const std::string& dhcid,
+ const uint64_t expires,
+ const uint16_t len,
+ const std::string& fqdn="",
+ const bool use_conflict_resolution = true) {
+ NameChangeRequestPtr ncr;
+ ASSERT_NO_THROW(ncr = CfgMgr::instance().getD2ClientMgr().peekAt(0));
+ ASSERT_TRUE(ncr);
+
+ EXPECT_EQ(type, ncr->getChangeType());
+ EXPECT_EQ(forward, ncr->isForwardChange());
+ EXPECT_EQ(reverse, ncr->isReverseChange());
+ EXPECT_EQ(addr, ncr->getIpAddress());
+ EXPECT_EQ(dhcid, ncr->getDhcid().toStr());
+ EXPECT_EQ(expires, ncr->getLeaseExpiresOn());
+ EXPECT_EQ(len, ncr->getLeaseLength());
+ EXPECT_EQ(isc::dhcp_ddns::ST_NEW, ncr->getStatus());
+ EXPECT_EQ(use_conflict_resolution, ncr->useConflictResolution());
+
+ if (!fqdn.empty()) {
+ EXPECT_EQ(fqdn, ncr->getFqdn());
+ }
+
+ // Process the message off the queue
+ ASSERT_NO_THROW(CfgMgr::instance().getD2ClientMgr().runReadyIO());
+ }
+
+ /// @brief Sets the FQDN information for a lease and queues an NCR.
+ ///
+ /// @param fwd Perform forward update.
+ /// @param rev Perform reverse update.
+ /// @param fqdn Hostname.
+ void queueRemovalNCR(const bool fwd, const bool rev, const std::string& fqdn) {
+ lease_->fqdn_fwd_ = fwd;
+ lease_->fqdn_rev_ = rev;
+ lease_->hostname_ = fqdn;
+
+ /// Send NCR to D2.
+ ASSERT_NO_THROW(queueNCR(CHG_REMOVE, lease_));
+ }
+
+ /// @brief Sets the FQDN information for a lease and queues an NCR.
+ ///
+ /// @param chg_type Name change type.
+ /// @param fwd Perform forward update.
+ /// @param rev Perform reverse update.
+ /// @param fqdn Hostname.
+ void sendNCR(const NameChangeType chg_type, const bool fwd, const bool rev,
+ const std::string& fqdn) {
+ lease_->fqdn_fwd_ = fwd;
+ lease_->fqdn_rev_ = rev;
+ lease_->hostname_ = fqdn;
+
+ /// Send NCR to D2.
+ ASSERT_NO_THROW(queueNCR(chg_type, lease_));
+ }
+
+ /// @brief Test that for the given values the NCR is not generated.
+ ///
+ /// @param chg_type Name change type.
+ /// @param fwd Perform forward update.
+ /// @param rev Perform reverse update.
+ /// @param fqdn Hostname.
+ void testNoUpdate(const NameChangeType chg_type, const bool fwd, const bool rev,
+ const std::string& fqdn) {
+ ASSERT_NO_FATAL_FAILURE(sendNCR(chg_type, fwd, rev, fqdn));
+ ASSERT_EQ(0, d2_mgr_.getQueueSize());
+ }
+
+ /// @brief Test that sending an NCR while DNS updates would not throw.
+ ///
+ /// @param chg_type Name change type.
+ void testD2Disabled(const NameChangeType chg_type) {
+ // Disable DDNS updates.
+ disableD2();
+ ASSERT_NO_FATAL_FAILURE(sendNCR(chg_type, true, true, "MYHOST.example.com."));
+ }
+
+ /// @brief Test that NCR is generated as expected.
+ ///
+ /// @param chg_type Name change type.
+ /// @param fwd Perform forward update.
+ /// @param rev Perform reverse update.
+ /// @param fqdn Hostname.
+ /// @param exp_dhcid Expected DHCID.
+ /// @param exp_use_cr expected value of conflict resolution flag
+ void testNCR(const NameChangeType chg_type, const bool fwd, const bool rev,
+ const std::string& fqdn, const std::string exp_dhcid,
+ const bool exp_use_cr = true) {
+ // Queue NCR.
+ ASSERT_NO_FATAL_FAILURE(sendNCR(chg_type, fwd, rev, fqdn));
+ // Expecting one NCR be generated.
+ ASSERT_EQ(1, d2_mgr_.getQueueSize());
+
+ uint32_t ttl = calculateDdnsTtl(lease_->valid_lft_);
+
+ // Check the details of the NCR.
+ verifyNameChangeRequest(chg_type, rev, fwd, lease_->addr_.toText(), exp_dhcid,
+ lease_->cltt_ + ttl, ttl, fqdn, exp_use_cr);
+ }
+
+ /// @brief Test that calling queueNCR for NULL lease doesn't cause
+ /// an exception.
+ ///
+ /// @param chg_type Name change type.
+ void testNullLease(const NameChangeType chg_type) {
+ lease_.reset();
+ ASSERT_NO_FATAL_FAILURE(queueNCR(chg_type, lease_));
+ EXPECT_EQ(0, d2_mgr_.getQueueSize());
+ }
+};
+
+/// @brief Test fixture class implementation for DHCPv6.
+class NCRGenerator6Test : public NCRGeneratorTest<Lease6Ptr> {
+public:
+
+ /// @brief Pointer to the DUID used in the tests.
+ DuidPtr duid_;
+
+ /// @brief Constructor.
+ ///
+ /// Initializes DUID.
+ NCRGenerator6Test()
+ : duid_() {
+ duid_.reset(new DUID(DUID::fromText("01:02:03:04:05:06:07:08:09")));
+ }
+
+ /// @brief Implementation of the method creating DHCPv6 lease instance.
+ virtual void initLease() {
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 64, 100, 200, 300, 400));
+ // Normally, this would be set via defaults
+ subnet->setDdnsUseConflictResolution(true);
+
+ Pool6Ptr pool(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ IOAddress("2001:db8:1::200")));
+ subnet->addPool(pool);
+ subnet_ = subnet;
+
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ cfg_mgr.getStagingCfg()->getCfgSubnets6()->add(subnet);
+ cfg_mgr.commit();
+
+ lease_.reset(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ duid_, 1234, 501, 502, subnet_->getID(), HWAddrPtr()));
+ }
+};
+
+
+// Test creation of the NameChangeRequest for both forward and reverse
+// mapping for the given lease.
+TEST_F(NCRGenerator6Test, fwdRev) {
+ // Part of the domain name is in upper case, to test that it gets converted
+ // to lower case before DHCID is computed. So, we should get the same DHCID
+ // as if we typed domain-name in lower case.
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNCR(CHG_REMOVE, true, true, "MYHOST.example.com.",
+ "000201BE0D7A66F8AB6C4082E7F8B81E2656667A102E3"
+ "D0ECCEA5E0DD71730F392119A");
+ }
+
+ // Now try the same test with all lower case.
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNCR(CHG_REMOVE, true, true, "myhost.example.com.",
+ "000201BE0D7A66F8AB6C4082E7F8B81E2656667A102E3"
+ "D0ECCEA5E0DD71730F392119A");
+ }
+
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNCR(CHG_ADD, true, true, "MYHOST.example.com.",
+ "000201BE0D7A66F8AB6C4082E7F8B81E2656667A102E3"
+ "D0ECCEA5E0DD71730F392119A");
+ }
+
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNCR(CHG_ADD, true, true, "myhost.example.com.",
+ "000201BE0D7A66F8AB6C4082E7F8B81E2656667A102E3"
+ "D0ECCEA5E0DD71730F392119A");
+ }
+
+}
+
+// Checks that NameChangeRequests are not created when ddns updates are disabled.
+TEST_F(NCRGenerator6Test, d2Disabled) {
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testD2Disabled(CHG_REMOVE);
+ }
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testD2Disabled(CHG_ADD);
+ }
+}
+
+// Test creation of the NameChangeRequest for reverse mapping in the
+// given lease.
+TEST_F(NCRGenerator6Test, revOnly) {
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNCR(CHG_REMOVE, false, true, "myhost.example.com.",
+ "000201BE0D7A66F8AB6C4082E7F8B81E2656667A102E3"
+ "D0ECCEA5E0DD71730F392119A");
+ }
+
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNCR(CHG_ADD, false, true, "myhost.example.com.",
+ "000201BE0D7A66F8AB6C4082E7F8B81E2656667A102E3"
+ "D0ECCEA5E0DD71730F392119A");
+ }
+}
+
+// Test creation of the NameChangeRequest for forward mapping in the
+// given lease.
+TEST_F(NCRGenerator6Test, fwdOnly) {
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNCR(CHG_REMOVE, true, false, "myhost.example.com.",
+ "000201BE0D7A66F8AB6C4082E7F8B81E2656667A102E3"
+ "D0ECCEA5E0DD71730F392119A");
+ }
+
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNCR(CHG_ADD, true, false, "myhost.example.com.",
+ "000201BE0D7A66F8AB6C4082E7F8B81E2656667A102E3"
+ "D0ECCEA5E0DD71730F392119A");
+ }
+}
+
+
+// Test that NameChangeRequest is not generated when neither forward
+// nor reverse DNS update has been performed for a lease.
+TEST_F(NCRGenerator6Test, noFwdRevUpdate) {
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNoUpdate(CHG_REMOVE, false, false, "myhost.example.com.");
+ }
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNoUpdate(CHG_ADD, false, false, "myhost.example.com.");
+ }
+}
+
+// Test that NameChangeRequest is not generated if the hostname hasn't been
+// specified for a lease for which forward and reverse mapping has been set.
+TEST_F(NCRGenerator6Test, noHostname) {
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNoUpdate(CHG_REMOVE, false, false, "");
+ }
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNoUpdate(CHG_ADD, false, false, "");
+ }
+}
+
+// Test that NameChangeRequest is not generated if an invalid hostname has
+// been specified for a lease for which forward and reverse mapping has been
+// set.
+TEST_F(NCRGenerator6Test, wrongHostname) {
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNoUpdate(CHG_REMOVE, false, false, "myhost...example.com.");
+ }
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNoUpdate(CHG_ADD, false, false, "myhost...example.com.");
+ }
+}
+
+// Test that NameChangeRequest is not generated if the lease is not an
+// address lease, i.e. is a prefix.
+TEST_F(NCRGenerator6Test, wrongLeaseType) {
+ // Change lease type to delegated prefix.
+ lease_->type_ = Lease::TYPE_PD;
+
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNoUpdate(CHG_REMOVE, true, true, "myhost.example.org.");
+ }
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNoUpdate(CHG_ADD, true, true, "myhost.example.org.");
+ }
+}
+
+// Test that NameChangeRequest is not generated if the lease is NULL,
+// and that the call to queueNCR doesn't cause an exception or
+// assertion.
+TEST_F(NCRGenerator6Test, nullLease) {
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNullLease(CHG_REMOVE);
+ }
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNullLease(CHG_ADD);
+ }
+}
+
+// Verify that conflict resolution is set correctly by v6 queueNCR()
+TEST_F(NCRGenerator6Test, useConflictResolution) {
+ {
+ SCOPED_TRACE("Subnet flag is false");
+ subnet_->setDdnsUseConflictResolution(false);
+ testNCR(CHG_REMOVE, true, true, "MYHOST.example.com.",
+ "000201BE0D7A66F8AB6C4082E7F8B81E2656667A102E3D0ECCEA5E0DD71730F392119A",
+ false);
+ }
+ {
+ SCOPED_TRACE("Subnet flag is true");
+ subnet_->setDdnsUseConflictResolution(true);
+ testNCR(CHG_REMOVE, true, true, "MYHOST.example.com.",
+ "000201BE0D7A66F8AB6C4082E7F8B81E2656667A102E3D0ECCEA5E0DD71730F392119A",
+ true);
+ }
+}
+
+/// @brief Test fixture class implementation for DHCPv4.
+class NCRGenerator4Test : public NCRGeneratorTest<Lease4Ptr> {
+public:
+
+ /// @brief Pointer to HW address used by the tests.
+ HWAddrPtr hwaddr_;
+
+ /// @brief Constructor.
+ ///
+ /// Initializes HW address.
+ NCRGenerator4Test()
+ : hwaddr_(new HWAddr(HWAddr::fromText("01:02:03:04:05:06"))) {
+ }
+
+ /// @brief Implementation of the method creating DHCPv4 lease instance.
+ virtual void initLease() {
+
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3));
+ // Normally, this would be set via defaults
+ subnet->setDdnsUseConflictResolution(true);
+
+ Pool4Ptr pool(new Pool4(IOAddress("192.0.2.100"),
+ IOAddress("192.0.2.200")));
+ subnet->addPool(pool);
+
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ cfg_mgr.getStagingCfg()->getCfgSubnets4()->add(subnet);
+ cfg_mgr.commit();
+
+ subnet_ = subnet;
+ lease_.reset(new Lease4(IOAddress("192.0.2.1"), hwaddr_, ClientIdPtr(),
+ 100, time(NULL), subnet_->getID()));
+ }
+
+};
+
+// Test creation of the NameChangeRequest for both forward and reverse
+// mapping for the given lease.
+TEST_F(NCRGenerator4Test, fwdRev) {
+ // Part of the domain name is in upper case, to test that it gets converted
+ // to lower case before DHCID is computed. So, we should get the same DHCID
+ // as if we typed domain-name in lower case.
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNCR(CHG_REMOVE, true, true, "MYHOST.example.com.",
+ "000001E356D43E5F0A496D65BCA24D982D646140813E3"
+ "B03AB370BFF46BFA309AE7BFD");
+ }
+
+ // Now try the same with all lower case.
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNCR(CHG_REMOVE, true, true, "myhost.example.com.",
+ "000001E356D43E5F0A496D65BCA24D982D646140813E3"
+ "B03AB370BFF46BFA309AE7BFD");
+ }
+
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNCR(CHG_ADD, true, true, "MYHOST.example.com.",
+ "000001E356D43E5F0A496D65BCA24D982D646140813E3"
+ "B03AB370BFF46BFA309AE7BFD");
+ }
+
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNCR(CHG_ADD, true, true, "myhost.example.com.",
+ "000001E356D43E5F0A496D65BCA24D982D646140813E3"
+ "B03AB370BFF46BFA309AE7BFD");
+ }
+}
+
+// Checks that NameChangeRequests are not created when ddns updates are disabled.
+TEST_F(NCRGenerator4Test, d2Disabled) {
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testD2Disabled(CHG_REMOVE);
+ }
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testD2Disabled(CHG_ADD);
+ }
+}
+
+// Test creation of the NameChangeRequest for reverse mapping in the
+// given lease.
+TEST_F(NCRGenerator4Test, revOnly) {
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNCR(CHG_REMOVE, false, true, "myhost.example.com.",
+ "000001E356D43E5F0A496D65BCA24D982D646140813E3B"
+ "03AB370BFF46BFA309AE7BFD");
+ }
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNCR(CHG_ADD, false, true, "myhost.example.com.",
+ "000001E356D43E5F0A496D65BCA24D982D646140813E3B"
+ "03AB370BFF46BFA309AE7BFD");
+ }
+}
+
+// Test creation of the NameChangeRequest for forward mapping in the
+// given lease.
+TEST_F(NCRGenerator4Test, fwdOnly) {
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNCR(CHG_REMOVE, true, false, "myhost.example.com.",
+ "000001E356D43E5F0A496D65BCA24D982D646140813E3B"
+ "03AB370BFF46BFA309AE7BFD");
+ }
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNCR(CHG_ADD, true, false, "myhost.example.com.",
+ "000001E356D43E5F0A496D65BCA24D982D646140813E3B"
+ "03AB370BFF46BFA309AE7BFD");
+ }
+}
+
+// Test that NameChangeRequest is not generated when neither forward
+// nor reverse DNS update has been performed for a lease.
+TEST_F(NCRGenerator4Test, noFwdRevUpdate) {
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNoUpdate(CHG_REMOVE, false, false, "myhost.example.com.");
+ }
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNoUpdate(CHG_ADD, false, false, "myhost.example.com.");
+ }
+}
+
+// Test that NameChangeRequest is not generated if the hostname hasn't been
+// specified for a lease for which forward and reverse mapping has been set.
+TEST_F(NCRGenerator4Test, noHostname) {
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNoUpdate(CHG_REMOVE, false, false, "");
+ }
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNoUpdate(CHG_ADD, false, false, "");
+ }
+}
+
+// Test that NameChangeRequest is not generated if the invalid hostname has
+// been specified for a lease for which forward and reverse mapping has been
+// set.
+TEST_F(NCRGenerator4Test, wrongHostname) {
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNoUpdate(CHG_REMOVE, false, false, "myhost...example.org.");
+ }
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNoUpdate(CHG_ADD, false, false, "myhost...example.org.");
+ }
+}
+
+// Test that the correct NameChangeRequest is generated when the lease
+// includes client identifier.
+TEST_F(NCRGenerator4Test, useClientId) {
+ lease_->client_id_ = ClientId::fromText("01:01:01:01");
+
+ ASSERT_NO_FATAL_FAILURE(queueRemovalNCR(true, true, "myhost.example.com."));
+ ASSERT_EQ(1, d2_mgr_.getQueueSize());
+
+ uint32_t ttl = calculateDdnsTtl(lease_->valid_lft_);
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+ "192.0.2.1",
+ "000101C7AA5420483BDA99C437636EA7DA2FE18"
+ "31C9679FEB031C360CA571298F3D1FA",
+ lease_->cltt_ + ttl, ttl);
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNCR(CHG_REMOVE, true, true, "myhost.example.com.",
+ "000101C7AA5420483BDA99C437636EA7DA2FE1831C9679"
+ "FEB031C360CA571298F3D1FA");
+ }
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNCR(CHG_ADD, true, true, "myhost.example.com.",
+ "000101C7AA5420483BDA99C437636EA7DA2FE1831C9679"
+ "FEB031C360CA571298F3D1FA");
+ }
+}
+
+// Test that NameChangeRequest is not generated if the lease is NULL,
+// and that the call to queueNCR doesn't cause an exception or
+// assertion.
+TEST_F(NCRGenerator4Test, nullLease) {
+ {
+ SCOPED_TRACE("case CHG_REMOVE");
+ testNullLease(CHG_REMOVE);
+ }
+ {
+ SCOPED_TRACE("case CHG_ADD");
+ testNullLease(CHG_ADD);
+ }
+}
+
+// Verify that conflict resolution is set correctly by v4 queueNCR()
+TEST_F(NCRGenerator4Test, useConflictResolution) {
+ {
+ SCOPED_TRACE("Subnet flag is false");
+ subnet_->setDdnsUseConflictResolution(false);
+ testNCR(CHG_REMOVE, true, true, "MYHOST.example.com.",
+ "000001E356D43E5F0A496D65BCA24D982D646140813E3"
+ "B03AB370BFF46BFA309AE7BFD", false);
+ }
+ {
+ SCOPED_TRACE("Subnet flag is true");
+ subnet_->setDdnsUseConflictResolution(true);
+ testNCR(CHG_REMOVE, true, true, "MYHOST.example.com.",
+ "000001E356D43E5F0A496D65BCA24D982D646140813E3"
+ "B03AB370BFF46BFA309AE7BFD", true);
+ }
+}
+
+// Verify that calculateDdnsTtl() produces the expected values.
+TEST_F(NCRGenerator4Test, calculateDdnsTtl) {
+
+ // A life time less than or equal to 1800 should yield a TTL of 600 seconds.
+ EXPECT_EQ(600, calculateDdnsTtl(100));
+
+ // A life time > 1800 should be 1/3 of the value.
+ EXPECT_EQ(601, calculateDdnsTtl(1803));
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/network_state_unittest.cc b/src/lib/dhcpsrv/tests/network_state_unittest.cc
new file mode 100644
index 0000000..7edf458
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/network_state_unittest.cc
@@ -0,0 +1,784 @@
+// Copyright (C) 2017-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/interval_timer.h>
+#include <asiolink/io_service.h>
+#include <dhcpsrv/network_state.h>
+#include <dhcpsrv/timer_mgr.h>
+#include <util/multi_threading_mgr.h>
+#include <gtest/gtest.h>
+#include <functional>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Test fixture class for @c NetworkState class.
+class NetworkStateTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ NetworkStateTest()
+ : io_service_(new IOService()) {
+ TimerMgr::instance()->unregisterTimers();
+ TimerMgr::instance()->setIOService(io_service_);
+ MultiThreadingMgr::instance().setMode(false);
+ }
+
+ /// @brief Destructor.
+ virtual ~NetworkStateTest() {
+ // Cancel timers.
+ TimerMgr::instance()->unregisterTimers();
+ // Make sure IO service will stop when no timers are scheduled.
+ io_service_->stopWork();
+ // Run outstanding tasks.
+ io_service_->run();
+ MultiThreadingMgr::instance().setMode(false);
+ }
+
+ /// @brief This test verifies the default is enable state.
+ void defaultTest();
+
+ /// @brief This test verifies that it is possible to disable and then enable DHCPv4
+ /// service using 'user command' origin.
+ void disableEnableService4UsingUserCommandOriginTest();
+
+ /// @brief This test verifies that it is possible to disable and then enable DHCPv4
+ /// service using 'HA command' origin.
+ void disableEnableService4UsingHACommandOriginTest();
+
+ /// @brief This test verifies that it is possible to disable and then enable DHCPv4
+ /// service using 'DB connection' origin.
+ void disableEnableService4UsingDBConnectionOriginTest();
+
+ /// @brief This test verifies that it is possible to disable and then enable DHCPv4
+ /// service using a combination of origins.
+ /// 1. Disable using 'user command' origin 2 times (expect disabled state).
+ /// 2. Disable using 'HA command' origin 2 times (expect disabled state).
+ /// 3. Disable using 'DB connection' origin 2 times (expect disabled state).
+ /// 4. Enable using 'user command' origin 1 time (expect disabled state).
+ /// 5. Enable using 'HA command' origin 1 time (expect disabled state).
+ /// 6. Enable using 'DB connection' origin 2 times (expect enabled state).
+ void disableEnableService4UsingMultipleOriginsTest();
+
+ /// @brief This test verifies that it is possible to disable and then enable DHCPv6
+ /// service using 'user command' origin.
+ void disableEnableService6UsingUserCommandOriginTest();
+
+ /// @brief This test verifies that it is possible to disable and then enable DHCPv6
+ /// service using 'HA command' origin.
+ void disableEnableService6UsingHACommandOriginTest();
+
+ /// @brief This test verifies that it is possible to disable and then enable DHCPv6
+ /// service using 'DB connection' origin.
+ void disableEnableService6UsingDBConnectionOriginTest();
+
+ /// @brief This test verifies that it is possible to disable and then enable DHCPv6
+ /// service using a combination of origins.
+ /// 1. Disable using 'user command' origin 2 times (expect disabled state).
+ /// 2. Disable using 'HA command' origin 2 times (expect disabled state).
+ /// 3. Disable using 'DB connection' origin 2 times (expect disabled state).
+ /// 4. Enable using 'user command' origin 1 time (expect disabled state).
+ /// 5. Enable using 'HA command' origin 1 time (expect disabled state).
+ /// 6. Enable using 'DB connection' origin 2 times (expect enabled state).
+ void disableEnableService6UsingMultipleOriginsTest();
+
+ /// @brief This test verifies that reset works, so that internal state is reset after
+ /// all managers are recreated.
+ /// 1. Disable using 'user command' origin 3 times (expect disabled state).
+ /// 2. Disable using 'HA command' origin 1 time (expect disabled state).
+ /// 3. Disable using 'DB connection' origin 1 time (expect disabled state).
+ /// 4. Reset using 'user command' origin (expect disabled state).
+ /// 5. Enable using 'HA command' origin 1 time (expect disabled state).
+ /// 6. Enable using 'DB connection' origin 1 time (expect enabled state).
+ /// 7. Disable using 'user command' origin 3 times (expect disabled state).
+ /// 8. Reset using 'user command' origin (expect enabled state).
+ void resetUsingUserCommandOriginTest();
+
+ /// @brief This test verifies that reset works, so that internal state is reset after
+ /// all managers are recreated.
+ /// 1. Disable using 'user command' origin 1 time (expect disabled state).
+ /// 2. Disable using 'HA command' origin 3 times (expect disabled state).
+ /// 3. Disable using 'DB connection' origin 1 time (expect disabled state).
+ /// 4. Reset using 'HA command' origin (expect disabled state).
+ /// 5. Enable using 'user command' origin 1 time (expect disabled state).
+ /// 6. Enable using 'DB connection' origin 1 time (expect enabled state).
+ /// 7. Disable using 'HA command' origin 3 times (expect disabled state).
+ /// 8. Reset using 'HA command' origin (expect enabled state).
+ void resetUsingHACommandOriginTest();
+
+ /// @brief This test verifies that reset works, so that internal state is reset after
+ /// all managers are recreated.
+ /// 1. Disable using 'user command' origin 1 time (expect disabled state).
+ /// 2. Disable using 'HA command' origin 1 time (expect disabled state).
+ /// 3. Disable using 'DB connection' origin 3 time (expect disabled state).
+ /// 4. Reset using 'DB connection' origin (expect disabled state).
+ /// 5. Enable using 'user command' origin 1 time (expect disabled state).
+ /// 6. Enable using 'DB connection' origin 1 time (expect enabled state).
+ /// 7. Disable using 'DB connection' origin 3 times (expect disabled state).
+ /// 8. Reset using 'DB connection' origin (expect enabled state).
+ void resetUsingDBConnectionOriginTest();
+
+ /// @brief This test verifies that enableAll() enables the service. This test will be
+ /// extended in the future to verify that it also enables disabled scopes.
+ void enableAllTest();
+
+ /// @brief This test verifies that it is possible to setup delayed execution of enableAll
+ /// function.
+ void delayedEnableAllTest();
+
+ /// @brief This test verifies that explicitly enabling the service cancels the timer
+ /// scheduled for automatically enabling it.
+ void earlyEnableAllTest();
+
+ /// @brief This test verifies that it is possible to call delayedEnableAll multiple times
+ /// and that it results in only one timer being scheduled.
+ void multipleDelayedEnableAllTest();
+
+ /// @brief This test verifies that it is possible to call delayedEnableAll multiple times
+ /// from different origins and that it results in each timer being scheduled.
+ void multipleDifferentOriginsDelayedEnableAllTest();
+
+ /// @brief Runs IO service with a timeout.
+ ///
+ /// @param timeout_ms Timeout for running IO service in milliseconds.
+ void runIOService(const long timeout_ms) {
+ sleep(timeout_ms/1000);
+ io_service_->poll();
+ }
+
+ /// @brief IO service used during the tests.
+ IOServicePtr io_service_;
+};
+
+// This test verifies the default is enable state.
+void
+NetworkStateTest::defaultTest() {
+ NetworkState state4(NetworkState::DHCPv4);
+ EXPECT_TRUE(state4.isServiceEnabled());
+ NetworkState state6(NetworkState::DHCPv6);
+ EXPECT_TRUE(state6.isServiceEnabled());
+}
+
+// This test verifies that it is possible to disable and then enable DHCPv4
+// service using 'user command' origin.
+void
+NetworkStateTest::disableEnableService4UsingUserCommandOriginTest() {
+ NetworkState state(NetworkState::DHCPv4);
+
+ // Test that enable/disable using 'user command' origin works
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+
+ // Test that using 'user command' origin does not use internal counter
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+}
+
+// This test verifies that it is possible to disable and then enable DHCPv4
+// service using 'HA command' origin.
+void
+NetworkStateTest::disableEnableService4UsingHACommandOriginTest() {
+ NetworkState state(NetworkState::DHCPv4);
+
+ // Test that enable/disable using 'HA command' origin works
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+
+ // Test that using 'HA command' origin does not use internal counter
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+}
+
+// This test verifies that it is possible to disable and then enable DHCPv4
+// service using 'DB connection' origin.
+void
+NetworkStateTest::disableEnableService4UsingDBConnectionOriginTest() {
+ NetworkState state(NetworkState::DHCPv4);
+
+ // Test that enable/disable using 'DB connection' origin works
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_TRUE(state.isServiceEnabled());
+
+ // Test that using 'DB connection' origin uses internal counter
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_TRUE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_TRUE(state.isServiceEnabled());
+}
+
+// This test verifies that it is possible to disable and then enable DHCPv4
+// service using a combination of origins.
+// 1. Disable using 'user command' origin 2 times (expect disabled state).
+// 2. Disable using 'HA command' origin 2 times (expect disabled state).
+// 3. Disable using 'DB connection' origin 2 times (expect disabled state).
+// 4. Enable using 'user command' origin 1 time (expect disabled state).
+// 5. Enable using 'HA command' origin 1 time (expect disabled state).
+// 6. Enable using 'DB connection' origin 2 times (expect enabled state).
+void
+NetworkStateTest::disableEnableService4UsingMultipleOriginsTest() {
+ NetworkState state(NetworkState::DHCPv4);
+
+ // Test that a combination properly affects the state
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_TRUE(state.isServiceEnabled());
+}
+
+// This test verifies that it is possible to disable and then enable DHCPv6
+// service using 'user command' origin.
+void
+NetworkStateTest::disableEnableService6UsingUserCommandOriginTest() {
+ NetworkState state(NetworkState::DHCPv6);
+
+ // Test that enable/disable using 'user command' origin works
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+
+ // Test that using 'user command' origin does not use internal counter
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+}
+
+// This test verifies that it is possible to disable and then enable DHCPv6
+// service using 'HA command' origin.
+void
+NetworkStateTest::disableEnableService6UsingHACommandOriginTest() {
+ NetworkState state(NetworkState::DHCPv6);
+
+ // Test that enable/disable using 'HA command' origin works
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+
+ // Test that using 'HA command' origin does not use internal counter
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+}
+
+// This test verifies that it is possible to disable and then enable DHCPv6
+// service using 'DB connection' origin.
+void
+NetworkStateTest::disableEnableService6UsingDBConnectionOriginTest() {
+ NetworkState state(NetworkState::DHCPv6);
+
+ // Test that enable/disable using 'DB connection' origin works
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_TRUE(state.isServiceEnabled());
+
+ // Test that using 'DB connection' origin uses internal counter
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_TRUE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_TRUE(state.isServiceEnabled());
+}
+
+// This test verifies that it is possible to disable and then enable DHCPv6
+// service using a combination of origins.
+// 1. Disable using 'user command' origin 2 times (expect disabled state).
+// 2. Disable using 'HA command' origin 2 times (expect disabled state).
+// 3. Disable using 'DB connection' origin 2 times (expect disabled state).
+// 4. Enable using 'user command' origin 1 time (expect disabled state).
+// 5. Enable using 'HA command' origin 1 time (expect disabled state).
+// 6. Enable using 'DB connection' origin 2 times (expect enabled state).
+void
+NetworkStateTest::disableEnableService6UsingMultipleOriginsTest() {
+ NetworkState state(NetworkState::DHCPv6);
+
+ // Test that a combination properly affects the state
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_TRUE(state.isServiceEnabled());
+}
+
+// This test verifies that reset works, so that internal state is reset after
+// all managers are recreated.
+// 1. Disable using 'user command' origin 3 times (expect disabled state).
+// 2. Disable using 'HA command' origin 1 time (expect disabled state).
+// 3. Disable using 'DB connection' origin 1 time (expect disabled state).
+// 4. Reset using 'user command' origin (expect disabled state).
+// 5. Enable using 'HA command' origin 1 time (expect disabled state).
+// 6. Enable using 'DB connection' origin 1 time (expect enabled state).
+// 7. Disable using 'user command' origin 3 times (expect disabled state).
+// 8. Reset using 'user command' origin (expect enabled state).
+void
+NetworkStateTest::resetUsingUserCommandOriginTest() {
+ NetworkState state(NetworkState::DHCPv4);
+
+ // Test User COMMAND + HA COMMAND + DB CONNECTION origins
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.reset(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_TRUE(state.isServiceEnabled());
+
+ // Test User COMMAND origin only
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.reset(NetworkState::Origin::USER_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+}
+
+// This test verifies that reset works, so that internal state is reset after
+// all managers are recreated.
+// 1. Disable using 'user command' origin 1 time (expect disabled state).
+// 2. Disable using 'HA command' origin 3 times (expect disabled state).
+// 3. Disable using 'DB connection' origin 1 time (expect disabled state).
+// 4. Reset using 'HA command' origin (expect disabled state).
+// 5. Enable using 'user command' origin 1 time (expect disabled state).
+// 6. Enable using 'DB connection' origin 1 time (expect enabled state).
+// 7. Disable using 'HA command' origin 3 times (expect disabled state).
+// 8. Reset using 'HA command' origin (expect enabled state).
+void
+NetworkStateTest::resetUsingHACommandOriginTest() {
+ NetworkState state(NetworkState::DHCPv4);
+
+ // Test HA COMMAND + User COMMAND + DB CONNECTION origins
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.reset(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_TRUE(state.isServiceEnabled());
+
+ // Test HA COMMAND origin only
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.reset(NetworkState::Origin::HA_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+}
+
+// This test verifies that reset works, so that internal state is reset after
+// all managers are recreated.
+// 1. Disable using 'user command' origin 1 time (expect disabled state).
+// 2. Disable using 'HA command' origin 1 time (expect disabled state).
+// 3. Disable using 'DB connection' origin 3 time (expect disabled state).
+// 4. Reset using 'DB connection' origin (expect disabled state).
+// 5. Enable using 'user command' origin 1 time (expect disabled state).
+// 6. Enable using 'DB connection' origin 1 time (expect enabled state).
+// 7. Disable using 'DB connection' origin 3 times (expect disabled state).
+// 8. Reset using 'DB connection' origin (expect enabled state).
+void
+NetworkStateTest::resetUsingDBConnectionOriginTest() {
+ NetworkState state(NetworkState::DHCPv4);
+
+ // Test DB CONNECTION + User COMMAND + HA COMMAND origins
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.reset(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+
+ // Test DB CONNECTION origin only
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.reset(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_TRUE(state.isServiceEnabled());
+}
+
+// This test verifies that enableAll() enables the service. This test will be
+// extended in the future to verify that it also enables disabled scopes.
+void
+NetworkStateTest::enableAllTest() {
+ NetworkState state(NetworkState::DHCPv4);
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableAll(NetworkState::Origin::USER_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableAll(NetworkState::Origin::HA_COMMAND);
+ EXPECT_TRUE(state.isServiceEnabled());
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_FALSE(state.isServiceEnabled());
+ state.enableAll(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_TRUE(state.isServiceEnabled());
+}
+
+// This test verifies that it is possible to setup delayed execution of enableAll
+// function.
+void
+NetworkStateTest::delayedEnableAllTest() {
+ NetworkState state(NetworkState::DHCPv4);
+ // Disable the service and then schedule enabling it in 1 second.
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ state.delayedEnableAll(1, NetworkState::Origin::USER_COMMAND);
+ // Initially the service should be still disabled.
+ EXPECT_FALSE(state.isServiceEnabled());
+ // After running IO service for 2 seconds, the service should be enabled.
+ runIOService(2000);
+ EXPECT_TRUE(state.isServiceEnabled());
+ // Disable the service and then schedule enabling it in 1 second.
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ state.delayedEnableAll(1, NetworkState::Origin::HA_COMMAND);
+ // Initially the service should be still disabled.
+ EXPECT_FALSE(state.isServiceEnabled());
+ // After running IO service for 2 seconds, the service should be enabled.
+ runIOService(2000);
+ EXPECT_TRUE(state.isServiceEnabled());
+ // Disable the service and then schedule enabling it in 1 second.
+ state.disableService(NetworkState::Origin::DB_CONNECTION);
+ EXPECT_THROW(state.delayedEnableAll(1, NetworkState::Origin::DB_CONNECTION), BadValue);
+}
+
+// This test verifies that explicitly enabling the service cancels the timer
+// scheduled for automatically enabling it.
+void
+NetworkStateTest::earlyEnableAllTest() {
+ NetworkState state(NetworkState::DHCPv4);
+ // Disable the service.
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ EXPECT_FALSE(state.isServiceEnabled());
+ // Schedule enabling the service in 2 seconds.
+ state.delayedEnableAll(2, NetworkState::Origin::USER_COMMAND);
+ // Explicitly enable the service.
+ state.enableAll(NetworkState::Origin::USER_COMMAND);
+ // The timer should be now canceled and the service should be enabled.
+ EXPECT_FALSE(state.isDelayedEnableAll());
+ EXPECT_TRUE(state.isServiceEnabled());
+}
+
+// This test verifies that it is possible to call delayedEnableAll multiple times
+// and that it results in only one timer being scheduled.
+void
+NetworkStateTest::multipleDelayedEnableAllTest() {
+ NetworkState state(NetworkState::DHCPv4);
+ // Disable the service and then schedule enabling it in 5 second.
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ // Schedule the first timer for 5 seconds.
+ state.delayedEnableAll(5, NetworkState::Origin::USER_COMMAND);
+ // When calling it the second time the old timer should be destroyed and
+ // the timeout should be set to 2 seconds.
+ state.delayedEnableAll(2, NetworkState::Origin::USER_COMMAND);
+ // Initially the service should be still disabled.
+ EXPECT_FALSE(state.isServiceEnabled());
+ // After running IO service for 3 seconds, the service should be enabled.
+ runIOService(3000);
+ EXPECT_TRUE(state.isServiceEnabled());
+ // The timer should not be present, even though the first timer was created
+ // with 5 seconds interval.
+ EXPECT_FALSE(state.isDelayedEnableAll());
+}
+
+// This test verifies that it is possible to call delayedEnableAll multiple times
+// from different origins and that it results in each timer being scheduled.
+void
+NetworkStateTest::multipleDifferentOriginsDelayedEnableAllTest() {
+ NetworkState state(NetworkState::DHCPv4);
+ // Disable the service and then schedule enabling it in 5 second.
+ state.disableService(NetworkState::Origin::HA_COMMAND);
+ // Disable the service and then schedule enabling it in 2 second.
+ state.disableService(NetworkState::Origin::USER_COMMAND);
+ // Schedule the first timer for 5 seconds.
+ state.delayedEnableAll(5, NetworkState::Origin::HA_COMMAND);
+ // When calling it the second time the old timer should not be destroyed and
+ // the new timeout should be set to 2 seconds.
+ state.delayedEnableAll(2, NetworkState::Origin::USER_COMMAND);
+ // Initially the service should be still disabled.
+ EXPECT_FALSE(state.isServiceEnabled());
+ // After running IO service for 3 seconds, the service should not be enabled.
+ runIOService(3000);
+ EXPECT_FALSE(state.isServiceEnabled());
+ // The timer should be present, because the first timer was created with 5
+ // seconds interval.
+ EXPECT_TRUE(state.isDelayedEnableAll());
+ // After running IO service for 3 seconds, the service should be enabled.
+ runIOService(3000);
+ EXPECT_TRUE(state.isServiceEnabled());
+ // The timer should not be present, because the first timer was created with
+ // 5 seconds interval.
+ EXPECT_FALSE(state.isDelayedEnableAll());
+}
+
+// Test invocations.
+
+TEST_F(NetworkStateTest, defaultTest) {
+ defaultTest();
+}
+
+TEST_F(NetworkStateTest, defaultTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ defaultTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService4UsingUserCommandOriginTest) {
+ disableEnableService4UsingUserCommandOriginTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService4UsingUserCommandOriginTestMultilThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ disableEnableService4UsingUserCommandOriginTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService4UsingHACommandOriginTest) {
+ disableEnableService4UsingHACommandOriginTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService4UsingHACommandOriginTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ disableEnableService4UsingHACommandOriginTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService4UsingDBConnectionOriginTest) {
+ disableEnableService4UsingDBConnectionOriginTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService4UsingDBConnectionOriginTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ disableEnableService4UsingDBConnectionOriginTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService4UsingMultipleOriginsTest) {
+ disableEnableService4UsingMultipleOriginsTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService4UsingMultipleOriginsTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ disableEnableService4UsingMultipleOriginsTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService6UsingUserCommandOriginTest) {
+ disableEnableService6UsingUserCommandOriginTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService6UsingUserCommandOriginTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ disableEnableService6UsingUserCommandOriginTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService6UsingHACommandOriginTest) {
+ disableEnableService6UsingHACommandOriginTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService6UsingHACommandOriginTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ disableEnableService6UsingHACommandOriginTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService6UsingDBConnectionOriginTest) {
+ disableEnableService6UsingDBConnectionOriginTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService6UsingDBConnectionOriginTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ disableEnableService6UsingDBConnectionOriginTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService6UsingMultipleOriginsTest) {
+ disableEnableService6UsingMultipleOriginsTest();
+}
+
+TEST_F(NetworkStateTest, disableEnableService6UsingMultipleOriginsTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ disableEnableService6UsingMultipleOriginsTest();
+}
+
+TEST_F(NetworkStateTest, resetUsingUserCommandOriginTest) {
+ resetUsingUserCommandOriginTest();
+}
+
+TEST_F(NetworkStateTest, resetUsingUserCommandOriginTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ resetUsingUserCommandOriginTest();
+}
+
+TEST_F(NetworkStateTest, resetUsingDBConnectionOriginTest) {
+ resetUsingDBConnectionOriginTest();
+}
+
+TEST_F(NetworkStateTest, resetUsingDBConnectionOriginTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ resetUsingDBConnectionOriginTest();
+}
+
+TEST_F(NetworkStateTest, resetUsingHACommandOriginTest) {
+ resetUsingHACommandOriginTest();
+}
+
+TEST_F(NetworkStateTest, resetUsingHACommandOriginTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ resetUsingHACommandOriginTest();
+}
+
+TEST_F(NetworkStateTest, enableAllTest) {
+ enableAllTest();
+}
+
+TEST_F(NetworkStateTest, enableAllTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ enableAllTest();
+}
+
+TEST_F(NetworkStateTest, delayedEnableAllTest) {
+ delayedEnableAllTest();
+}
+
+TEST_F(NetworkStateTest, delayedEnableAllTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ delayedEnableAllTest();
+}
+
+TEST_F(NetworkStateTest, earlyEnableAllTest) {
+ earlyEnableAllTest();
+}
+
+TEST_F(NetworkStateTest, earlyEnableAllTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ earlyEnableAllTest();
+}
+
+TEST_F(NetworkStateTest, multipleDelayedEnableAllTest) {
+ multipleDelayedEnableAllTest();
+}
+
+TEST_F(NetworkStateTest, multipleDelayedEnableAllTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ multipleDelayedEnableAllTest();
+}
+
+TEST_F(NetworkStateTest, multipleDifferentOriginsDelayedEnableAllTest) {
+ multipleDifferentOriginsDelayedEnableAllTest();
+}
+
+TEST_F(NetworkStateTest, multipleDifferentOriginsDelayedEnableAllTestMultiThreading) {
+ MultiThreadingMgr::instance().setMode(true);
+ multipleDifferentOriginsDelayedEnableAllTest();
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/network_unittest.cc b/src/lib/dhcpsrv/tests/network_unittest.cc
new file mode 100644
index 0000000..725edc9
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/network_unittest.cc
@@ -0,0 +1,777 @@
+// Copyright (C) 2019-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/option.h>
+#include <dhcpsrv/network.h>
+#include <dhcpsrv/parsers/base_network_parser.h>
+#include <testutils/gtest_utils.h>
+#include <boost/shared_ptr.hpp>
+#include <functional>
+#include <gtest/gtest.h>
+
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::util;
+using namespace isc::test;
+
+namespace {
+
+class TestNetwork;
+
+/// @brief Shared pointer to the derivation of the @c Network class
+/// used in tests.
+typedef boost::shared_ptr<TestNetwork> TestNetworkPtr;
+
+/// @brief Derivation of the @c Network class allowing to set
+/// the parent @c Network object.
+class TestNetwork : public virtual Network {
+public:
+
+ /// @brief Associates parent network with this network.
+ ///
+ /// @param parent Pointer to the instance of the parent network.
+ void setParent(TestNetworkPtr parent) {
+ parent_network_ = boost::dynamic_pointer_cast<Network>(parent);
+ }
+};
+
+/// @brief Derivation of the @c Network4 class allowing to set
+/// the parent @c Network object.
+class TestNetwork4 : public TestNetwork, public Network4 { };
+
+/// @brief Derivation of the @c Network6 class allowing to set
+/// the parent @c Network object.
+class TestNetwork6 : public TestNetwork, public Network6 { };
+
+/// @brief Test fixture class for testing @c Network class and
+/// its derivations.
+class NetworkTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ NetworkTest() : globals_(new CfgGlobals()) {
+ }
+
+ /// @brief Returns pointer to the function which returns configured
+ /// global parameters.
+ FetchNetworkGlobalsFn getFetchGlobalsFn() {
+ return (std::bind(&NetworkTest::fetchGlobalsFn, this));
+ }
+
+ /// @brief Returns configured global parameters.
+ ConstCfgGlobalsPtr fetchGlobalsFn() {
+ return (globals_);
+ }
+
+ /// @brief Test that inheritance mechanism is used for the particular
+ /// network parameter.
+ ///
+ /// @tparam BaseType1 Type of the network object to be tested, e.g.
+ /// @c TestNetwork, @c TestNetwork4 etc.
+ /// @tparam BaseType2 Type of the object to which the tested parameter
+ /// belongs, e.g. @c Network, @c Network4 etc.
+ /// @tparam ParameterType1 Type returned by the accessor of the parameter
+ /// under test, e.g. @c Optional<std::string>.
+ /// @tparam ParameterType2 Type of the value accepted by the modifier
+ /// method which sets the value under test. It may be the same as
+ /// @c ParameterType1 but not necessarily. For example, the @c ParameterType1
+ /// may be @c Optional<std::string> while the @c ParameterType2 is
+ /// @c std::string.
+ ///
+ /// @param GetMethodPointer Pointer to the method of the class under test
+ /// which returns the particular parameter.
+ /// @param SetMethodPointer Pointer to the method of the class under test
+ /// used to set the particular parameter.
+ /// @param network_value Value of the parameter to be assigned to the
+ /// parent network.
+ /// @param global_value Global value of the parameter to be assigned.
+ /// @param test_global_value Boolean value indicating if the inheritance
+ /// of the global value should be tested.
+ template<typename BaseType1, typename BaseType2, typename ParameterType1,
+ typename ParameterType2>
+ void testNetworkInheritance(ParameterType1(BaseType2::*GetMethodPointer)
+ (const Network::Inheritance&) const,
+ void(BaseType2::*SetMethodPointer)(const ParameterType2&),
+ typename ParameterType1::ValueType network_value,
+ typename ParameterType1::ValueType global_value,
+ const bool test_global_value = true) {
+
+ // Create child network object. The value retrieved from that
+ // object should be unspecified until we set the value for the
+ // parent network or a global value.
+ boost::shared_ptr<BaseType1> net_child(new BaseType1());
+ EXPECT_TRUE(((*net_child).*GetMethodPointer)(Network::Inheritance::ALL).unspecified());
+
+ // Create parent network and set the value.
+ boost::shared_ptr<BaseType1> net_parent(new BaseType1());
+ ((*net_parent).*SetMethodPointer)(network_value);
+ EXPECT_EQ(network_value,
+ ((*net_parent).*GetMethodPointer)(Network::Inheritance::ALL).get());
+
+ // Assign callbacks that fetch global values to the networks.
+ net_child->setFetchGlobalsFn(getFetchGlobalsFn());
+ net_parent->setFetchGlobalsFn(getFetchGlobalsFn());
+
+ // Not all parameters have the corresponding global values.
+ if (test_global_value) {
+ // If there is a global value it should now be returned.
+ EXPECT_FALSE(((*net_child).*GetMethodPointer)(Network::Inheritance::ALL).unspecified());
+ EXPECT_EQ(global_value,
+ ((*net_child).*GetMethodPointer)(Network::Inheritance::ALL).get());
+
+ EXPECT_FALSE(((*net_child).*GetMethodPointer)(Network::Inheritance::GLOBAL).unspecified());
+ EXPECT_EQ(global_value,
+ ((*net_child).*GetMethodPointer)(Network::Inheritance::GLOBAL).get());
+
+ EXPECT_TRUE(((*net_child).*GetMethodPointer)(Network::Inheritance::NONE).unspecified());
+ EXPECT_TRUE(((*net_child).*GetMethodPointer)(Network::Inheritance::PARENT_NETWORK).unspecified());
+ }
+
+ // Associated the network with its parent.
+ ASSERT_NO_THROW(net_child->setParent(net_parent));
+
+ // This time the parent specific value should be returned.
+ EXPECT_FALSE(((*net_child).*GetMethodPointer)(Network::Inheritance::ALL).unspecified());
+ EXPECT_EQ(network_value,
+ ((*net_child).*GetMethodPointer)(Network::Inheritance::ALL).get());
+ }
+
+ /// @brief Holds the collection of configured globals.
+ CfgGlobalsPtr globals_;
+};
+
+// This test verifies that the inheritance is supported for certain
+// network parameters.
+TEST_F(NetworkTest, inheritanceSupport4) {
+ // Set global values for each parameter.
+ // One day move to indexes...
+ globals_->set("valid-lifetime", Element::create(80));
+ globals_->set("renew-timer", Element::create(80));
+ globals_->set("rebind-timer", Element::create(80));
+ globals_->set("reservations-global", Element::create(false));
+ globals_->set("reservations-in-subnet", Element::create(false));
+ globals_->set("reservations-out-of-pool", Element::create(false));
+ globals_->set("calculate-tee-times", Element::create(false));
+ globals_->set("t1-percent", Element::create(0.75));
+ globals_->set("t2-percent", Element::create(0.6));
+ globals_->set("match-client-id", Element::create(true));
+ globals_->set("authoritative", Element::create(false));
+ globals_->set("next-server", Element::create("192.0.2.3"));
+ globals_->set("server-hostname", Element::create("g"));
+ globals_->set("boot-file-name", Element::create("g"));
+ globals_->set("ddns-send-updates", Element::create(true));
+ globals_->set("ddns-override-no-update", Element::create(true));
+ globals_->set("ddns-override-client-update", Element::create(true));
+ globals_->set("ddns-replace-client-name", Element::create("always"));
+ globals_->set("ddns-generated-prefix", Element::create("gp"));
+ globals_->set("ddns-qualifying-suffix", Element::create("gs"));
+ globals_->set("hostname-char-set", Element::create("gc"));
+ globals_->set("hostname-char-replacement", Element::create("gr"));
+ globals_->set("store-extended-info", Element::create(true));
+ globals_->set("cache-threshold", Element::create(.25));
+ globals_->set("cache-max-age", Element::create(20));
+ globals_->set("ddns-update-on-renew", Element::create(true));
+ globals_->set("ddns-use-conflict-resolution", Element::create(true));
+
+ // For each parameter for which inheritance is supported run
+ // the test that checks if the values are inherited properly.
+
+ {
+ SCOPED_TRACE("client_class");
+ testNetworkInheritance<TestNetwork>(&Network::getClientClass,
+ &Network::allowClientClass,
+ "n", "g", false);
+ }
+ {
+ SCOPED_TRACE("valid-lifetime");
+ testNetworkInheritance<TestNetwork>(&Network::getValid, &Network::setValid,
+ 60, 80);
+ }
+ {
+ SCOPED_TRACE("renew-timer");
+ testNetworkInheritance<TestNetwork>(&Network::getT1, &Network::setT1,
+ 60, 80);
+ }
+ {
+ SCOPED_TRACE("rebind-timer");
+ testNetworkInheritance<TestNetwork>(&Network::getT2, &Network::setT2,
+ 60, 80);
+ }
+ {
+ SCOPED_TRACE("reservation-global");
+ testNetworkInheritance<TestNetwork>(&Network::getReservationsGlobal,
+ &Network::setReservationsGlobal,
+ true, false);
+ }
+ {
+ SCOPED_TRACE("reservation-in-subnet");
+ testNetworkInheritance<TestNetwork>(&Network::getReservationsInSubnet,
+ &Network::setReservationsInSubnet,
+ true, false);
+ }
+ {
+ SCOPED_TRACE("reservation-out-of-pool");
+ testNetworkInheritance<TestNetwork>(&Network::getReservationsOutOfPool,
+ &Network::setReservationsOutOfPool,
+ true, false);
+ }
+ {
+ SCOPED_TRACE("calculate-tee-times");
+ testNetworkInheritance<TestNetwork>(&Network::getCalculateTeeTimes,
+ &Network::setCalculateTeeTimes,
+ true, false);
+ }
+ {
+ SCOPED_TRACE("t1-percent");
+ testNetworkInheritance<TestNetwork>(&Network::getT1Percent,
+ &Network::setT1Percent,
+ 0.5, 0.75);
+ }
+ {
+ SCOPED_TRACE("t2-percent");
+ testNetworkInheritance<TestNetwork>(&Network::getT2Percent,
+ &Network::setT2Percent,
+ 0.3, 0.6);
+ }
+ {
+ SCOPED_TRACE("match-client-id");
+ testNetworkInheritance<TestNetwork4>(&Network4::getMatchClientId,
+ &Network4::setMatchClientId,
+ false, true);
+ }
+ {
+ SCOPED_TRACE("authoritative");
+ testNetworkInheritance<TestNetwork4>(&Network4::getAuthoritative,
+ &Network4::setAuthoritative,
+ true, false);
+ }
+ {
+ SCOPED_TRACE("next-server");
+ testNetworkInheritance<TestNetwork4>(&Network4::getSiaddr,
+ &Network4::setSiaddr,
+ IOAddress("192.0.2.0"),
+ IOAddress("192.0.2.3"));
+ }
+ {
+ SCOPED_TRACE("server-hostname");
+ testNetworkInheritance<TestNetwork4>(&Network4::getSname,
+ &Network4::setSname,
+ "n", "g");
+ }
+ {
+ SCOPED_TRACE("boot-file-name");
+ testNetworkInheritance<TestNetwork4>(&Network4::getFilename,
+ &Network4::setFilename,
+ "n", "g");
+ }
+ {
+ SCOPED_TRACE("ddns-send-updates");
+ testNetworkInheritance<TestNetwork4>(&Network4::getDdnsSendUpdates,
+ &Network4::setDdnsSendUpdates,
+ false, true);
+ }
+ {
+ SCOPED_TRACE("ddns-override-no-update");
+ testNetworkInheritance<TestNetwork4>(&Network4::getDdnsOverrideNoUpdate,
+ &Network4::setDdnsOverrideNoUpdate,
+ false, true);
+ }
+ {
+ SCOPED_TRACE("ddns-override-client-update");
+ testNetworkInheritance<TestNetwork4>(&Network4::getDdnsOverrideClientUpdate,
+ &Network4::setDdnsOverrideClientUpdate,
+ false, true);
+ }
+
+ {
+ SCOPED_TRACE("ddns-replace-client-name");
+ testNetworkInheritance<TestNetwork4>(&Network4::getDdnsReplaceClientNameMode,
+ &Network4::setDdnsReplaceClientNameMode,
+ D2ClientConfig::RCM_WHEN_PRESENT,
+ D2ClientConfig::RCM_ALWAYS);
+ }
+ {
+ SCOPED_TRACE("ddns-generated-prefix");
+ testNetworkInheritance<TestNetwork4>(&Network4::getDdnsGeneratedPrefix,
+ &Network4::setDdnsGeneratedPrefix,
+ "np", "gp");
+ }
+ {
+ SCOPED_TRACE("ddns-qualifying-suffix");
+ testNetworkInheritance<TestNetwork4>(&Network4::getDdnsQualifyingSuffix,
+ &Network4::setDdnsQualifyingSuffix,
+ "ns", "gs");
+ }
+ {
+ SCOPED_TRACE("hostname-char-set");
+ testNetworkInheritance<TestNetwork4>(&Network4::getHostnameCharSet,
+ &Network4::setHostnameCharSet,
+ "nc", "gc");
+ }
+ {
+ SCOPED_TRACE("hostname-char-replacement");
+ testNetworkInheritance<TestNetwork4>(&Network4::getHostnameCharReplacement,
+ &Network4::setHostnameCharReplacement,
+ "nr", "gr");
+ }
+ {
+ SCOPED_TRACE("store-extended-info");
+ testNetworkInheritance<TestNetwork4>(&Network4::getStoreExtendedInfo,
+ &Network4::setStoreExtendedInfo,
+ false, true);
+ }
+ {
+ SCOPED_TRACE("cache-threshold");
+ testNetworkInheritance<TestNetwork4>(&Network::getCacheThreshold,
+ &Network::setCacheThreshold,
+ .1, .25);
+ }
+ {
+ SCOPED_TRACE("cache-max-age");
+ testNetworkInheritance<TestNetwork4>(&Network::getCacheMaxAge,
+ &Network::setCacheMaxAge,
+ 10, 20);
+ }
+ {
+ SCOPED_TRACE("ddns-update-on-renew");
+ testNetworkInheritance<TestNetwork4>(&Network4::getDdnsUpdateOnRenew,
+ &Network4::setDdnsUpdateOnRenew,
+ false, true);
+ }
+ {
+ SCOPED_TRACE("ddns-use-conflict-resolution");
+ testNetworkInheritance<TestNetwork4>(&Network4::getDdnsUseConflictResolution,
+ &Network4::setDdnsUseConflictResolution,
+ false, true);
+ }
+}
+
+// This test verifies that the inheritance is supported for DHCPv6
+// specific network parameters.
+TEST_F(NetworkTest, inheritanceSupport6) {
+ // Set global values for each parameter.
+ globals_->set("preferred-lifetime", Element::create(80));
+ // Note that currently rapid commit is not a global parameter.
+ globals_->set("ddns-send-updates", Element::create(true));
+ globals_->set("ddns-override-no-update", Element::create(true));
+ globals_->set("ddns-override-client-update", Element::create(true));
+ globals_->set("ddns-replace-client-name", Element::create("always"));
+ globals_->set("ddns-generated-prefix", Element::create("gp"));
+ globals_->set("ddns-qualifying-suffix", Element::create("gs"));
+ globals_->set("hostname-char-set", Element::create("gc"));
+ globals_->set("hostname-char-replacement", Element::create("gr"));
+ globals_->set("store-extended-info", Element::create(true));
+ globals_->set("ddns-update-on-renew", Element::create(true));
+ globals_->set("ddns-use-conflict-resolution", Element::create(true));
+
+ // For each parameter for which inheritance is supported run
+ // the test that checks if the values are inherited properly.
+
+ {
+ SCOPED_TRACE("preferred-lifetime");
+ testNetworkInheritance<TestNetwork6>(&Network6::getPreferred,
+ &Network6::setPreferred,
+ 60, 80);
+ }
+ {
+ SCOPED_TRACE("ddns-send-updates");
+ testNetworkInheritance<TestNetwork6>(&Network6::getDdnsSendUpdates,
+ &Network6::setDdnsSendUpdates,
+ false, true);
+ }
+ {
+ SCOPED_TRACE("ddns-override-no-update");
+ testNetworkInheritance<TestNetwork6>(&Network6::getDdnsOverrideNoUpdate,
+ &Network6::setDdnsOverrideNoUpdate,
+ false, true);
+ }
+ {
+ SCOPED_TRACE("ddns-override-client-update");
+ testNetworkInheritance<TestNetwork6>(&Network6::getDdnsOverrideClientUpdate,
+ &Network6::setDdnsOverrideClientUpdate,
+ false, true);
+ }
+
+ {
+ SCOPED_TRACE("ddns-replace-client-name");
+ testNetworkInheritance<TestNetwork6>(&Network6::getDdnsReplaceClientNameMode,
+ &Network6::setDdnsReplaceClientNameMode,
+ D2ClientConfig::RCM_WHEN_PRESENT,
+ D2ClientConfig::RCM_ALWAYS);
+ }
+ {
+ SCOPED_TRACE("ddns-generated-prefix");
+ testNetworkInheritance<TestNetwork6>(&Network6::getDdnsGeneratedPrefix,
+ &Network6::setDdnsGeneratedPrefix,
+ "np", "gp");
+ }
+ {
+ SCOPED_TRACE("ddns-qualifying-suffix");
+ testNetworkInheritance<TestNetwork6>(&Network6::getDdnsQualifyingSuffix,
+ &Network6::setDdnsQualifyingSuffix,
+ "ns", "gs");
+ }
+ {
+ SCOPED_TRACE("hostname-char-set");
+ testNetworkInheritance<TestNetwork6>(&Network6::getHostnameCharSet,
+ &Network6::setHostnameCharSet,
+ "nc", "gc");
+ }
+ {
+ SCOPED_TRACE("hostname-char-replacement");
+ testNetworkInheritance<TestNetwork6>(&Network6::getHostnameCharReplacement,
+ &Network6::setHostnameCharReplacement,
+ "nr", "gr");
+ }
+ {
+ SCOPED_TRACE("store-extended-info");
+ testNetworkInheritance<TestNetwork6>(&Network6::getStoreExtendedInfo,
+ &Network6::setStoreExtendedInfo,
+ false, true);
+ }
+ {
+ SCOPED_TRACE("ddns-update-on-renew");
+ testNetworkInheritance<TestNetwork6>(&Network6::getDdnsUpdateOnRenew,
+ &Network6::setDdnsUpdateOnRenew,
+ false, true);
+ }
+ {
+ SCOPED_TRACE("ddns-use-conflict-resolution");
+ testNetworkInheritance<TestNetwork6>(&Network6::getDdnsUseConflictResolution,
+ &Network6::setDdnsUseConflictResolution,
+ false, true);
+ }
+
+ // Interface-id requires special type of test.
+ boost::shared_ptr<TestNetwork6> net_child(new TestNetwork6());
+ EXPECT_FALSE(net_child->getInterfaceId());
+ EXPECT_FALSE(net_child->getInterfaceId(Network::Inheritance::NONE));
+ EXPECT_FALSE(net_child->getInterfaceId(Network::Inheritance::PARENT_NETWORK));
+ EXPECT_FALSE(net_child->getInterfaceId(Network::Inheritance::GLOBAL));
+
+ OptionPtr interface_id(new Option(Option::V6, D6O_INTERFACE_ID,
+ OptionBuffer(10, 0xFF)));
+
+ boost::shared_ptr<TestNetwork6> net_parent(new TestNetwork6());
+ net_parent->setInterfaceId(interface_id);
+
+ ASSERT_NO_THROW(net_child->setParent(net_parent));
+
+ // The interface-id belongs to the parent.
+ EXPECT_TRUE(net_child->getInterfaceId());
+ EXPECT_FALSE(net_child->getInterfaceId(Network::Inheritance::NONE));
+ EXPECT_TRUE(net_child->getInterfaceId(Network::Inheritance::PARENT_NETWORK));
+ EXPECT_FALSE(net_child->getInterfaceId(Network::Inheritance::GLOBAL));
+
+ // Check the values are expected.
+ EXPECT_EQ(interface_id, net_child->getInterfaceId());
+ EXPECT_EQ(interface_id, net_child->getInterfaceId(Network::Inheritance::PARENT_NETWORK));
+
+ // Assign different interface id to a child.
+ interface_id.reset(new Option(Option::V6, D6O_INTERFACE_ID,
+ OptionBuffer(10, 0xFE)));
+ net_child->setInterfaceId(interface_id);
+
+ // This time, the child specific value can be fetched.
+ EXPECT_TRUE(net_child->getInterfaceId());
+ EXPECT_TRUE(net_child->getInterfaceId(Network::Inheritance::NONE));
+ EXPECT_TRUE(net_child->getInterfaceId(Network::Inheritance::PARENT_NETWORK));
+ EXPECT_FALSE(net_child->getInterfaceId(Network::Inheritance::GLOBAL));
+
+ EXPECT_EQ(interface_id, net_child->getInterfaceId());
+ EXPECT_EQ(interface_id, net_child->getInterfaceId(Network::Inheritance::NONE));
+}
+
+// Test that child network returns unspecified value if neither
+// parent no global value exists.
+TEST_F(NetworkTest, getPropertyNoParentNoChild) {
+ NetworkPtr net_child(new Network());
+ EXPECT_TRUE(net_child->getValid().unspecified());
+}
+
+// Test that child network returns specified value.
+TEST_F(NetworkTest, getPropertyNoParentChild) {
+ NetworkPtr net_child(new Network());
+ net_child->setValid(12345);
+
+ EXPECT_FALSE(net_child->getValid().unspecified());
+ EXPECT_FALSE(net_child->getValid(Network::Inheritance::NONE).unspecified());
+ EXPECT_TRUE(net_child->getValid(Network::Inheritance::PARENT_NETWORK).unspecified());
+ EXPECT_TRUE(net_child->getValid(Network::Inheritance::GLOBAL).unspecified());
+
+ EXPECT_EQ(12345, net_child->getValid(Network::Inheritance::NONE).get());
+ EXPECT_EQ(12345, net_child->getValid().get());
+}
+
+// Test that parent specific value is returned when the value
+// is not specified for the child network.
+TEST_F(NetworkTest, getPropertyParentNoChild) {
+ TestNetworkPtr net_child(new TestNetwork());
+ EXPECT_TRUE(net_child->getValid().unspecified());
+
+ TestNetworkPtr net_parent(new TestNetwork());
+ net_parent->setValid(23456);
+ EXPECT_EQ(23456, net_parent->getValid().get());
+
+ ASSERT_NO_THROW(net_child->setParent(net_parent));
+
+ EXPECT_FALSE(net_child->getValid().unspecified());
+ EXPECT_TRUE(net_child->getValid(Network::Inheritance::NONE).unspecified());
+ EXPECT_FALSE(net_child->getValid(Network::Inheritance::PARENT_NETWORK).unspecified());
+ EXPECT_TRUE(net_child->getValid(Network::Inheritance::GLOBAL).unspecified());
+
+ EXPECT_EQ(23456, net_child->getValid().get());
+}
+
+// Test that value specified for the child network takes
+// precedence over the value specified for the parent network.
+TEST_F(NetworkTest, getPropertyParentChild) {
+ TestNetworkPtr net_child(new TestNetwork());
+ net_child->setValid(12345);
+ EXPECT_EQ(12345, net_child->getValid().get());
+
+ TestNetworkPtr net_parent(new TestNetwork());
+ net_parent->setValid(23456);
+ EXPECT_EQ(23456, net_parent->getValid().get());
+
+ ASSERT_NO_THROW(net_child->setParent(net_parent));
+
+ EXPECT_FALSE(net_child->getValid().unspecified());
+ EXPECT_FALSE(net_child->getValid(Network::Inheritance::NONE).unspecified());
+ EXPECT_FALSE(net_child->getValid(Network::Inheritance::PARENT_NETWORK).unspecified());
+ EXPECT_TRUE(net_child->getValid(Network::Inheritance::GLOBAL).unspecified());
+
+ EXPECT_EQ(12345, net_child->getValid().get());
+}
+
+// Test that global value is inherited if there is no network
+// specific value.
+TEST_F(NetworkTest, getPropertyGlobalNoParentNoChild) {
+ TestNetworkPtr net_child(new TestNetwork());
+
+ globals_->set("valid-lifetime", Element::create(34567));
+
+ net_child->setFetchGlobalsFn(getFetchGlobalsFn());
+
+ EXPECT_FALSE(net_child->getValid().unspecified());
+ EXPECT_TRUE(net_child->getValid(Network::Inheritance::NONE).unspecified());
+ EXPECT_TRUE(net_child->getValid(Network::Inheritance::PARENT_NETWORK).unspecified());
+ EXPECT_FALSE(net_child->getValid(Network::Inheritance::GLOBAL).unspecified());
+
+ EXPECT_EQ(34567, net_child->getValid().get());
+}
+
+// Test that getSiaddr() never fails.
+TEST_F(NetworkTest, getSiaddrNeverFail) {
+ TestNetworkPtr net_child(new TestNetwork4());
+
+ // Set the next-server textual address to the empty string.
+ // Note that IOAddress("") throws IOError.
+ globals_->set("next-server", Element::create(""));
+
+ net_child->setFetchGlobalsFn(getFetchGlobalsFn());
+
+ // Get an IPv4 view of the test network.
+ auto net4_child = boost::dynamic_pointer_cast<Network4>(net_child);
+ EXPECT_NO_THROW(net4_child->getSiaddr());
+}
+
+/// @brief Test fixture class for testing @c moveReservationMode.
+class NetworkReservationTest : public ::testing::Test {
+public:
+
+ /// @brief Move test error case.
+ ///
+ /// Error cases of @ref BaseNetworkParser::moveReservationMode.
+ ///
+ /// @param config String with the config to test.
+ /// @param expected String with the expected error message.
+ void TestError(const std::string& config, const std::string& expected) {
+ ElementPtr cfg;
+ ASSERT_NO_THROW(cfg = Element::fromJSON(config))
+ << "bad config, test broken";
+
+ ElementPtr copy = isc::data::copy(cfg);
+
+ EXPECT_THROW_MSG(BaseNetworkParser::moveReservationMode(cfg),
+ DhcpConfigError, expected);
+
+ ASSERT_TRUE(copy->equals(*cfg));
+ }
+
+ /// @brief Move test case.
+ ///
+ /// Test cases of @ref BaseNetworkParser::moveReservationMode.
+ ///
+ /// @param config String with the config to test.
+ /// @param expected String with the config after move.
+ void TestMove(const std::string& config, const std::string& expected) {
+ ElementPtr cfg;
+ ASSERT_NO_THROW(cfg = Element::fromJSON(config))
+ << "bad config, test broken";
+
+ EXPECT_NO_THROW(BaseNetworkParser::moveReservationMode(cfg));
+
+ EXPECT_EQ(expected, cfg->str());
+ }
+};
+
+/// @brief Test @ref BaseNetworkParser::moveReservationMode error cases.
+TEST_F(NetworkReservationTest, errors) {
+ // Conflicts.
+ std::string config = "{\n"
+ "\"reservation-mode\": \"all\",\n"
+ "\"reservations-global\": true\n"
+ "}";
+ std::string expected = "invalid use of both 'reservation-mode'"
+ " and one of 'reservations-global', 'reservations-in-subnet'"
+ " or 'reservations-out-of-pool' parameters";
+ TestError(config, expected);
+
+ config = "{\n"
+ "\"reservation-mode\": \"all\",\n"
+ "\"reservations-in-subnet\": true\n"
+ "}";
+ TestError(config, expected);
+
+ config = "{\n"
+ "\"reservation-mode\": \"all\",\n"
+ "\"reservations-out-of-pool\": false\n"
+ "}";
+ TestError(config, expected);
+
+ // Unknown mode.
+ config = "{\n"
+ "\"reservation-mode\": \"foo\"\n"
+ "}";
+ expected = "invalid reservation-mode parameter: 'foo' (<string>:2:21)";
+ TestError(config, expected);
+}
+
+/// @brief Test @ref BaseNetworkParser::moveReservationMode.
+TEST_F(NetworkReservationTest, move) {
+ // No-ops.
+ std::string config = "{\n"
+ "}";
+ std::string expected = "{ "
+ " }";
+ TestMove(config, expected);
+
+ config = "{\n"
+ "\"reservations-global\": true\n"
+ "}";
+ expected = "{"
+ " \"reservations-global\": true"
+ " }";
+ TestMove(config, expected);
+
+ // Disabled.
+ config = "{\n"
+ "\"reservation-mode\": \"disabled\"\n"
+ "}";
+ expected = "{"
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": false"
+ " }";
+ TestMove(config, expected);
+
+ config = "{\n"
+ "\"reservation-mode\": \"off\"\n"
+ "}";
+ TestMove(config, expected);
+
+ // Out-of-pool.
+ config = "{\n"
+ "\"reservation-mode\": \"out-of-pool\"\n"
+ "}";
+ expected = "{"
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": true"
+ " }";
+ TestMove(config, expected);
+
+ // Global.
+ config = "{\n"
+ "\"reservation-mode\": \"global\"\n"
+ "}";
+ expected = "{"
+ " \"reservations-global\": true,"
+ " \"reservations-in-subnet\": false"
+ " }";
+ TestMove(config, expected);
+
+ // All.
+ config = "{\n"
+ "\"reservation-mode\": \"all\"\n"
+ "}";
+ expected = "{"
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": false"
+ " }";
+ TestMove(config, expected);
+
+ config = "{\n"
+ "\"foobar\": 1234,\n"
+ "\"reservation-mode\": \"all\"\n"
+ "}";
+ expected = "{"
+ " \"foobar\": 1234,"
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": false"
+ " }";
+ TestMove(config, expected);
+}
+
+// This test verifies that the inheritance is supported for triplets.
+// Note that triplets have no comparison operator.
+TEST_F(NetworkTest, inheritanceTriplet) {
+ NetworkPtr net(new Network());
+ EXPECT_TRUE(net->getValid().unspecified());
+ EXPECT_TRUE(net->getValid(Network::Inheritance::ALL).unspecified());
+ EXPECT_TRUE(net->getValid(Network::Inheritance::GLOBAL).unspecified());
+
+ // Set valid lifetime global parameter.
+ globals_->set("valid-lifetime", Element::create(200));
+ net->setFetchGlobalsFn(getFetchGlobalsFn());
+ EXPECT_FALSE(net->getValid().unspecified());
+ EXPECT_FALSE(net->getValid(Network::Inheritance::ALL).unspecified());
+ EXPECT_FALSE(net->getValid(Network::Inheritance::GLOBAL).unspecified());
+ EXPECT_EQ(200, net->getValid().get());
+ EXPECT_EQ(200, net->getValid(Network::Inheritance::ALL).get());
+ EXPECT_EQ(200, net->getValid(Network::Inheritance::GLOBAL).get());
+ EXPECT_EQ(200, net->getValid().getMin());
+ EXPECT_EQ(200, net->getValid(Network::Inheritance::ALL).getMin());
+ EXPECT_EQ(200, net->getValid(Network::Inheritance::GLOBAL).getMin());
+ EXPECT_EQ(200, net->getValid().getMax());
+ EXPECT_EQ(200, net->getValid(Network::Inheritance::ALL).getMax());
+ EXPECT_EQ(200, net->getValid(Network::Inheritance::GLOBAL).getMax());
+
+ // Set all valid lifetime global parameters.
+ globals_->set("min-valid-lifetime", Element::create(100));
+ globals_->set("max-valid-lifetime", Element::create(300));
+ EXPECT_FALSE(net->getValid().unspecified());
+ EXPECT_FALSE(net->getValid(Network::Inheritance::ALL).unspecified());
+ EXPECT_FALSE(net->getValid(Network::Inheritance::GLOBAL).unspecified());
+ EXPECT_EQ(200, net->getValid().get());
+ EXPECT_EQ(200, net->getValid(Network::Inheritance::ALL).get());
+ EXPECT_EQ(200, net->getValid(Network::Inheritance::GLOBAL).get());
+ EXPECT_EQ(100, net->getValid().getMin());
+ EXPECT_EQ(100, net->getValid(Network::Inheritance::ALL).getMin());
+ EXPECT_EQ(100, net->getValid(Network::Inheritance::GLOBAL).getMin());
+ EXPECT_EQ(300, net->getValid().getMax());
+ EXPECT_EQ(300, net->getValid(Network::Inheritance::ALL).getMax());
+ EXPECT_EQ(300, net->getValid(Network::Inheritance::GLOBAL).getMax());
+}
+
+}
diff --git a/src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc b/src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc
new file mode 100644
index 0000000..2c707f2
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc
@@ -0,0 +1,1674 @@
+// Copyright (C) 2016-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <dhcpsrv/testutils/test_utils.h>
+#include <exceptions/exceptions.h>
+#include <dhcpsrv/host.h>
+#include <dhcpsrv/pgsql_host_data_source.h>
+#include <dhcpsrv/testutils/generic_host_data_source_unittest.h>
+#include <dhcpsrv/testutils/host_data_source_utils.h>
+#include <dhcpsrv/host_mgr.h>
+#include <dhcpsrv/host_data_source_factory.h>
+#include <pgsql/pgsql_connection.h>
+#include <pgsql/testutils/pgsql_schema.h>
+#include <testutils/multi_threading_utils.h>
+#include <util/multi_threading_mgr.h>
+
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <utility>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::db;
+using namespace isc::db::test;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::data;
+using namespace isc::test;
+using namespace isc::util;
+using namespace std;
+
+namespace {
+
+class PgSqlHostDataSourceTest : public GenericHostDataSourceTest {
+public:
+ /// @brief Clears the database and opens connection to it.
+ void initializeTest() {
+ // Ensure we have the proper schema with no transient data.
+ createPgSQLSchema();
+
+ // Connect to the database
+ try {
+ HostMgr::create();
+ HostMgr::addBackend(validPgSQLConnectionString());
+ } catch (...) {
+ std::cerr << "*** ERROR: unable to open database. The test\n"
+ "*** environment is broken and must be fixed before\n"
+ "*** the PostgreSQL tests will run correctly.\n"
+ "*** The reason for the problem is described in the\n"
+ "*** accompanying exception output.\n";
+ throw;
+ }
+
+ hdsptr_ = HostMgr::instance().getHostDataSource();
+ hdsptr_->setIPReservationsUnique(true);
+
+ MultiThreadingMgr::instance().setMode(false);
+ }
+
+ /// @brief Destroys the HDS and the schema.
+ void destroyTest() {
+ try {
+ hdsptr_->rollback();
+ } catch (...) {
+ // Rollback may fail if backend is in read only mode. That's ok.
+ }
+ HostMgr::delAllBackends();
+ hdsptr_.reset();
+ // If data wipe enabled, delete transient data otherwise destroy the schema
+ destroyPgSQLSchema();
+ }
+
+ /// @brief Constructor
+ ///
+ /// Deletes everything from the database and opens it.
+ PgSqlHostDataSourceTest() {
+ initializeTest();
+ }
+
+ /// @brief Destructor
+ ///
+ /// Rolls back all pending transactions. The deletion of hdsptr_ will close
+ /// the database. Then reopen it and delete everything created by the test.
+ virtual ~PgSqlHostDataSourceTest() {
+ destroyTest();
+ }
+
+ /// @brief Reopen the database
+ ///
+ /// Closes the database and re-open it. Anything committed should be
+ /// visible.
+ ///
+ /// Parameter is ignored for PostgreSQL backend as the v4 and v6 hosts share
+ /// the same database.
+ void reopen(Universe) {
+ HostMgr::create();
+ HostMgr::addBackend(validPgSQLConnectionString());
+ hdsptr_ = HostMgr::instance().getHostDataSource();
+ }
+
+ /// @brief returns number of rows in a table
+ ///
+ /// Note: This method uses its own connection. It will not work if your test
+ /// uses transactions.
+ ///
+ /// @param name of the table
+ /// @return number of rows currently present in the table
+ int countRowsInTable(const std::string& table) {
+ string query = "SELECT * FROM " + table;
+
+ DatabaseConnection::ParameterMap params;
+ params["name"] = "keatest";
+ params["user"] = "keatest";
+ params["password"] = "keatest";
+
+ PgSqlConnection conn(params);
+ conn.openDatabase();
+
+ PgSqlResult r(PQexec(conn, query.c_str()));
+ if (PQresultStatus(r) != PGRES_TUPLES_OK) {
+ isc_throw(DbOperationError, "Query failed: " << PQerrorMessage(conn));
+ }
+
+ int numrows = PQntuples(r);
+
+ return (numrows);
+ }
+
+ /// @brief Returns number of IPv4 options currently stored in DB.
+ virtual int countDBOptions4() {
+ return (countRowsInTable("dhcp4_options"));
+ }
+
+ /// @brief Returns number of IPv6 options currently stored in DB.
+ virtual int countDBOptions6() {
+ return (countRowsInTable("dhcp6_options"));
+ }
+
+ /// @brief Returns number of IPv6 reservations currently stored in DB.
+ virtual int countDBReservations6() {
+ return (countRowsInTable("ipv6_reservations"));
+ }
+
+};
+
+/// @brief Check that database can be opened
+///
+/// This test checks if the PgSqlHostDataSource can be instantiated. This happens
+/// only if the database can be opened. Note that this is not part of the
+/// PgSqlHostMgr test fixture set. This test checks that the database can be
+/// opened: the fixtures assume that and check basic operations.
+TEST(PgSqlHostDataSource, OpenDatabase) {
+ // Schema needs to be created for the test to work.
+ destroyPgSQLSchema();
+ createPgSQLSchema();
+
+ // Check that host manager opens the database correctly and tidy up. If it
+ // fails, print the error message.
+ try {
+ HostMgr::create();
+ EXPECT_NO_THROW(HostMgr::addBackend(validPgSQLConnectionString()));
+ HostMgr::delBackend("postgresql");
+ } catch (const isc::Exception& ex) {
+ FAIL() << "*** ERROR: unable to open database, reason:\n"
+ << " " << ex.what() << "\n"
+ << "*** The test environment is broken and must be fixed\n"
+ << "*** before the PostgreSQL tests will run correctly.\n";
+ }
+
+ // Check that host manager opens the database correctly with a longer
+ // timeout. If it fails, print the error message.
+ try {
+ string connection_string = validPgSQLConnectionString() + string(" ") +
+ string(VALID_TIMEOUT);
+ HostMgr::create();
+ EXPECT_NO_THROW(HostMgr::addBackend(connection_string));
+ HostMgr::delBackend("postgresql");
+ } catch (const isc::Exception& ex) {
+ FAIL() << "*** ERROR: unable to open database, reason:\n"
+ << " " << ex.what() << "\n"
+ << "*** The test environment is broken and must be fixed\n"
+ << "*** before the PostgreSQL tests will run correctly.\n";
+ }
+
+ // Check that attempting to get an instance of the host data source when
+ // none is set returns empty pointer.
+ EXPECT_FALSE(HostMgr::instance().getHostDataSource());
+
+ // Check that wrong specification of backend throws an exception.
+ // (This is really a check on HostDataSourceFactory, but is convenient to
+ // perform here.)
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ NULL, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ InvalidParameter);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ INVALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)),
+ InvalidType);
+
+ // Check that invalid login data causes an exception.
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, INVALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, INVALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, INVALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD, INVALID_TIMEOUT_1)),
+ DbInvalidTimeout);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD, INVALID_TIMEOUT_2)),
+ DbInvalidTimeout);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD,
+ VALID_TIMEOUT, INVALID_READONLY_DB)), DbInvalidReadOnly);
+
+ // Check for missing parameters
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, NULL, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ NoDatabaseName);
+
+ // Check for SSL/TLS support.
+#ifdef HAVE_PGSQL_SSL
+ EXPECT_NO_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD,
+ 0, 0, 0, 0, VALID_CA)));
+#else
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD,
+ 0, 0, 0, 0, VALID_CA)), DbOpenError);
+#endif
+
+ // Tidy up after the test
+ destroyPgSQLSchema();
+}
+
+/// @brief Check that database can be opened with Multi-Threading
+///
+/// This test checks if the PgSqlHostDataSource can be instantiated. This happens
+/// only if the database can be opened. Note that this is not part of the
+/// PgSqlHostMgr test fixture set. This test checks that the database can be
+/// opened: the fixtures assume that and check basic operations.
+TEST(PgSqlHostDataSource, OpenDatabaseMultiThreading) {
+ // Enable Multi-Threading.
+ MultiThreadingTest mt(true);
+
+ // Schema needs to be created for the test to work.
+ destroyPgSQLSchema();
+ createPgSQLSchema();
+
+ // Check that host manager opens the database correctly and tidy up. If it
+ // fails, print the error message.
+ try {
+ HostMgr::create();
+ EXPECT_NO_THROW(HostMgr::addBackend(validPgSQLConnectionString()));
+ HostMgr::delBackend("postgresql");
+ } catch (const isc::Exception& ex) {
+ FAIL() << "*** ERROR: unable to open database, reason:\n"
+ << " " << ex.what() << "\n"
+ << "*** The test environment is broken and must be fixed\n"
+ << "*** before the PostgreSQL tests will run correctly.\n";
+ }
+
+ // Check that host manager opens the database correctly with a longer
+ // timeout. If it fails, print the error message.
+ try {
+ string connection_string = validPgSQLConnectionString() + string(" ") +
+ string(VALID_TIMEOUT);
+ HostMgr::create();
+ EXPECT_NO_THROW(HostMgr::addBackend(connection_string));
+ HostMgr::delBackend("postgresql");
+ } catch (const isc::Exception& ex) {
+ FAIL() << "*** ERROR: unable to open database, reason:\n"
+ << " " << ex.what() << "\n"
+ << "*** The test environment is broken and must be fixed\n"
+ << "*** before the PostgreSQL tests will run correctly.\n";
+ }
+
+ // Check that attempting to get an instance of the host data source when
+ // none is set returns empty pointer.
+ EXPECT_FALSE(HostMgr::instance().getHostDataSource());
+
+ // Check that wrong specification of backend throws an exception.
+ // (This is really a check on HostDataSourceFactory, but is convenient to
+ // perform here.)
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ NULL, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ InvalidParameter);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ INVALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)),
+ InvalidType);
+
+ // Check that invalid login data causes an exception.
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, INVALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, INVALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, INVALID_PASSWORD)),
+ DbOpenError);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD, INVALID_TIMEOUT_1)),
+ DbInvalidTimeout);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD, INVALID_TIMEOUT_2)),
+ DbInvalidTimeout);
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD,
+ VALID_TIMEOUT, INVALID_READONLY_DB)), DbInvalidReadOnly);
+
+ // Check for missing parameters
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, NULL, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ NoDatabaseName);
+
+ // Tidy up after the test
+ destroyPgSQLSchema();
+}
+
+/// @brief Flag used to detect calls to db_lost_callback function
+bool callback_called = false;
+
+/// @brief Callback function used in open database testing
+bool db_lost_callback(ReconnectCtlPtr /* db_conn_retry */) {
+ return (callback_called = true);
+}
+
+/// @brief Make sure open failures do NOT invoke db lost callback
+/// The db lost callback should only be invoked after successfully
+/// opening the DB and then subsequently losing it. Failing to
+/// open should be handled directly by the application layer.
+/// There is simply no good way to break the connection in a
+/// unit test environment. So testing the callback invocation
+/// in a unit test is next to impossible. That has to be done
+/// as a system test.
+TEST(PgSqlHostDataSource, NoCallbackOnOpenFail) {
+ // Schema needs to be created for the test to work.
+ destroyPgSQLSchema();
+ createPgSQLSchema();
+
+ callback_called = false;
+ DatabaseConnection::db_lost_callback_ = db_lost_callback;
+ HostMgr::create();
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, INVALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+
+ EXPECT_FALSE(callback_called);
+ destroyPgSQLSchema();
+}
+
+/// @brief Make sure open failures do NOT invoke db lost callback
+/// The db lost callback should only be invoked after successfully
+/// opening the DB and then subsequently losing it. Failing to
+/// open should be handled directly by the application layer.
+/// There is simply no good way to break the connection in a
+/// unit test environment. So testing the callback invocation
+/// in a unit test is next to impossible. That has to be done
+/// as a system test.
+TEST(PgSqlHostDataSource, NoCallbackOnOpenFailMultiThreading) {
+ // Enable Multi-Threading.
+ MultiThreadingTest mt(true);
+
+ // Schema needs to be created for the test to work.
+ destroyPgSQLSchema();
+ createPgSQLSchema();
+
+ callback_called = false;
+ DatabaseConnection::db_lost_callback_ = db_lost_callback;
+ HostMgr::create();
+ EXPECT_THROW(HostMgr::addBackend(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, INVALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+
+ EXPECT_FALSE(callback_called);
+ destroyPgSQLSchema();
+}
+
+/// @brief This test verifies that database backend can operate in Read-Only mode.
+TEST_F(PgSqlHostDataSourceTest, testReadOnlyDatabase) {
+ testReadOnlyDatabase(PGSQL_VALID_TYPE);
+}
+
+/// @brief This test verifies that database backend can operate in Read-Only mode.
+TEST_F(PgSqlHostDataSourceTest, testReadOnlyDatabaseMultiThreading) {
+ MultiThreadingTest mt(true);
+ testReadOnlyDatabase(PGSQL_VALID_TYPE);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by IPv4
+/// address. Host uses hw address as identifier.
+TEST_F(PgSqlHostDataSourceTest, basic4HWAddr) {
+ testBasic4(Host::IDENT_HWADDR);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by IPv4
+/// address. Host uses hw address as identifier.
+TEST_F(PgSqlHostDataSourceTest, basic4HWAddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testBasic4(Host::IDENT_HWADDR);
+}
+
+/// @brief Verifies that IPv4 host reservation with options can have the global
+/// subnet id value
+TEST_F(PgSqlHostDataSourceTest, globalSubnetId4) {
+ testGlobalSubnetId4();
+}
+
+/// @brief Verifies that IPv4 host reservation with options can have the global
+/// subnet id value
+TEST_F(PgSqlHostDataSourceTest, globalSubnetId4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGlobalSubnetId4();
+}
+
+/// @brief Verifies that IPv6 host reservation with options can have the global
+/// subnet id value
+TEST_F(PgSqlHostDataSourceTest, globalSubnetId6) {
+ testGlobalSubnetId6();
+}
+
+/// @brief Verifies that IPv6 host reservation with options can have the global
+/// subnet id value
+TEST_F(PgSqlHostDataSourceTest, globalSubnetId6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGlobalSubnetId6();
+}
+
+/// @brief Verifies that IPv4 host reservation with options can have a max value
+/// for dhcp4_subnet id
+TEST_F(PgSqlHostDataSourceTest, maxSubnetId4) {
+ testMaxSubnetId4();
+}
+
+/// @brief Verifies that IPv4 host reservation with options can have a max value
+/// for dhcp4_subnet id
+TEST_F(PgSqlHostDataSourceTest, maxSubnetId4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMaxSubnetId4();
+}
+
+/// @brief Verifies that IPv6 host reservation with options can have a max value
+/// for dhcp6_subnet id
+TEST_F(PgSqlHostDataSourceTest, maxSubnetId6) {
+ testMaxSubnetId6();
+}
+
+/// @brief Verifies that IPv6 host reservation with options can have a max value
+/// for dhcp6_subnet id
+TEST_F(PgSqlHostDataSourceTest, maxSubnetId6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMaxSubnetId6();
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+TEST_F(PgSqlHostDataSourceTest, getAll4BySubnet) {
+ testGetAll4();
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+TEST_F(PgSqlHostDataSourceTest, getAll4BySubnetMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetAll4();
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+TEST_F(PgSqlHostDataSourceTest, getAll6BySubnet) {
+ testGetAll6();
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+TEST_F(PgSqlHostDataSourceTest, getAll6BySubnetMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetAll6();
+}
+
+/// @brief Verifies that host reservations with the same hostname can be retrieved
+TEST_F(PgSqlHostDataSourceTest, getAllbyHostname) {
+ testGetAllbyHostname();
+}
+
+/// @brief Verifies that host reservations with the same hostname can be retrieved
+TEST_F(PgSqlHostDataSourceTest, getAllbyHostnameMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetAllbyHostname();
+}
+
+/// @brief Verifies that IPv4 host reservations with the same hostname and in
+/// the same subnet can be retrieved
+TEST_F(PgSqlHostDataSourceTest, getAllbyHostnameSubnet4) {
+ testGetAllbyHostnameSubnet4();
+}
+
+/// @brief Verifies that IPv4 host reservations with the same hostname and in
+/// the same subnet can be retrieved
+TEST_F(PgSqlHostDataSourceTest, getAllbyHostnameSubnet4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetAllbyHostnameSubnet4();
+}
+
+/// @brief Verifies that IPv6 host reservations with the same hostname and in
+/// the same subnet can be retrieved
+TEST_F(PgSqlHostDataSourceTest, getAllbyHostnameSubnet6) {
+ testGetAllbyHostnameSubnet6();
+}
+
+/// @brief Verifies that IPv6 host reservations with the same hostname and in
+/// the same subnet can be retrieved
+TEST_F(PgSqlHostDataSourceTest, getAllbyHostnameSubnet6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetAllbyHostnameSubnet6();
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+/// by pages.
+TEST_F(PgSqlHostDataSourceTest, getPage4) {
+ testGetPage4();
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+/// by pages.
+TEST_F(PgSqlHostDataSourceTest, getPage4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPage4();
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+/// by pages.
+TEST_F(PgSqlHostDataSourceTest, getPage6) {
+ testGetPage6();
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+/// by pages.
+TEST_F(PgSqlHostDataSourceTest, getPage6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPage6();
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+/// by pages without truncation from the limit.
+TEST_F(PgSqlHostDataSourceTest, getPageLimit4) {
+ testGetPageLimit4(Host::IDENT_DUID);
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+/// by pages without truncation from the limit.
+TEST_F(PgSqlHostDataSourceTest, getPageLimit4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPageLimit4(Host::IDENT_DUID);
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+/// by pages without truncation from the limit.
+TEST_F(PgSqlHostDataSourceTest, getPageLimit6) {
+ testGetPageLimit6(Host::IDENT_HWADDR);
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+/// by pages without truncation from the limit.
+TEST_F(PgSqlHostDataSourceTest, getPageLimit6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPageLimit6(Host::IDENT_HWADDR);
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+/// by pages even with multiple subnets.
+TEST_F(PgSqlHostDataSourceTest, getPage4Subnets) {
+ testGetPage4Subnets();
+}
+
+/// @brief Verifies that IPv4 host reservations in the same subnet can be retrieved
+/// by pages even with multiple subnets.
+TEST_F(PgSqlHostDataSourceTest, getPage4SubnetsMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPage4Subnets();
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+/// by pages even with multiple subnets.
+TEST_F(PgSqlHostDataSourceTest, getPage6Subnets) {
+ testGetPage6Subnets();
+}
+
+/// @brief Verifies that IPv6 host reservations in the same subnet can be retrieved
+/// by pages even with multiple subnets.
+TEST_F(PgSqlHostDataSourceTest, getPage6SubnetsMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPage6Subnets();
+}
+
+// Verifies that all IPv4 host reservations can be retrieved by pages.
+TEST_F(PgSqlHostDataSourceTest, getPage4All) {
+ testGetPage4All();
+}
+
+// Verifies that all IPv4 host reservations can be retrieved by pages.
+TEST_F(PgSqlHostDataSourceTest, getPage4AllMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPage4All();
+}
+
+// Verifies that all IPv6 host reservations can be retrieved by pages.
+TEST_F(PgSqlHostDataSourceTest, getPage6All) {
+ testGetPage6All();
+}
+
+// Verifies that all IPv6 host reservations can be retrieved by pages.
+TEST_F(PgSqlHostDataSourceTest, getPage6AllMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetPage6All();
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by IPv4
+/// address. Host uses client-id (DUID) as identifier.
+TEST_F(PgSqlHostDataSourceTest, basic4ClientId) {
+ testBasic4(Host::IDENT_DUID);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by IPv4
+/// address. Host uses client-id (DUID) as identifier.
+TEST_F(PgSqlHostDataSourceTest, basic4ClientIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testBasic4(Host::IDENT_DUID);
+}
+
+/// @brief Test verifies that multiple hosts can be added and later retrieved by their
+/// reserved IPv4 address. This test uses HW addresses as identifiers.
+TEST_F(PgSqlHostDataSourceTest, getByIPv4HWaddr) {
+ testGetByIPv4(Host::IDENT_HWADDR);
+}
+
+/// @brief Test verifies that multiple hosts can be added and later retrieved by their
+/// reserved IPv4 address. This test uses HW addresses as identifiers.
+TEST_F(PgSqlHostDataSourceTest, getByIPv4HWaddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetByIPv4(Host::IDENT_HWADDR);
+}
+
+/// @brief Test verifies that multiple hosts can be added and later retrieved by their
+/// reserved IPv4 address. This test uses client-id (DUID) as identifiers.
+TEST_F(PgSqlHostDataSourceTest, getByIPv4ClientId) {
+ testGetByIPv4(Host::IDENT_DUID);
+}
+
+/// @brief Test verifies that multiple hosts can be added and later retrieved by their
+/// reserved IPv4 address. This test uses client-id (DUID) as identifiers.
+TEST_F(PgSqlHostDataSourceTest, getByIPv4ClientIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetByIPv4(Host::IDENT_DUID);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// hardware address.
+TEST_F(PgSqlHostDataSourceTest, get4ByHWaddr) {
+ testGet4ByIdentifier(Host::IDENT_HWADDR);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// hardware address.
+TEST_F(PgSqlHostDataSourceTest, get4ByHWaddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGet4ByIdentifier(Host::IDENT_HWADDR);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// DUID.
+TEST_F(PgSqlHostDataSourceTest, get4ByDUID) {
+ testGet4ByIdentifier(Host::IDENT_DUID);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// DUID.
+TEST_F(PgSqlHostDataSourceTest, get4ByDUIDMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGet4ByIdentifier(Host::IDENT_DUID);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// circuit id.
+TEST_F(PgSqlHostDataSourceTest, get4ByCircuitId) {
+ testGet4ByIdentifier(Host::IDENT_CIRCUIT_ID);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// circuit id.
+TEST_F(PgSqlHostDataSourceTest, get4ByCircuitIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGet4ByIdentifier(Host::IDENT_CIRCUIT_ID);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// client-id.
+TEST_F(PgSqlHostDataSourceTest, get4ByClientId) {
+ testGet4ByIdentifier(Host::IDENT_CLIENT_ID);
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// client-id.
+TEST_F(PgSqlHostDataSourceTest, get4ByClientIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGet4ByIdentifier(Host::IDENT_CLIENT_ID);
+}
+
+/// @brief Test verifies if hardware address and client identifier are not confused.
+TEST_F(PgSqlHostDataSourceTest, hwaddrNotClientId1) {
+ testHWAddrNotClientId();
+}
+
+/// @brief Test verifies if hardware address and client identifier are not confused.
+TEST_F(PgSqlHostDataSourceTest, hwaddrNotClientId1MultiThreading) {
+ MultiThreadingTest mt(true);
+ testHWAddrNotClientId();
+}
+
+/// @brief Test verifies if hardware address and client identifier are not confused.
+TEST_F(PgSqlHostDataSourceTest, hwaddrNotClientId2) {
+ testClientIdNotHWAddr();
+}
+
+/// @brief Test verifies if hardware address and client identifier are not confused.
+TEST_F(PgSqlHostDataSourceTest, hwaddrNotClientId2MultiThreading) {
+ MultiThreadingTest mt(true);
+ testClientIdNotHWAddr();
+}
+
+/// @brief Test verifies if a host with FQDN hostname can be stored and later retrieved.
+TEST_F(PgSqlHostDataSourceTest, hostnameFQDN) {
+ testHostname("foo.example.org", 1);
+}
+
+/// @brief Test verifies if a host with FQDN hostname can be stored and later retrieved.
+TEST_F(PgSqlHostDataSourceTest, hostnameFQDNMultiThreading) {
+ MultiThreadingTest mt(true);
+ testHostname("foo.example.org", 1);
+}
+
+/// @brief Test verifies if 100 hosts with unique FQDN hostnames can be stored and later
+/// retrieved.
+TEST_F(PgSqlHostDataSourceTest, hostnameFQDN100) {
+ testHostname("foo.example.org", 100);
+}
+
+/// @brief Test verifies if 100 hosts with unique FQDN hostnames can be stored and later
+/// retrieved.
+TEST_F(PgSqlHostDataSourceTest, hostnameFQDN100MultiThreading) {
+ MultiThreadingTest mt(true);
+ testHostname("foo.example.org", 100);
+}
+
+/// @brief Test verifies if a host without any hostname specified can be stored and later
+/// retrieved.
+TEST_F(PgSqlHostDataSourceTest, noHostname) {
+ testHostname("", 1);
+}
+
+/// @brief Test verifies if a host without any hostname specified can be stored and later
+/// retrieved.
+TEST_F(PgSqlHostDataSourceTest, noHostnameMultiThreading) {
+ MultiThreadingTest mt(true);
+ testHostname("", 1);
+}
+
+/// @brief Test verifies if a host with user context can be stored and later retrieved.
+TEST_F(PgSqlHostDataSourceTest, usercontext) {
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testUserContext(Element::fromJSON(comment));
+}
+
+/// @brief Test verifies if a host with user context can be stored and later retrieved.
+TEST_F(PgSqlHostDataSourceTest, usercontextMultiThreading) {
+ MultiThreadingTest mt(true);
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testUserContext(Element::fromJSON(comment));
+}
+
+/// @brief Test verifies if the hardware or client-id query can match hardware address.
+TEST_F(PgSqlHostDataSourceTest, DISABLED_hwaddrOrClientId1) {
+ /// @todo: The logic behind ::get4(subnet_id, hwaddr, duid) call needs to
+ /// be discussed.
+ ///
+ /// @todo: Add host reservation with hardware address X, try to retrieve
+ /// host for hardware address X or client identifier Y, verify that the
+ /// reservation is returned.
+}
+
+/// @brief Test verifies if the hardware or client-id query can match hardware address.
+TEST_F(PgSqlHostDataSourceTest, DISABLED_hwaddrOrClientId1MultiThreading) {
+ MultiThreadingTest mt(true);
+ /// @todo: The logic behind ::get4(subnet_id, hwaddr, duid) call needs to
+ /// be discussed.
+ ///
+ /// @todo: Add host reservation with hardware address X, try to retrieve
+ /// host for hardware address X or client identifier Y, verify that the
+ /// reservation is returned.
+}
+
+/// @brief Test verifies if the hardware or client-id query can match client-id.
+TEST_F(PgSqlHostDataSourceTest, DISABLED_hwaddrOrClientId2) {
+ /// @todo: The logic behind ::get4(subnet_id, hwaddr, duid) call needs to
+ /// be discussed.
+ ///
+ /// @todo: Add host reservation with client identifier Y, try to retrieve
+ /// host for hardware address X or client identifier Y, verify that the
+ /// reservation is returned.
+}
+
+/// @brief Test verifies if the hardware or client-id query can match client-id.
+TEST_F(PgSqlHostDataSourceTest, DISABLED_hwaddrOrClientId2MultiThreading) {
+ MultiThreadingTest mt(true);
+ /// @todo: The logic behind ::get4(subnet_id, hwaddr, duid) call needs to
+ /// be discussed.
+ ///
+ /// @todo: Add host reservation with client identifier Y, try to retrieve
+ /// host for hardware address X or client identifier Y, verify that the
+ /// reservation is returned.
+}
+
+/// @brief Test verifies that host with IPv6 address and DUID can be added and
+/// later retrieved by IPv6 address.
+TEST_F(PgSqlHostDataSourceTest, get6AddrWithDuid) {
+ testGetByIPv6(Host::IDENT_DUID, false);
+}
+
+/// @brief Test verifies that host with IPv6 address and DUID can be added and
+/// later retrieved by IPv6 address.
+TEST_F(PgSqlHostDataSourceTest, get6AddrWithDuidMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetByIPv6(Host::IDENT_DUID, false);
+}
+
+/// @brief Test verifies that host with IPv6 address and HWAddr can be added and
+/// later retrieved by IPv6 address.
+TEST_F(PgSqlHostDataSourceTest, get6AddrWithHWAddr) {
+ testGetByIPv6(Host::IDENT_HWADDR, false);
+}
+
+/// @brief Test verifies that host with IPv6 address and HWAddr can be added and
+/// later retrieved by IPv6 address.
+TEST_F(PgSqlHostDataSourceTest, get6AddrWithHWAddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetByIPv6(Host::IDENT_HWADDR, false);
+}
+
+/// @brief Test verifies that host with IPv6 prefix and DUID can be added and
+/// later retrieved by IPv6 prefix.
+TEST_F(PgSqlHostDataSourceTest, get6PrefixWithDuid) {
+ testGetByIPv6(Host::IDENT_DUID, true);
+}
+
+/// @brief Test verifies that host with IPv6 prefix and DUID can be added and
+/// later retrieved by IPv6 prefix.
+TEST_F(PgSqlHostDataSourceTest, get6PrefixWithDuidMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetByIPv6(Host::IDENT_DUID, true);
+}
+
+/// @brief Test verifies that host with IPv6 prefix and HWAddr can be added and
+/// later retrieved by IPv6 prefix.
+TEST_F(PgSqlHostDataSourceTest, get6PrefixWithHWaddr) {
+ testGetByIPv6(Host::IDENT_HWADDR, true);
+}
+
+/// @brief Test verifies that host with IPv6 prefix and HWAddr can be added and
+/// later retrieved by IPv6 prefix.
+TEST_F(PgSqlHostDataSourceTest, get6PrefixWithHWaddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetByIPv6(Host::IDENT_HWADDR, true);
+}
+
+/// @brief Test verifies that host with IPv6 prefix reservation can be retrieved
+/// by subnet id and prefix value.
+TEST_F(PgSqlHostDataSourceTest, get6SubnetPrefix) {
+ testGetBySubnetIPv6();
+}
+
+/// @brief Test verifies that host with IPv6 prefix reservation can be retrieved
+/// by subnet id and prefix value.
+TEST_F(PgSqlHostDataSourceTest, get6SubnetPrefixMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetBySubnetIPv6();
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// hardware address.
+TEST_F(PgSqlHostDataSourceTest, get6ByHWaddr) {
+ testGet6ByHWAddr();
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// hardware address.
+TEST_F(PgSqlHostDataSourceTest, get6ByHWaddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGet6ByHWAddr();
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// client identifier.
+TEST_F(PgSqlHostDataSourceTest, get6ByClientId) {
+ testGet6ByClientId();
+}
+
+/// @brief Test verifies if a host reservation can be added and later retrieved by
+/// client identifier.
+TEST_F(PgSqlHostDataSourceTest, get6ByClientIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGet6ByClientId();
+}
+
+/// @brief Test verifies if a host reservation can be stored with both IPv6 address and
+/// prefix.
+TEST_F(PgSqlHostDataSourceTest, addr6AndPrefix) {
+ testAddr6AndPrefix();
+}
+
+/// @brief Test verifies if a host reservation can be stored with both IPv6 address and
+/// prefix.
+TEST_F(PgSqlHostDataSourceTest, addr6AndPrefixMultiThreading) {
+ MultiThreadingTest mt(true);
+ testAddr6AndPrefix();
+}
+
+/// @brief Tests if host with multiple IPv6 reservations can be added and then
+/// retrieved correctly. Test checks reservations comparing.
+TEST_F(PgSqlHostDataSourceTest, multipleReservations) {
+ testMultipleReservations();
+}
+
+/// @brief Tests if host with multiple IPv6 reservations can be added and then
+/// retrieved correctly. Test checks reservations comparing.
+TEST_F(PgSqlHostDataSourceTest, multipleReservationsMultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleReservations();
+}
+
+/// @brief Tests if compareIPv6Reservations() method treats same pool of reservations
+/// but added in different order as equal.
+TEST_F(PgSqlHostDataSourceTest, multipleReservationsDifferentOrder) {
+ testMultipleReservationsDifferentOrder();
+}
+
+/// @brief Tests if compareIPv6Reservations() method treats same pool of reservations
+/// but added in different order as equal.
+TEST_F(PgSqlHostDataSourceTest, multipleReservationsDifferentOrderMultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleReservationsDifferentOrder();
+}
+
+/// @brief Test that multiple client classes for IPv4 can be inserted and
+/// retrieved for a given host reservation.
+TEST_F(PgSqlHostDataSourceTest, multipleClientClasses4) {
+ testMultipleClientClasses4();
+}
+
+/// @brief Test that multiple client classes for IPv4 can be inserted and
+/// retrieved for a given host reservation.
+TEST_F(PgSqlHostDataSourceTest, multipleClientClasses4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleClientClasses4();
+}
+
+/// @brief Test that multiple client classes for IPv6 can be inserted and
+/// retrieved for a given host reservation.
+TEST_F(PgSqlHostDataSourceTest, multipleClientClasses6) {
+ testMultipleClientClasses6();
+}
+
+/// @brief Test that multiple client classes for IPv6 can be inserted and
+/// retrieved for a given host reservation.
+TEST_F(PgSqlHostDataSourceTest, multipleClientClasses6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleClientClasses6();
+}
+
+/// @brief Test that multiple client classes for both IPv4 and IPv6 can
+/// be inserted and retrieved for a given host reservation.
+TEST_F(PgSqlHostDataSourceTest, multipleClientClassesBoth) {
+ testMultipleClientClassesBoth();
+}
+
+/// @brief Test that multiple client classes for both IPv4 and IPv6 can
+/// be inserted and retrieved for a given host reservation.
+TEST_F(PgSqlHostDataSourceTest, multipleClientClassesBothMultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleClientClassesBoth();
+}
+
+/// @brief Test if the same host can have reservations in different subnets (with the
+/// same hardware address). The test logic is as follows:
+/// Insert 10 host reservations for a given physical host (the same
+/// hardware address), but for different subnets (different subnet-ids).
+/// Make sure that getAll() returns them all correctly.
+TEST_F(PgSqlHostDataSourceTest, multipleSubnetsHWAddr) {
+ testMultipleSubnets(10, Host::IDENT_HWADDR);
+}
+
+/// @brief Test if the same host can have reservations in different subnets (with the
+/// same hardware address). The test logic is as follows:
+/// Insert 10 host reservations for a given physical host (the same
+/// hardware address), but for different subnets (different subnet-ids).
+/// Make sure that getAll() returns them all correctly.
+TEST_F(PgSqlHostDataSourceTest, multipleSubnetsHWAddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleSubnets(10, Host::IDENT_HWADDR);
+}
+
+/// @brief Test if the same host can have reservations in different subnets (with the
+/// same client identifier). The test logic is as follows:
+///
+/// Insert 10 host reservations for a given physical host (the same
+/// client-identifier), but for different subnets (different subnet-ids).
+/// Make sure that getAll() returns them correctly.
+TEST_F(PgSqlHostDataSourceTest, multipleSubnetsClientId) {
+ testMultipleSubnets(10, Host::IDENT_DUID);
+}
+
+/// @brief Test if the same host can have reservations in different subnets (with the
+/// same client identifier). The test logic is as follows:
+///
+/// Insert 10 host reservations for a given physical host (the same
+/// client-identifier), but for different subnets (different subnet-ids).
+/// Make sure that getAll() returns them correctly.
+TEST_F(PgSqlHostDataSourceTest, multipleSubnetsClientIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleSubnets(10, Host::IDENT_DUID);
+}
+
+/// @brief Test if host reservations made for different IPv6 subnets are handled correctly.
+/// The test logic is as follows:
+///
+/// Insert 10 host reservations for different subnets. Make sure that
+/// get6(subnet-id, ...) calls return correct reservation.
+TEST_F(PgSqlHostDataSourceTest, subnetId6) {
+ testSubnetId6(10, Host::IDENT_HWADDR);
+}
+
+/// @brief Test if host reservations made for different IPv6 subnets are handled correctly.
+/// The test logic is as follows:
+///
+/// Insert 10 host reservations for different subnets. Make sure that
+/// get6(subnet-id, ...) calls return correct reservation.
+TEST_F(PgSqlHostDataSourceTest, subnetId6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testSubnetId6(10, Host::IDENT_HWADDR);
+}
+
+/// @brief Test if the duplicate host instances can't be inserted. The test logic is as
+/// follows: try to add multiple instances of the same host reservation and
+/// verify that the second and following attempts will throw exceptions.
+/// Hosts with same DUID.
+TEST_F(PgSqlHostDataSourceTest, addDuplicate6WithDUID) {
+ testAddDuplicate6WithSameDUID();
+}
+
+/// @brief Test if the duplicate host instances can't be inserted. The test logic is as
+/// follows: try to add multiple instances of the same host reservation and
+/// verify that the second and following attempts will throw exceptions.
+/// Hosts with same DUID.
+TEST_F(PgSqlHostDataSourceTest, addDuplicate6WithDUIDMultiThreading) {
+ MultiThreadingTest mt(true);
+ testAddDuplicate6WithSameDUID();
+}
+
+/// @brief Test if the duplicate host instances can't be inserted. The test logic is as
+/// follows: try to add multiple instances of the same host reservation and
+/// verify that the second and following attempts will throw exceptions.
+/// Hosts with same HWAddr.
+TEST_F(PgSqlHostDataSourceTest, addDuplicate6WithHWAddr) {
+ testAddDuplicate6WithSameHWAddr();
+}
+
+/// @brief Test if the duplicate host instances can't be inserted. The test logic is as
+/// follows: try to add multiple instances of the same host reservation and
+/// verify that the second and following attempts will throw exceptions.
+/// Hosts with same HWAddr.
+TEST_F(PgSqlHostDataSourceTest, addDuplicate6WithHWAddrMultiThreading) {
+ MultiThreadingTest mt(true);
+ testAddDuplicate6WithSameHWAddr();
+}
+
+/// @brief Test if the same IPv6 reservation can't be inserted multiple times.
+TEST_F(PgSqlHostDataSourceTest, addDuplicateIPv6) {
+ testAddDuplicateIPv6();
+}
+
+/// @brief Test if the same IPv6 reservation can't be inserted multiple times.
+TEST_F(PgSqlHostDataSourceTest, addDuplicateIPv6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testAddDuplicateIPv6();
+}
+
+/// @brief Test if the host reservation for the same IPv6 address can be inserted
+/// multiple times when allowed by the configuration and when the host identifier
+/// is different.
+TEST_F(PgSqlHostDataSourceTest, allowDuplicateIPv6) {
+ testAllowDuplicateIPv6();
+}
+
+/// @brief Test if the host reservation for the same IPv6 address can be inserted
+/// multiple times when allowed by the configuration and when the host identifier
+/// is different.
+TEST_F(PgSqlHostDataSourceTest, allowDuplicateIPv6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testAllowDuplicateIPv6();
+}
+
+/// @brief Test if the duplicate IPv4 host instances can't be inserted. The test logic is as
+/// follows: try to add multiple instances of the same host reservation and
+/// verify that the second and following attempts will throw exceptions.
+TEST_F(PgSqlHostDataSourceTest, addDuplicateIPv4) {
+ testAddDuplicateIPv4();
+}
+
+/// @brief Test if the duplicate IPv4 host instances can't be inserted. The test logic is as
+/// follows: try to add multiple instances of the same host reservation and
+/// verify that the second and following attempts will throw exceptions.
+TEST_F(PgSqlHostDataSourceTest, addDuplicateIPv4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testAddDuplicateIPv4();
+}
+
+/// @brief Test if the host reservation for the same IPv4 address can be inserted
+/// multiple times when allowed by the configuration and when the host identifier
+/// is different.
+TEST_F(PgSqlHostDataSourceTest, allowDuplicateIPv4) {
+ testAllowDuplicateIPv4();
+}
+
+/// @brief Test if the host reservation for the same IPv4 address can be inserted
+/// multiple times when allowed by the configuration and when the host identifier
+/// is different.
+TEST_F(PgSqlHostDataSourceTest, allowDuplicateIPv4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testAllowDuplicateIPv4();
+}
+
+/// @brief This test verifies that DHCPv4 options can be inserted in a binary format
+/// and retrieved from the PostgreSQL host database.
+TEST_F(PgSqlHostDataSourceTest, optionsReservations4) {
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations4(false, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv4 options can be inserted in a binary format
+/// and retrieved from the PostgreSQL host database.
+TEST_F(PgSqlHostDataSourceTest, optionsReservations4MultiThreading) {
+ MultiThreadingTest mt(true);
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations4(false, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv6 options can be inserted in a binary format
+/// and retrieved from the PostgreSQL host database.
+TEST_F(PgSqlHostDataSourceTest, optionsReservations6) {
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations6(false, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv6 options can be inserted in a binary format
+/// and retrieved from the PostgreSQL host database.
+TEST_F(PgSqlHostDataSourceTest, optionsReservations6MultiThreading) {
+ MultiThreadingTest mt(true);
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations6(false, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv4 and DHCPv6 options can be inserted in a
+/// binary format and retrieved with a single query to the database.
+TEST_F(PgSqlHostDataSourceTest, optionsReservations46) {
+ testOptionsReservations46(false);
+}
+
+/// @brief This test verifies that DHCPv4 and DHCPv6 options can be inserted in a
+/// binary format and retrieved with a single query to the database.
+TEST_F(PgSqlHostDataSourceTest, optionsReservations46MultiThreading) {
+ MultiThreadingTest mt(true);
+ testOptionsReservations46(false);
+}
+
+/// @brief This test verifies that DHCPv4 options can be inserted in a textual format
+/// and retrieved from the PostgreSQL host database.
+TEST_F(PgSqlHostDataSourceTest, formattedOptionsReservations4) {
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations4(true, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv4 options can be inserted in a textual format
+/// and retrieved from the PostgreSQL host database.
+TEST_F(PgSqlHostDataSourceTest, formattedOptionsReservations4MultiThreading) {
+ MultiThreadingTest mt(true);
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations4(true, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv6 options can be inserted in a textual format
+/// and retrieved from the PostgreSQL host database.
+TEST_F(PgSqlHostDataSourceTest, formattedOptionsReservations6) {
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations6(true, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv6 options can be inserted in a textual format
+/// and retrieved from the PostgreSQL host database.
+TEST_F(PgSqlHostDataSourceTest, formattedOptionsReservations6MultiThreading) {
+ MultiThreadingTest mt(true);
+ string comment = "{ \"comment\": \"a host reservation\" }";
+ testOptionsReservations6(true, Element::fromJSON(comment));
+}
+
+/// @brief This test verifies that DHCPv4 and DHCPv6 options can be inserted in a
+/// textual format and retrieved with a single query to the database.
+TEST_F(PgSqlHostDataSourceTest, formattedOptionsReservations46) {
+ testOptionsReservations46(true);
+}
+
+/// @brief This test verifies that DHCPv4 and DHCPv6 options can be inserted in a
+/// textual format and retrieved with a single query to the database.
+TEST_F(PgSqlHostDataSourceTest, formattedOptionsReservations46MultiThreading) {
+ MultiThreadingTest mt(true);
+ testOptionsReservations46(true);
+}
+
+/// @brief This test checks transactional insertion of the host information
+/// into the database. The failure to insert host information at
+/// any stage should cause the whole transaction to be rolled back.
+TEST_F(PgSqlHostDataSourceTest, testAddRollback) {
+ // Make sure we have the pointer to the host data source.
+ ASSERT_TRUE(hdsptr_);
+
+ // To test the transaction rollback mechanism we need to cause the
+ // insertion of host information to fail at some stage. The 'hosts'
+ // table should be updated correctly but the failure should occur
+ // when inserting reservations or options. The simplest way to
+ // achieve that is to simply drop one of the tables. To do so, we
+ // connect to the database and issue a DROP query.
+ DatabaseConnection::ParameterMap params;
+ params["name"] = "keatest";
+ params["user"] = "keatest";
+ params["password"] = "keatest";
+ PgSqlConnection conn(params);
+ ASSERT_NO_THROW(conn.openDatabase());
+
+ PgSqlResult r(PQexec(conn, "DROP TABLE IF EXISTS ipv6_reservations"));
+ ASSERT_TRUE (PQresultStatus(r) == PGRES_COMMAND_OK)
+ << " drop command failed :" << PQerrorMessage(conn);
+
+ // Create a host with a reservation.
+ HostPtr host = HostDataSourceUtils::initializeHost6("2001:db8:1::1",
+ Host::IDENT_HWADDR, false, "randomKey");
+ // Let's assign some DHCPv4 subnet to the host, because we will use the
+ // DHCPv4 subnet to try to retrieve the host after failed insertion.
+ host->setIPv4SubnetID(SubnetID(4));
+
+ // There is no ipv6_reservations table, so the insertion should fail.
+ ASSERT_THROW(hdsptr_->add(host), DbOperationError);
+
+ // Even though we have created a DHCPv6 host, we can't use get6()
+ // method to retrieve the host from the database, because the
+ // query would expect that the ipv6_reservations table is present.
+ // Therefore, the query would fail. Instead, we use the get4 method
+ // which uses the same client identifier, but doesn't attempt to
+ // retrieve the data from ipv6_reservations table. The query should
+ // pass but return no host because the (insert) transaction is expected
+ // to be rolled back.
+ ConstHostPtr from_hds = hdsptr_->get4(host->getIPv4SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ EXPECT_FALSE(from_hds);
+}
+
+/// @brief This test checks transactional insertion of the host information
+/// into the database. The failure to insert host information at
+/// any stage should cause the whole transaction to be rolled back.
+TEST_F(PgSqlHostDataSourceTest, testAddRollbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ // Make sure we have the pointer to the host data source.
+ ASSERT_TRUE(hdsptr_);
+
+ // To test the transaction rollback mechanism we need to cause the
+ // insertion of host information to fail at some stage. The 'hosts'
+ // table should be updated correctly but the failure should occur
+ // when inserting reservations or options. The simplest way to
+ // achieve that is to simply drop one of the tables. To do so, we
+ // connect to the database and issue a DROP query.
+ DatabaseConnection::ParameterMap params;
+ params["name"] = "keatest";
+ params["user"] = "keatest";
+ params["password"] = "keatest";
+ PgSqlConnection conn(params);
+ ASSERT_NO_THROW(conn.openDatabase());
+
+ PgSqlResult r(PQexec(conn, "DROP TABLE IF EXISTS ipv6_reservations"));
+ ASSERT_TRUE (PQresultStatus(r) == PGRES_COMMAND_OK)
+ << " drop command failed :" << PQerrorMessage(conn);
+
+ // Create a host with a reservation.
+ HostPtr host = HostDataSourceUtils::initializeHost6("2001:db8:1::1",
+ Host::IDENT_HWADDR, false, "randomKey");
+ // Let's assign some DHCPv4 subnet to the host, because we will use the
+ // DHCPv4 subnet to try to retrieve the host after failed insertion.
+ host->setIPv4SubnetID(SubnetID(4));
+
+ // There is no ipv6_reservations table, so the insertion should fail.
+ ASSERT_THROW(hdsptr_->add(host), DbOperationError);
+
+ // Even though we have created a DHCPv6 host, we can't use get6()
+ // method to retrieve the host from the database, because the
+ // query would expect that the ipv6_reservations table is present.
+ // Therefore, the query would fail. Instead, we use the get4 method
+ // which uses the same client identifier, but doesn't attempt to
+ // retrieve the data from ipv6_reservations table. The query should
+ // pass but return no host because the (insert) transaction is expected
+ // to be rolled back.
+ ConstHostPtr from_hds = hdsptr_->get4(host->getIPv4SubnetID(),
+ host->getIdentifierType(),
+ &host->getIdentifier()[0],
+ host->getIdentifier().size());
+ EXPECT_FALSE(from_hds);
+}
+
+/// @brief This test checks that siaddr, sname, file fields can be retrieved
+/// from a database for a host.
+TEST_F(PgSqlHostDataSourceTest, messageFields) {
+ testMessageFields4();
+}
+
+/// @brief This test checks that siaddr, sname, file fields can be retrieved
+/// from a database for a host.
+TEST_F(PgSqlHostDataSourceTest, messageFieldsMultiThreading) {
+ MultiThreadingTest mt(true);
+ testMessageFields4();
+}
+
+/// @brief Check that delete(subnet-id, addr4) works.
+TEST_F(PgSqlHostDataSourceTest, deleteByAddr4) {
+ testDeleteByAddr4();
+}
+
+/// @brief Check that delete(subnet-id, addr4) works.
+TEST_F(PgSqlHostDataSourceTest, deleteByAddr4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testDeleteByAddr4();
+}
+
+/// @brief Check that delete(subnet4-id, identifier-type, identifier) works.
+TEST_F(PgSqlHostDataSourceTest, deleteById4) {
+ testDeleteById4();
+}
+
+/// @brief Check that delete(subnet4-id, identifier-type, identifier) works.
+TEST_F(PgSqlHostDataSourceTest, deleteById4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testDeleteById4();
+}
+
+/// @brief Check that delete(subnet4-id, identifier-type, identifier) works,
+/// even when options are present.
+TEST_F(PgSqlHostDataSourceTest, deleteById4Options) {
+ testDeleteById4Options();
+}
+
+/// @brief Check that delete(subnet4-id, identifier-type, identifier) works,
+/// even when options are present.
+TEST_F(PgSqlHostDataSourceTest, deleteById4OptionsMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDeleteById4Options();
+}
+
+/// @brief Check that delete(subnet6-id, identifier-type, identifier) works.
+TEST_F(PgSqlHostDataSourceTest, deleteById6) {
+ testDeleteById6();
+}
+
+/// @brief Check that delete(subnet6-id, identifier-type, identifier) works.
+TEST_F(PgSqlHostDataSourceTest, deleteById6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testDeleteById6();
+}
+
+/// @brief Check that delete(subnet6-id, identifier-type, identifier) works,
+/// even when options are present.
+TEST_F(PgSqlHostDataSourceTest, deleteById6Options) {
+ testDeleteById6Options();
+}
+
+/// @brief Check that delete(subnet6-id, identifier-type, identifier) works,
+/// even when options are present.
+TEST_F(PgSqlHostDataSourceTest, deleteById6OptionsMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDeleteById6Options();
+}
+
+/// @brief Tests that multiple reservations without IPv4 addresses can be
+/// specified within a subnet.
+TEST_F(PgSqlHostDataSourceTest, testMultipleHostsNoAddress4) {
+ testMultipleHostsNoAddress4();
+}
+
+/// @brief Tests that multiple reservations without IPv4 addresses can be
+/// specified within a subnet.
+TEST_F(PgSqlHostDataSourceTest, testMultipleHostsNoAddress4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleHostsNoAddress4();
+}
+
+/// @brief Tests that multiple hosts can be specified within an IPv6 subnet.
+TEST_F(PgSqlHostDataSourceTest, testMultipleHosts6) {
+ testMultipleHosts6();
+}
+
+/// @brief Tests that multiple hosts can be specified within an IPv6 subnet.
+TEST_F(PgSqlHostDataSourceTest, testMultipleHosts6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMultipleHosts6();
+}
+
+/// @brief Test fixture class for validating @c HostMgr using
+/// PostgreSQL as alternate host data source.
+class PgSQLHostMgrTest : public HostMgrTest {
+protected:
+
+ /// @brief Build PostgreSQL schema for a test.
+ virtual void SetUp();
+
+ /// @brief Rollback and drop PostgreSQL schema after the test.
+ virtual void TearDown();
+};
+
+void
+PgSQLHostMgrTest::SetUp() {
+ HostMgrTest::SetUp();
+
+ // Ensure we have the proper schema with no transient data.
+ db::test::createPgSQLSchema();
+
+ // Connect to the database
+ try {
+ HostMgr::addBackend(db::test::validPgSQLConnectionString());
+ } catch (...) {
+ std::cerr << "*** ERROR: unable to open database. The test\n"
+ "*** environment is broken and must be fixed before\n"
+ "*** the PostgreSQL tests will run correctly.\n"
+ "*** The reason for the problem is described in the\n"
+ "*** accompanying exception output.\n";
+ throw;
+ }
+}
+
+void
+PgSQLHostMgrTest::TearDown() {
+ try {
+ HostMgr::instance().getHostDataSource()->rollback();
+ } catch(...) {
+ // we don't care if we aren't in a transaction.
+ }
+
+ HostMgr::delBackend("postgresql");
+ // If data wipe enabled, delete transient data otherwise destroy the schema
+ db::test::destroyPgSQLSchema();
+}
+
+/// @brief Test fixture class for validating @c HostMgr using
+/// PostgreSQL as alternate host data source and PostgreSQL connectivity loss.
+class PgSQLHostMgrDbLostCallbackTest : public HostMgrDbLostCallbackTest {
+public:
+ virtual void destroySchema() {
+ // If data wipe enabled, delete transient data otherwise destroy the schema
+ db::test::destroyPgSQLSchema();
+ }
+
+ virtual void createSchema() {
+ // Ensure we have the proper schema with no transient data.
+ db::test::createPgSQLSchema();
+ }
+
+ virtual std::string validConnectString() {
+ return (db::test::validPgSQLConnectionString());
+ }
+
+ virtual std::string invalidConnectString() {
+ return (connectionString(PGSQL_VALID_TYPE, INVALID_NAME, VALID_HOST,
+ VALID_USER, VALID_PASSWORD));
+ }
+};
+
+// This test verifies that reservations for a particular client can
+// be retrieved from the configuration file and a database simultaneously.
+TEST_F(PgSQLHostMgrTest, getAll) {
+ testGetAll(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that reservations for a particular subnet can
+// be retrieved from the configuration file and a database simultaneously.
+TEST_F(PgSQLHostMgrTest, getAll4BySubnet) {
+ testGetAll4BySubnet(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that reservations for a particular subnet can
+// be retrieved from the configuration file and a database simultaneously.
+TEST_F(PgSQLHostMgrTest, getAll6BySubnet) {
+ testGetAll6BySubnet(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that HostMgr returns all reservations for the specified
+// IPv4 subnet and reserved address.
+TEST_F(PgSQLHostMgrTest, getAll4BySubnetIP) {
+ testGetAll4BySubnetIP(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that HostMgr returns all reservations for the specified
+// IPv6 subnet and reserved address.
+TEST_F(PgSQLHostMgrTest, getAll6BySubnetIP) {
+ testGetAll6BySubnetIP(*getCfgHosts(), *getCfgHosts());
+}
+
+// This test verifies that reservations for a particular hostname can be
+// retrieved from the configuration file and a database simultaneously.
+TEST_F(PgSQLHostMgrTest, getAllbyHostname) {
+ testGetAllbyHostname(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that reservations for a particular hostname and
+// DHCPv4 subnet can be retrieved from the configuration file and a
+// database simultaneously.
+TEST_F(PgSQLHostMgrTest, getAllbyHostnameSubnet4) {
+ testGetAllbyHostnameSubnet4(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that reservations for a particular hostname and
+// DHCPv6 subnet can be retrieved from the configuration file and a
+// database simultaneously.
+TEST_F(PgSQLHostMgrTest, getAllbyHostnameSubnet6) {
+ testGetAllbyHostnameSubnet6(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that reservations for a particular subnet can
+// be retrieved by pages from the configuration file and a database
+// simultaneously.
+TEST_F(PgSQLHostMgrTest, getPage4) {
+ testGetPage4(true);
+}
+
+// This test verifies that all v4 reservations be retrieved by pages
+// from the configuration file and a database simultaneously.
+TEST_F(PgSQLHostMgrTest, getPage4All) {
+ testGetPage4All(true);
+}
+
+// This test verifies that reservations for a particular subnet can
+// be retrieved by pages from the configuration file and a database
+// simultaneously.
+TEST_F(PgSQLHostMgrTest, getPage6) {
+ testGetPage6(true);
+}
+
+// This test verifies that all v6 reservations be retrieved by pages
+// from the configuration file and a database simultaneously.
+TEST_F(PgSQLHostMgrTest, getPage6All) {
+ testGetPage6All(true);
+}
+
+// This test verifies that IPv4 reservations for a particular client can
+// be retrieved from the configuration file and a database simultaneously.
+TEST_F(PgSQLHostMgrTest, getAll4) {
+ testGetAll4(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that the IPv4 reservation can be retrieved from a
+// database.
+TEST_F(PgSQLHostMgrTest, get4) {
+ testGet4(HostMgr::instance());
+}
+
+// This test verifies that the IPv6 reservation can be retrieved from a
+// database.
+TEST_F(PgSQLHostMgrTest, get6) {
+ testGet6(HostMgr::instance());
+}
+
+// This test verifies that the IPv6 prefix reservation can be retrieved
+// from a configuration file and a database.
+TEST_F(PgSQLHostMgrTest, get6ByPrefix) {
+ testGet6ByPrefix(*getCfgHosts(), HostMgr::instance());
+}
+
+// This test verifies that it is possible to control whether the reserved
+// IP addresses are unique or non unique via the HostMgr.
+TEST_F(PgSQLHostMgrTest, setIPReservationsUnique) {
+ EXPECT_TRUE(HostMgr::instance().setIPReservationsUnique(true));
+ EXPECT_TRUE(HostMgr::instance().setIPReservationsUnique(false));
+}
+
+/// @brief Verifies that db lost callback is not invoked on an open failure
+TEST_F(PgSQLHostMgrDbLostCallbackTest, testNoCallbackOnOpenFailure) {
+ MultiThreadingTest mt(false);
+ testNoCallbackOnOpenFailure();
+}
+
+/// @brief Verifies that db lost callback is not invoked on an open failure
+TEST_F(PgSQLHostMgrDbLostCallbackTest, testNoCallbackOnOpenFailureMultiThreading) {
+ MultiThreadingTest mt(true);
+ testNoCallbackOnOpenFailure();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSQLHostMgrDbLostCallbackTest, testDbLostAndRecoveredCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndRecoveredCallback();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSQLHostMgrDbLostCallbackTest, testDbLostAndRecoveredCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndRecoveredCallback();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSQLHostMgrDbLostCallbackTest, testDbLostAndFailedCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndFailedCallback();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSQLHostMgrDbLostCallbackTest, testDbLostAndFailedCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndFailedCallback();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSQLHostMgrDbLostCallbackTest, testDbLostAndRecoveredAfterTimeoutCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndRecoveredAfterTimeoutCallback();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSQLHostMgrDbLostCallbackTest, testDbLostAndRecoveredAfterTimeoutCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndRecoveredAfterTimeoutCallback();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSQLHostMgrDbLostCallbackTest, testDbLostAndFailedAfterTimeoutCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndFailedAfterTimeoutCallback();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSQLHostMgrDbLostCallbackTest, testDbLostAndFailedAfterTimeoutCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndFailedAfterTimeoutCallback();
+}
+
+} // namespace
diff --git a/src/lib/dhcpsrv/tests/pgsql_lease_mgr_unittest.cc b/src/lib/dhcpsrv/tests/pgsql_lease_mgr_unittest.cc
new file mode 100644
index 0000000..84d8d20
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/pgsql_lease_mgr_unittest.cc
@@ -0,0 +1,1160 @@
+// Copyright (C) 2014-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/pgsql_lease_mgr.h>
+#include <dhcpsrv/testutils/test_utils.h>
+#include <dhcpsrv/testutils/pgsql_generic_backend_unittest.h>
+#include <dhcpsrv/tests/generic_lease_mgr_unittest.h>
+#include <exceptions/exceptions.h>
+#include <pgsql/pgsql_connection.h>
+#include <pgsql/testutils/pgsql_schema.h>
+#include <testutils/gtest_utils.h>
+#include <testutils/multi_threading_utils.h>
+#include <util/multi_threading_mgr.h>
+
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <utility>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::db;
+using namespace isc::db::test;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::test;
+using namespace isc::util;
+using namespace std;
+
+namespace {
+
+
+/// @brief Test fixture class for testing PostgreSQL Lease Manager
+///
+/// Opens the database prior to each test and closes it afterwards.
+/// All pending transactions are deleted prior to closure.
+
+class PgSqlLeaseMgrTest : public GenericLeaseMgrTest {
+public:
+ /// @brief Clears the database and opens connection to it.
+ void initializeTest() {
+ // Ensure we have the proper schema with no transient data.
+ createPgSQLSchema();
+
+ // Connect to the database
+ try {
+ LeaseMgrFactory::create(validPgSQLConnectionString());
+ } catch (...) {
+ std::cerr << "*** ERROR: unable to open database. The test\n"
+ "*** environment is broken and must be fixed before\n"
+ "*** the PostgreSQL tests will run correctly.\n"
+ "*** The reason for the problem is described in the\n"
+ "*** accompanying exception output.\n";
+ throw;
+ }
+
+ lmptr_ = &(LeaseMgrFactory::instance());
+
+ MultiThreadingMgr::instance().setMode(false);
+ }
+
+ /// @brief Destroys the LM and the schema.
+ void destroyTest() {
+ LeaseMgrFactory::destroy();
+ // If data wipe enabled, delete transient data otherwise destroy the schema
+ destroyPgSQLSchema();
+
+ // Disable Multi-Threading.
+ MultiThreadingMgr::instance().setMode(false);
+ }
+
+ /// @brief Constructor
+ ///
+ /// Deletes everything from the database and opens it.
+ PgSqlLeaseMgrTest() {
+ initializeTest();
+ }
+
+ /// @brief Destructor
+ ///
+ /// Rolls back all pending transactions. The deletion of lmptr_ will close
+ /// the database. Then reopen it and delete everything created by the test.
+ virtual ~PgSqlLeaseMgrTest() {
+ destroyTest();
+ }
+
+ /// @brief Reopen the database
+ ///
+ /// Closes the database and re-open it. Anything committed should be
+ /// visible.
+ ///
+ /// Parameter is ignored for PostgreSQL backend as the v4 and v6 leases share
+ /// the same database.
+ void reopen(Universe) {
+ LeaseMgrFactory::destroy();
+ LeaseMgrFactory::create(validPgSQLConnectionString());
+ lmptr_ = &(LeaseMgrFactory::instance());
+ }
+};
+
+/// @brief Check that database can be opened
+///
+/// This test checks if the PgSqlLeaseMgr can be instantiated. This happens
+/// only if the database can be opened. Note that this is not part of the
+/// PgSqlLeaseMgr test fixture set. This test checks that the database can be
+/// opened: the fixtures assume that and check basic operations.
+TEST(PgSqlOpenTest, OpenDatabase) {
+ // Explicitly disable Multi-Threading.
+ MultiThreadingMgr::instance().setMode(false);
+
+ // Schema needs to be created for the test to work.
+ createPgSQLSchema();
+
+ // Check that lease manager opens the database correctly and tidy up. If it
+ // fails, print the error message.
+ try {
+ LeaseMgrFactory::create(validPgSQLConnectionString());
+ EXPECT_NO_THROW((void)LeaseMgrFactory::instance());
+ LeaseMgrFactory::destroy();
+ } catch (const isc::Exception& ex) {
+ FAIL() << "*** ERROR: unable to open database, reason:\n"
+ << " " << ex.what() << "\n"
+ << "*** The test environment is broken and must be fixed\n"
+ << "*** before the PostgreSQL tests will run correctly.\n";
+ }
+
+ // Check that lease manager opens the database correctly with a longer
+ // timeout. If it fails, print the error message.
+ try {
+ string connection_string = validPgSQLConnectionString() + string(" ") +
+ string(VALID_TIMEOUT);
+ LeaseMgrFactory::create(connection_string);
+ EXPECT_NO_THROW((void) LeaseMgrFactory::instance());
+ LeaseMgrFactory::destroy();
+ } catch (const isc::Exception& ex) {
+ FAIL() << "*** ERROR: unable to open database, reason:\n"
+ << " " << ex.what() << "\n"
+ << "*** The test environment is broken and must be fixed\n"
+ << "*** before the PostgreSQL tests will run correctly.\n";
+ }
+
+ // Check that attempting to get an instance of the lease manager when
+ // none is set throws an exception.
+ EXPECT_THROW(LeaseMgrFactory::instance(), NoLeaseManager);
+
+ // Check that wrong specification of backend throws an exception.
+ // (This is really a check on LeaseMgrFactory, but is convenient to
+ // perform here.)
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ NULL, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ InvalidParameter);
+
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ INVALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)),
+ InvalidType);
+
+ // Check that invalid login data causes an exception.
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ PGSQL_VALID_TYPE, INVALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, INVALID_HOST, VALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ DbOpenError);
+
+ // This test might fail if 'auth-method' in PostgresSQL host-based authentication
+ // file (/var/lib/pgsql/9.4/data/pg_hba.conf) is set to 'trust',
+ // which allows logging without password. 'Auth-method' should be changed to 'password'.
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, INVALID_PASSWORD)),
+ DbOpenError);
+
+ // Check for invalid timeouts
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD, INVALID_TIMEOUT_1)),
+ DbInvalidTimeout);
+
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD, INVALID_TIMEOUT_2)),
+ DbInvalidTimeout);
+
+ // Check for missing parameters
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ PGSQL_VALID_TYPE, NULL, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+ NoDatabaseName);
+
+ // Check for SSL/TLS support.
+#ifdef HAVE_PGSQL_SSL
+ EXPECT_NO_THROW(LeaseMgrFactory::create(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD,
+ 0, 0, 0, 0, VALID_CA)));
+#else
+ EXPECT_THROW(LeaseMgrFactory::create(connectionString(
+ PGSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD,
+ 0, 0, 0, 0, VALID_CA)), DbOpenError);
+#endif
+
+ // Tidy up after the test
+ destroyPgSQLSchema();
+ LeaseMgrFactory::destroy();
+}
+
+/// @brief Check that database can be opened with Multi-Threading
+TEST(PgSqlOpenTest, OpenDatabaseMultiThreading) {
+ // Enable Multi-Threading.
+ MultiThreadingTest mt(true);
+
+ // Schema needs to be created for the test to work.
+ createPgSQLSchema();
+
+ // Check that lease manager opens the database correctly and tidy up. If it
+ // fails, print the error message.
+ try {
+ LeaseMgrFactory::create(validPgSQLConnectionString());
+ EXPECT_NO_THROW((void)LeaseMgrFactory::instance());
+ LeaseMgrFactory::destroy();
+ } catch (const isc::Exception& ex) {
+ FAIL() << "*** ERROR: unable to open database, reason:\n"
+ << " " << ex.what() << "\n"
+ << "*** The test environment is broken and must be fixed\n"
+ << "*** before the PostgreSQL tests will run correctly.\n";
+ }
+
+ // Tidy up after the test
+ destroyPgSQLSchema();
+ LeaseMgrFactory::destroy();
+}
+
+/// @brief Check the getType() method
+///
+/// getType() returns a string giving the type of the backend, which should
+/// always be "postgresql".
+TEST_F(PgSqlLeaseMgrTest, getType) {
+ EXPECT_EQ(std::string("postgresql"), lmptr_->getType());
+}
+
+/// @brief Check getName() returns correct database name
+TEST_F(PgSqlLeaseMgrTest, getName) {
+ EXPECT_EQ(std::string("keatest"), lmptr_->getName());
+}
+
+/// @brief Check that getVersion() returns the expected version
+TEST_F(PgSqlLeaseMgrTest, checkVersion) {
+ // Check version
+ pair<uint32_t, uint32_t> version;
+ ASSERT_NO_THROW(version = lmptr_->getVersion());
+ EXPECT_EQ(PGSQL_SCHEMA_VERSION_MAJOR, version.first);
+ EXPECT_EQ(PGSQL_SCHEMA_VERSION_MINOR, version.second);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+/// LEASE4 /////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Basic Lease4 Checks
+///
+/// Checks that the addLease, getLease4 (by address) and deleteLease (with an
+/// IPv4 address) works.
+TEST_F(PgSqlLeaseMgrTest, basicLease4) {
+ testBasicLease4();
+}
+
+/// @brief Basic Lease4 Checks
+TEST_F(PgSqlLeaseMgrTest, basicLease4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testBasicLease4();
+}
+
+/// @brief Check that Lease4 code safely handles invalid dates.
+TEST_F(PgSqlLeaseMgrTest, maxDate4) {
+ testMaxDate4();
+}
+
+/// @brief Check that Lease4 code safely handles invalid dates.
+TEST_F(PgSqlLeaseMgrTest, maxDate4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMaxDate4();
+}
+
+/// @brief checks that infinite lifetimes do not overflow.
+TEST_F(PgSqlLeaseMgrTest, infiniteLifeTime4) {
+ testInfiniteLifeTime4();
+}
+
+/// @brief Lease4 update tests
+///
+/// Checks that we are able to update a lease in the database.
+TEST_F(PgSqlLeaseMgrTest, updateLease4) {
+ testUpdateLease4();
+}
+
+/// @brief Lease4 update tests
+TEST_F(PgSqlLeaseMgrTest, updateLease4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testUpdateLease4();
+}
+
+/// @brief Lease4 concurrent update tests
+///
+/// Checks that we are not able to concurrently update a lease in the database.
+TEST_F(PgSqlLeaseMgrTest, concurrentUpdateLease4) {
+ testConcurrentUpdateLease4();
+}
+
+/// @brief Lease4 concurrent update tests
+///
+/// Checks that we are not able to concurrently update a lease in the database.
+TEST_F(PgSqlLeaseMgrTest, concurrentUpdateLease4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testConcurrentUpdateLease4();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address
+TEST_F(PgSqlLeaseMgrTest, getLease4HWAddr1) {
+ testGetLease4HWAddr1();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address
+TEST_F(PgSqlLeaseMgrTest, getLease4HWAddr1MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4HWAddr1();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address
+TEST_F(PgSqlLeaseMgrTest, getLease4HWAddr2) {
+ testGetLease4HWAddr2();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address
+TEST_F(PgSqlLeaseMgrTest, getLease4HWAddr2MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4HWAddr2();
+}
+
+/// @brief Get lease4 by hardware address (2)
+///
+/// Check that the system can cope with getting a hardware address of
+/// any size.
+TEST_F(PgSqlLeaseMgrTest, getLease4HWAddrSize) {
+ testGetLease4HWAddrSize();
+}
+
+/// @brief Get lease4 by hardware address (2)
+TEST_F(PgSqlLeaseMgrTest, getLease4HWAddrSizeMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4HWAddrSize();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address & Subnet ID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// a combination of hardware address and subnet ID
+TEST_F(PgSqlLeaseMgrTest, getLease4HwaddrSubnetId) {
+ testGetLease4HWAddrSubnetId();
+}
+
+/// @brief Check GetLease4 methods - access by Hardware Address & Subnet ID
+TEST_F(PgSqlLeaseMgrTest, getLease4HwaddrSubnetIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4HWAddrSubnetId();
+}
+
+/// @brief Get lease4 by hardware address and subnet ID (2)
+///
+/// Check that the system can cope with getting a hardware address of
+/// any size.
+TEST_F(PgSqlLeaseMgrTest, getLease4HWAddrSubnetIdSize) {
+ testGetLease4HWAddrSubnetIdSize();
+}
+
+/// @brief Get lease4 by hardware address and subnet ID (2)
+TEST_F(PgSqlLeaseMgrTest, getLease4HWAddrSubnetIdSizeMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4HWAddrSubnetIdSize();
+}
+
+/// @brief This test was derived from memfile.
+TEST_F(PgSqlLeaseMgrTest, getLease4ClientId) {
+ testGetLease4ClientId();
+}
+
+/// @brief This test was derived from memfile.
+TEST_F(PgSqlLeaseMgrTest, getLease4ClientIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4ClientId();
+}
+
+/// @brief Check GetLease4 methods - access by Client ID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// the Client ID.
+TEST_F(PgSqlLeaseMgrTest, getLease4ClientId2) {
+ testGetLease4ClientId2();
+}
+
+/// @brief Check GetLease4 methods - access by Client ID
+TEST_F(PgSqlLeaseMgrTest, getLease4ClientId2MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4ClientId2();
+}
+
+/// @brief Get Lease4 by client ID (2)
+///
+/// Check that the system can cope with a client ID of any size.
+TEST_F(PgSqlLeaseMgrTest, getLease4ClientIdSize) {
+ testGetLease4ClientIdSize();
+}
+
+/// @brief Get Lease4 by client ID (2)
+TEST_F(PgSqlLeaseMgrTest, getLease4ClientIdSizeMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4ClientIdSize();
+}
+
+/// @brief Check GetLease4 methods - access by Client ID & Subnet ID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// a combination of client and subnet IDs.
+TEST_F(PgSqlLeaseMgrTest, getLease4ClientIdSubnetId) {
+ testGetLease4ClientIdSubnetId();
+}
+
+/// @brief Check GetLease4 methods - access by Client ID & Subnet ID
+TEST_F(PgSqlLeaseMgrTest, getLease4ClientIdSubnetIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease4ClientIdSubnetId();
+}
+
+/// @brief This test checks that all IPv4 leases for a specified subnet id are returned.
+TEST_F(PgSqlLeaseMgrTest, getLeases4SubnetId) {
+ testGetLeases4SubnetId();
+}
+
+/// @brief This test checks that all IPv4 leases for a specified subnet id are returned.
+TEST_F(PgSqlLeaseMgrTest, getLeases4SubnetIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases4SubnetId();
+}
+
+/// @brief This test checks that all IPv4 leases with a specified hostname are returned.
+TEST_F(PgSqlLeaseMgrTest, getLeases4Hostname) {
+ testGetLeases4Hostname();
+}
+
+/// @brief This test checks that all IPv4 leases with a specified hostname are returned.
+TEST_F(PgSqlLeaseMgrTest, getLeases4HostnameMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases4Hostname();
+}
+
+/// @brief This test checks that all IPv4 leases are returned.
+TEST_F(PgSqlLeaseMgrTest, getLeases4) {
+ testGetLeases4();
+}
+
+/// @brief This test checks that all IPv4 leases are returned.
+TEST_F(PgSqlLeaseMgrTest, getLeases4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases4();
+}
+
+/// @brief Test that a range of IPv4 leases is returned with paging.
+TEST_F(PgSqlLeaseMgrTest, getLeases4Paged) {
+ testGetLeases4Paged();
+}
+
+/// @brief Test that a range of IPv4 leases is returned with paging.
+TEST_F(PgSqlLeaseMgrTest, getLeases4PagedMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases4Paged();
+}
+
+/// @brief This test checks that all IPv6 leases for a specified subnet id are returned.
+TEST_F(PgSqlLeaseMgrTest, getLeases6SubnetId) {
+ testGetLeases6SubnetId();
+}
+
+/// @brief This test checks that all IPv6 leases for a specified subnet id are returned.
+TEST_F(PgSqlLeaseMgrTest, getLeases6SubnetIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases6SubnetId();
+}
+
+/// @brief This test checks that all IPv6 leases with a specified hostname are returned.
+TEST_F(PgSqlLeaseMgrTest, getLeases6Hostname) {
+ testGetLeases6Hostname();
+}
+
+/// @brief This test checks that all IPv6 leases with a specified hostname are returned.
+TEST_F(PgSqlLeaseMgrTest, getLeases6HostnameMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases6Hostname();
+}
+
+/// @brief This test checks that all IPv6 leases are returned.
+TEST_F(PgSqlLeaseMgrTest, getLeases6) {
+ testGetLeases6();
+}
+
+/// @brief This test checks that all IPv6 leases are returned.
+TEST_F(PgSqlLeaseMgrTest, getLeases6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases6();
+}
+
+/// @brief Test that a range of IPv6 leases is returned with paging.
+TEST_F(PgSqlLeaseMgrTest, getLeases6Paged) {
+ testGetLeases6Paged();
+}
+
+/// @brief Test that a range of IPv6 leases is returned with paging.
+TEST_F(PgSqlLeaseMgrTest, getLeases6PagedMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases6Paged();
+}
+
+/// @brief Basic Lease4 Checks
+///
+/// Checks that the addLease, getLease4(by address), getLease4(hwaddr,subnet_id),
+/// updateLease4() and deleteLease can handle NULL client-id.
+/// (client-id is optional and may not be present)
+TEST_F(PgSqlLeaseMgrTest, lease4NullClientId) {
+ testLease4NullClientId();
+}
+
+/// @brief Basic Lease4 Checks
+TEST_F(PgSqlLeaseMgrTest, lease4NullClientIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testLease4NullClientId();
+}
+
+/// @brief Verify that too long hostname for Lease4 is not accepted.
+///
+/// Checks that the it is not possible to create a lease when the hostname
+/// length exceeds 255 characters.
+TEST_F(PgSqlLeaseMgrTest, lease4InvalidHostname) {
+ testLease4InvalidHostname();
+}
+
+/// @brief Verify that too long hostname for Lease4 is not accepted.
+TEST_F(PgSqlLeaseMgrTest, lease4InvalidHostnameMultiThreading) {
+ MultiThreadingTest mt(true);
+ testLease4InvalidHostname();
+}
+
+/// @brief Check that the expired DHCPv4 leases can be retrieved.
+///
+/// This test adds a number of leases to the lease database and marks
+/// some of them as expired. Then it queries for expired leases and checks
+/// whether only expired leases are returned, and that they are returned in
+/// the order from most to least expired. It also checks that the lease
+/// which is marked as 'reclaimed' is not returned.
+TEST_F(PgSqlLeaseMgrTest, getExpiredLeases4) {
+ testGetExpiredLeases4();
+}
+
+/// @brief Check that the expired DHCPv4 leases can be retrieved.
+TEST_F(PgSqlLeaseMgrTest, getExpiredLeases4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetExpiredLeases4();
+}
+
+/// @brief Checks that DHCPv4 leases with infinite valid lifetime
+/// will never expire.
+TEST_F(PgSqlLeaseMgrTest, infiniteAreNotExpired4) {
+ testInfiniteAreNotExpired4();
+}
+
+/// @brief Check that expired reclaimed DHCPv4 leases are removed.
+TEST_F(PgSqlLeaseMgrTest, deleteExpiredReclaimedLeases4) {
+ testDeleteExpiredReclaimedLeases4();
+}
+
+/// @brief Check that expired reclaimed DHCPv4 leases are removed.
+TEST_F(PgSqlLeaseMgrTest, deleteExpiredReclaimedLeases4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testDeleteExpiredReclaimedLeases4();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+/// LEASE6 /////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Test checks whether simple add, get and delete operations
+/// are possible on Lease6
+TEST_F(PgSqlLeaseMgrTest, testAddGetDelete6) {
+ testAddGetDelete6();
+}
+
+/// @brief Test checks whether simple add, get and delete operations
+/// are possible on Lease6
+TEST_F(PgSqlLeaseMgrTest, testAddGetDelete6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testAddGetDelete6();
+}
+
+/// @brief Basic Lease6 Checks
+///
+/// Checks that the addLease, getLease6 (by address) and deleteLease (with an
+/// IPv6 address) works.
+TEST_F(PgSqlLeaseMgrTest, basicLease6) {
+ testBasicLease6();
+}
+
+/// @brief Basic Lease6 Checks
+TEST_F(PgSqlLeaseMgrTest, basicLease6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testBasicLease6();
+}
+
+/// @brief Check that Lease6 code safely handles invalid dates.
+TEST_F(PgSqlLeaseMgrTest, maxDate6) {
+ testMaxDate6();
+}
+
+/// @brief Check that Lease6 code safely handles invalid dates.
+TEST_F(PgSqlLeaseMgrTest, maxDate6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testMaxDate6();
+}
+
+/// @brief checks that infinite lifetimes do not overflow.
+TEST_F(PgSqlLeaseMgrTest, infiniteLifeTime6) {
+ testInfiniteLifeTime6();
+}
+
+/// @brief Verify that too long hostname for Lease6 is not accepted.
+///
+/// Checks that the it is not possible to create a lease when the hostname
+/// length exceeds 255 characters.
+TEST_F(PgSqlLeaseMgrTest, lease6InvalidHostname) {
+ testLease6InvalidHostname();
+}
+
+/// @brief Verify that too long hostname for Lease6 is not accepted.
+TEST_F(PgSqlLeaseMgrTest, lease6InvalidHostnameMultiThreading) {
+ MultiThreadingTest mt(true);
+ testLease6InvalidHostname();
+}
+
+/// @brief Verify that large IAID values work correctly.
+///
+/// Adds lease with a large IAID to the database and verifies it can
+/// fetched correctly.
+TEST_F(PgSqlLeaseMgrTest, leases6LargeIaidCheck) {
+ testLease6LargeIaidCheck();
+}
+
+/// @brief Verify that large IAID values work correctly.
+TEST_F(PgSqlLeaseMgrTest, leases6LargeIaidCheckMultiThreading) {
+ MultiThreadingTest mt(true);
+ testLease6LargeIaidCheck();
+}
+
+/// @brief Check GetLease6 methods - access by DUID/IAID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// a combination of DUID and IAID.
+TEST_F(PgSqlLeaseMgrTest, getLeases6DuidIaid) {
+ testGetLeases6DuidIaid();
+}
+
+/// @brief Check GetLease6 methods - access by DUID/IAID
+TEST_F(PgSqlLeaseMgrTest, getLeases6DuidIaidMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases6DuidIaid();
+}
+
+/// @brief Check that the system can cope with a DUID of allowed size.
+TEST_F(PgSqlLeaseMgrTest, getLeases6DuidSize) {
+ testGetLeases6DuidSize();
+}
+
+/// @brief Check that the system can cope with a DUID of allowed size.
+TEST_F(PgSqlLeaseMgrTest, getLeases6DuidSizeMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases6DuidSize();
+}
+
+/// @brief Check that getLease6 methods discriminate by lease type.
+///
+/// Adds six leases, two per lease type all with the same duid and iad but
+/// with alternating subnet_ids.
+/// It then verifies that all of getLeases6() method variants correctly
+/// discriminate between the leases based on lease type alone.
+TEST_F(PgSqlLeaseMgrTest, lease6LeaseTypeCheck) {
+ testLease6LeaseTypeCheck();
+}
+
+/// @brief Check that getLease6 methods discriminate by lease type.
+TEST_F(PgSqlLeaseMgrTest, lease6LeaseTypeCheckMultiThreading) {
+ MultiThreadingTest mt(true);
+ testLease6LeaseTypeCheck();
+}
+
+/// @brief Check GetLease6 methods - access by DUID/IAID/SubnetID
+///
+/// Adds leases to the database and checks that they can be accessed via
+/// a combination of DIUID and IAID.
+TEST_F(PgSqlLeaseMgrTest, getLease6DuidIaidSubnetId) {
+ testGetLease6DuidIaidSubnetId();
+}
+
+/// @brief Check GetLease6 methods - access by DUID/IAID/SubnetID
+TEST_F(PgSqlLeaseMgrTest, getLease6DuidIaidSubnetIdMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease6DuidIaidSubnetId();
+}
+
+/// @brief Test checks that getLease6() works with different DUID sizes
+TEST_F(PgSqlLeaseMgrTest, getLease6DuidIaidSubnetIdSize) {
+ testGetLease6DuidIaidSubnetIdSize();
+}
+
+/// @brief Test checks that getLease6() works with different DUID sizes
+TEST_F(PgSqlLeaseMgrTest, getLease6DuidIaidSubnetIdSizeMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLease6DuidIaidSubnetIdSize();
+}
+
+/// @brief check leases could be retrieved by DUID
+///
+/// Create leases, add them to backend and verify if it can be queried
+/// using DUID index
+TEST_F(PgSqlLeaseMgrTest, getLeases6Duid) {
+ testGetLeases6Duid();
+}
+
+/// @brief check leases could be retrieved by DUID
+TEST_F(PgSqlLeaseMgrTest, getLeases6DuidMultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetLeases6Duid();
+}
+
+/// @brief Lease6 update tests
+///
+/// Checks that we are able to update a lease in the database.
+TEST_F(PgSqlLeaseMgrTest, updateLease6) {
+ testUpdateLease6();
+}
+
+/// @brief Lease6 update tests
+TEST_F(PgSqlLeaseMgrTest, updateLease6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testUpdateLease6();
+}
+
+/// @brief Lease6 concurrent update tests
+///
+/// Checks that we are not able to concurrently update a lease in the database.
+TEST_F(PgSqlLeaseMgrTest, concurrentUpdateLease6) {
+ testConcurrentUpdateLease6();
+}
+
+/// @brief Lease6 concurrent update tests
+///
+/// Checks that we are not able to concurrently update a lease in the database.
+TEST_F(PgSqlLeaseMgrTest, concurrentUpdateLease6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testConcurrentUpdateLease6();
+}
+
+/// @brief DHCPv4 Lease recreation tests
+///
+/// Checks that the lease can be created, deleted and recreated with
+/// different parameters. It also checks that the re-created lease is
+/// correctly stored in the lease database.
+TEST_F(PgSqlLeaseMgrTest, testRecreateLease4) {
+ testRecreateLease4();
+}
+
+/// @brief DHCPv4 Lease recreation tests
+TEST_F(PgSqlLeaseMgrTest, testRecreateLease4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testRecreateLease4();
+}
+
+/// @brief DHCPv6 Lease recreation tests
+///
+/// Checks that the lease can be created, deleted and recreated with
+/// different parameters. It also checks that the re-created lease is
+/// correctly stored in the lease database.
+TEST_F(PgSqlLeaseMgrTest, testRecreateLease6) {
+ testRecreateLease6();
+}
+
+/// @brief DHCPv6 Lease recreation tests
+TEST_F(PgSqlLeaseMgrTest, testRecreateLease6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testRecreateLease6();
+}
+
+/// @brief Checks that null DUID is not allowed.
+TEST_F(PgSqlLeaseMgrTest, nullDuid) {
+ testNullDuid();
+}
+
+/// @brief Checks that null DUID is not allowed.
+TEST_F(PgSqlLeaseMgrTest, nullDuidMultiThreading) {
+ MultiThreadingTest mt(true);
+ testNullDuid();
+}
+
+/// @brief Tests whether PostgreSQL can store and retrieve hardware addresses
+TEST_F(PgSqlLeaseMgrTest, testLease6Mac) {
+ testLease6MAC();
+}
+
+/// @brief Tests whether PostgreSQL can store and retrieve hardware addresses
+TEST_F(PgSqlLeaseMgrTest, testLease6MacMultiThreading) {
+ MultiThreadingTest mt(true);
+ testLease6MAC();
+}
+
+/// @brief Tests whether PostgreSQL can store and retrieve hardware addresses
+TEST_F(PgSqlLeaseMgrTest, testLease6HWTypeAndSource) {
+ testLease6HWTypeAndSource();
+}
+
+/// @brief Tests whether PostgreSQL can store and retrieve hardware addresses
+TEST_F(PgSqlLeaseMgrTest, testLease6HWTypeAndSourceMultiThreading) {
+ MultiThreadingTest mt(true);
+ testLease6HWTypeAndSource();
+}
+
+/// @brief Check that the expired DHCPv6 leases can be retrieved.
+///
+/// This test adds a number of leases to the lease database and marks
+/// some of them as expired. Then it queries for expired leases and checks
+/// whether only expired leases are returned, and that they are returned in
+/// the order from most to least expired. It also checks that the lease
+/// which is marked as 'reclaimed' is not returned.
+TEST_F(PgSqlLeaseMgrTest, getExpiredLeases6) {
+ testGetExpiredLeases6();
+}
+
+/// @brief Check that the expired DHCPv6 leases can be retrieved.
+TEST_F(PgSqlLeaseMgrTest, getExpiredLeases6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testGetExpiredLeases6();
+}
+
+/// @brief Checks that DHCPv6 leases with infinite valid lifetime
+/// will never expire.
+TEST_F(PgSqlLeaseMgrTest, infiniteAreNotExpired6) {
+ testInfiniteAreNotExpired6();
+}
+
+/// @brief Check that expired reclaimed DHCPv6 leases are removed.
+TEST_F(PgSqlLeaseMgrTest, deleteExpiredReclaimedLeases6) {
+ testDeleteExpiredReclaimedLeases6();
+}
+
+/// @brief Check that expired reclaimed DHCPv6 leases are removed.
+TEST_F(PgSqlLeaseMgrTest, deleteExpiredReclaimedLeases6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testDeleteExpiredReclaimedLeases6();
+}
+
+/// @brief Verifies that IPv4 lease statistics can be recalculated.
+TEST_F(PgSqlLeaseMgrTest, recountLeaseStats4) {
+ testRecountLeaseStats4();
+}
+
+/// @brief Verifies that IPv4 lease statistics can be recalculated.
+TEST_F(PgSqlLeaseMgrTest, recountLeaseStats4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testRecountLeaseStats4();
+}
+
+/// @brief Verifies that IPv6 lease statistics can be recalculated.
+TEST_F(PgSqlLeaseMgrTest, recountLeaseStats6) {
+ testRecountLeaseStats6();
+}
+
+/// @brief Verifies that IPv6 lease statistics can be recalculated.
+TEST_F(PgSqlLeaseMgrTest, recountLeaseStats6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testRecountLeaseStats6();
+}
+
+/// @brief Tests that leases from specific subnet can be removed.
+TEST_F(PgSqlLeaseMgrTest, DISABLED_wipeLeases4) {
+ testWipeLeases4();
+}
+
+/// @brief Tests that leases from specific subnet can be removed.
+TEST_F(PgSqlLeaseMgrTest, DISABLED_wipeLeases4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testWipeLeases4();
+}
+
+/// @brief Tests that leases from specific subnet can be removed.
+TEST_F(PgSqlLeaseMgrTest, DISABLED_wipeLeases6) {
+ testWipeLeases6();
+}
+
+/// @brief Tests that leases from specific subnet can be removed.
+TEST_F(PgSqlLeaseMgrTest, DISABLED_wipeLeases6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testWipeLeases6();
+}
+
+/// @brief Test fixture class for validating @c LeaseMgr using
+/// PostgreSQL as back end and PostgreSQL connectivity loss.
+class PgSqlLeaseMgrDbLostCallbackTest : public LeaseMgrDbLostCallbackTest {
+public:
+ virtual void destroySchema() {
+ destroyPgSQLSchema();
+ }
+
+ virtual void createSchema() {
+ createPgSQLSchema();
+ }
+
+ virtual std::string validConnectString() {
+ return (validPgSQLConnectionString());
+ }
+
+ virtual std::string invalidConnectString() {
+ return (connectionString(PGSQL_VALID_TYPE, INVALID_NAME, VALID_HOST,
+ VALID_USER, VALID_PASSWORD));
+ }
+};
+
+/// @brief Verifies that db lost callback is not invoked on an open failure
+TEST_F(PgSqlLeaseMgrDbLostCallbackTest, testNoCallbackOnOpenFailure) {
+ MultiThreadingTest mt(false);
+ testNoCallbackOnOpenFailure();
+}
+
+/// @brief Verifies that db lost callback is not invoked on an open failure
+TEST_F(PgSqlLeaseMgrDbLostCallbackTest, testNoCallbackOnOpenFailureMultiThreading) {
+ MultiThreadingTest mt(true);
+ testNoCallbackOnOpenFailure();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSqlLeaseMgrDbLostCallbackTest, testDbLostAndRecoveredCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndRecoveredCallback();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSqlLeaseMgrDbLostCallbackTest, testDbLostAndRecoveredCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndRecoveredCallback();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSqlLeaseMgrDbLostCallbackTest, testDbLostAndFailedCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndFailedCallback();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSqlLeaseMgrDbLostCallbackTest, testDbLostAndFailedCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndFailedCallback();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSqlLeaseMgrDbLostCallbackTest, testDbLostAndRecoveredAfterTimeoutCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndRecoveredAfterTimeoutCallback();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSqlLeaseMgrDbLostCallbackTest, testDbLostAndRecoveredAfterTimeoutCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndRecoveredAfterTimeoutCallback();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSqlLeaseMgrDbLostCallbackTest, testDbLostAndFailedAfterTimeoutCallback) {
+ MultiThreadingTest mt(false);
+ testDbLostAndFailedAfterTimeoutCallback();
+}
+
+/// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly.
+TEST_F(PgSqlLeaseMgrDbLostCallbackTest, testDbLostAndFailedAfterTimeoutCallbackMultiThreading) {
+ MultiThreadingTest mt(true);
+ testDbLostAndFailedAfterTimeoutCallback();
+}
+
+/// @brief Tests v4 lease stats query variants.
+TEST_F(PgSqlLeaseMgrTest, leaseStatsQuery4) {
+ testLeaseStatsQuery4();
+}
+
+/// @brief Tests v4 lease stats query variants.
+TEST_F(PgSqlLeaseMgrTest, leaseStatsQuery4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testLeaseStatsQuery4();
+}
+
+/// @brief Tests v6 lease stats query variants.
+TEST_F(PgSqlLeaseMgrTest, leaseStatsQuery6) {
+ testLeaseStatsQuery6();
+}
+
+/// @brief Tests v6 lease stats query variants.
+TEST_F(PgSqlLeaseMgrTest, leaseStatsQuery6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testLeaseStatsQuery6();
+}
+
+/// @brief Tests v4 lease stats to be attributed to the wrong subnet.
+TEST_F(PgSqlLeaseMgrTest, leaseStatsQueryAttribution4) {
+ testLeaseStatsQueryAttribution4();
+}
+
+/// @brief Tests v4 lease stats to be attributed to the wrong subnet.
+TEST_F(PgSqlLeaseMgrTest, leaseStatsQueryAttribution4MultiThreading) {
+ MultiThreadingTest mt(true);
+ testLeaseStatsQueryAttribution4();
+}
+
+/// @brief Tests v6 lease stats to be attributed to the wrong subnet.
+TEST_F(PgSqlLeaseMgrTest, leaseStatsQueryAttribution6) {
+ testLeaseStatsQueryAttribution6();
+}
+
+/// @brief Tests v6 lease stats to be attributed to the wrong subnet.
+TEST_F(PgSqlLeaseMgrTest, leaseStatsQueryAttribution6MultiThreading) {
+ MultiThreadingTest mt(true);
+ testLeaseStatsQueryAttribution6();
+}
+
+/// @brief This test is a basic check for the generic backend test class,
+/// rather than any production code check.
+TEST_F(PgSqlGenericBackendTest, leaseCount) {
+
+ // Create database connection parameter list
+ DatabaseConnection::ParameterMap params;
+ params["name"] = "keatest";
+ params["user"] = "keatest";
+ params["password"] = "keatest";
+
+ // Create and open the database connection
+ PgSqlConnection conn(params);
+ conn.openDatabase();
+
+ // Check that the countRows is working. It's used extensively in other
+ // tests, so basic check is enough here.
+ EXPECT_EQ(0, countRows(conn, "lease4"));
+}
+
+// Verifies that v4 class lease counts are correctly adjusted
+// when leases have class lists.
+TEST_F(PgSqlLeaseMgrTest, classLeaseCount4) {
+ if (!LeaseMgrFactory::instance().isJsonSupported()) {
+ std::cout << "Skipped test because of lack of JSON support in the database." << std::endl;
+ return;
+ }
+
+ testClassLeaseCount4();
+}
+
+// Verifies that v6 IA_NA class lease counts are correctly adjusted
+// when leases have class lists.
+TEST_F(PgSqlLeaseMgrTest, classLeaseCount6_NA) {
+ if (!LeaseMgrFactory::instance().isJsonSupported()) {
+ std::cout << "Skipped test because of lack of JSON support in the database." << std::endl;
+ return;
+ }
+
+ testClassLeaseCount6(Lease::TYPE_NA);
+}
+
+// Verifies that v6 IA_PD class lease counts are correctly adjusted
+// when leases have class lists.
+TEST_F(PgSqlLeaseMgrTest, classLeaseCount6_PD) {
+ if (!LeaseMgrFactory::instance().isJsonSupported()) {
+ std::cout << "Skipped test because of lack of JSON support in the database." << std::endl;
+ return;
+ }
+
+ testClassLeaseCount6(Lease::TYPE_PD);
+}
+
+/// @brief Checks that no exceptions are thrown when inquiring about JSON
+/// support and prints an informative message.
+TEST_F(PgSqlLeaseMgrTest, isJsonSupported) {
+ bool json_supported;
+ ASSERT_NO_THROW_LOG(json_supported = LeaseMgrFactory::instance().isJsonSupported());
+ std::cout << "JSON support is " << (json_supported ? "" : "not ") <<
+ "enabled in the database." << std::endl;
+}
+
+/// @brief Checks that a null user context allows allocation.
+TEST_F(PgSqlLeaseMgrTest, checkLimitsNull) {
+ std::string text;
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits4(nullptr));
+ EXPECT_TRUE(text.empty());
+ ASSERT_NO_THROW_LOG(text = LeaseMgrFactory::instance().checkLimits6(nullptr));
+ EXPECT_TRUE(text.empty());
+}
+
+/// @brief Checks a few v4 limit checking scenarios.
+TEST_F(PgSqlLeaseMgrTest, checkLimits4) {
+ // Limit checking should be precluded at reconfiguration time on systems
+ // that don't have JSON support in the database. It's fine if it throws.
+ if (!LeaseMgrFactory::instance().isJsonSupported()) {
+ ASSERT_THROW_MSG(LeaseMgrFactory::instance().checkLimits4(
+ isc::data::Element::createMap()), isc::db::DbOperationError,
+ "Statement exec failed for: check_lease4_limits, status: 7sqlstate:[ 42883 ], "
+ "reason: ERROR: operator does not exist: json -> unknown\n"
+ "LINE 1: ...* FROM JSON_ARRAY_ELEMENTS(json_cast(user_context)->'ISC'->'...\n"
+ " ^\n"
+ "HINT: No operator matches the given name and argument type(s). "
+ "You might need to add explicit type casts.\n"
+ "QUERY: SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(user_context)"
+ "->'ISC'->'limits'->'client-classes')\n"
+ "CONTEXT: PL/pgSQL function checklease4limits(text) line 10 at FOR over SELECT rows\n");
+ return;
+ }
+
+ // The rest of the checks are only for databases with JSON support.
+ testLeaseLimits4();
+}
+
+/// @brief Checks a few v6 limit checking scenarios.
+TEST_F(PgSqlLeaseMgrTest, checkLimits6) {
+ // Limit checking should be precluded at reconfiguration time on systems
+ // that don't have JSON support in the database. It's fine if it throws.
+ if (!LeaseMgrFactory::instance().isJsonSupported()) {
+ ASSERT_THROW_MSG(LeaseMgrFactory::instance().checkLimits6(
+ isc::data::Element::createMap()), isc::db::DbOperationError,
+ "Statement exec failed for: check_lease6_limits, status: 7sqlstate:[ 42883 ], "
+ "reason: ERROR: operator does not exist: json -> unknown\n"
+ "LINE 1: ...* FROM JSON_ARRAY_ELEMENTS(json_cast(user_context)->'ISC'->'...\n"
+ " ^\n"
+ "HINT: No operator matches the given name and argument type(s). "
+ "You might need to add explicit type casts.\n"
+ "QUERY: SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(user_context)"
+ "->'ISC'->'limits'->'client-classes')\n"
+ "CONTEXT: PL/pgSQL function checklease6limits(text) line 10 at FOR over SELECT rows\n");
+ return;
+ }
+
+ // The rest of the checks are only for databases with JSON support.
+ testLeaseLimits6();
+}
+
+} // namespace
diff --git a/src/lib/dhcpsrv/tests/pool_unittest.cc b/src/lib/dhcpsrv/tests/pool_unittest.cc
new file mode 100644
index 0000000..99af4bc
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/pool_unittest.cc
@@ -0,0 +1,751 @@
+// Copyright (C) 2012-2019 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <dhcp/option6_pdexclude.h>
+#include <dhcpsrv/pool.h>
+#include <testutils/test_to_element.h>
+
+#include <boost/scoped_ptr.hpp>
+
+#include <gtest/gtest.h>
+
+#include <iostream>
+#include <vector>
+#include <sstream>
+
+using boost::scoped_ptr;
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::data;
+using namespace isc::asiolink;
+
+namespace {
+
+TEST(Pool4Test, constructor_first_last) {
+
+ // let's construct 192.0.2.1-192.0.2.255 pool
+ Pool4 pool1(IOAddress("192.0.2.1"), IOAddress("192.0.2.255"));
+
+ EXPECT_EQ(IOAddress("192.0.2.1"), pool1.getFirstAddress());
+ EXPECT_EQ(IOAddress("192.0.2.255"), pool1.getLastAddress());
+
+ // This is Pool4, IPv6 addresses do not belong here
+ EXPECT_THROW(Pool4(IOAddress("2001:db8::1"),
+ IOAddress("192.168.0.5")), BadValue);
+ EXPECT_THROW(Pool4(IOAddress("192.168.0.2"),
+ IOAddress("2001:db8::1")), BadValue);
+
+ // Should throw. Range should be 192.0.2.1-192.0.2.2, not
+ // the other way around.
+ EXPECT_THROW(Pool4(IOAddress("192.0.2.2"), IOAddress("192.0.2.1")),
+ BadValue);
+}
+
+TEST(Pool4Test, constructor_prefix_len) {
+
+ // let's construct 2001:db8:1::/96 pool
+ Pool4 pool1(IOAddress("192.0.2.0"), 25);
+
+ EXPECT_EQ("192.0.2.0", pool1.getFirstAddress().toText());
+ EXPECT_EQ("192.0.2.127", pool1.getLastAddress().toText());
+
+ // No such thing as /33 prefix
+ EXPECT_THROW(Pool4(IOAddress("192.0.2.1"), 33), BadValue);
+
+ // /0 prefix does not make sense
+ EXPECT_THROW(Pool4(IOAddress("192.0.2.0"), 0), BadValue);
+
+ // This is Pool6, IPv4 addresses do not belong here
+ EXPECT_THROW(Pool4(IOAddress("2001:db8::1"), 20), BadValue);
+}
+
+TEST(Pool4Test, in_range) {
+ Pool4 pool1(IOAddress("192.0.2.10"), IOAddress("192.0.2.20"));
+
+ EXPECT_FALSE(pool1.inRange(IOAddress("192.0.2.0")));
+ EXPECT_TRUE(pool1.inRange(IOAddress("192.0.2.10")));
+ EXPECT_TRUE(pool1.inRange(IOAddress("192.0.2.17")));
+ EXPECT_TRUE(pool1.inRange(IOAddress("192.0.2.20")));
+ EXPECT_FALSE(pool1.inRange(IOAddress("192.0.2.21")));
+ EXPECT_FALSE(pool1.inRange(IOAddress("192.0.2.255")));
+ EXPECT_FALSE(pool1.inRange(IOAddress("255.255.255.255")));
+ EXPECT_FALSE(pool1.inRange(IOAddress("0.0.0.0")));
+}
+
+// Checks if the number of possible leases in range is reported correctly.
+TEST(Pool4Test, leasesCount) {
+ Pool4 pool1(IOAddress("192.0.2.10"), IOAddress("192.0.2.20"));
+ EXPECT_EQ(11, pool1.getCapacity());
+
+ Pool4 pool2(IOAddress("192.0.2.0"), IOAddress("192.0.2.255"));
+ EXPECT_EQ(256, pool2.getCapacity());
+
+ Pool4 pool3(IOAddress("192.168.0.0"), IOAddress("192.168.255.255"));
+ EXPECT_EQ(65536, pool3.getCapacity());
+
+ Pool4 pool4(IOAddress("10.0.0.0"), IOAddress("10.255.255.255"));
+ EXPECT_EQ(16777216, pool4.getCapacity());
+}
+
+// This test creates 100 pools and verifies that their IDs are unique.
+TEST(Pool4Test, unique_id) {
+
+ const int num_pools = 100;
+ std::vector<Pool4Ptr> pools;
+
+ for (int i = 0; i < num_pools; ++i) {
+ pools.push_back(Pool4Ptr(new Pool4(IOAddress("192.0.2.0"),
+ IOAddress("192.0.2.255"))));
+ }
+
+ for (int i = 0; i < num_pools; ++i) {
+ for (int j = i + 1; j < num_pools; ++j) {
+ if (pools[i]->getId() == pools[j]->getId()) {
+ FAIL() << "Pool-ids must be unique";
+ }
+ }
+ }
+}
+
+// Simple check if toText returns reasonable values
+TEST(Pool4Test, toText) {
+ Pool4 pool1(IOAddress("192.0.2.7"), IOAddress("192.0.2.17"));
+ EXPECT_EQ("type=V4, 192.0.2.7-192.0.2.17", pool1.toText());
+
+ Pool4 pool2(IOAddress("192.0.2.128"), 28);
+ EXPECT_EQ("type=V4, 192.0.2.128-192.0.2.143", pool2.toText());
+
+ Pool4 pool3(IOAddress("192.0.2.0"), IOAddress("192.0.2.127"));
+ EXPECT_EQ("type=V4, 192.0.2.0-192.0.2.127", pool3.toText());
+}
+
+// Simple check if toElement returns reasonable values
+TEST(Pool4Test, toElement) {
+ Pool4 pool1(IOAddress("192.0.2.7"), IOAddress("192.0.2.17"));
+ std::string expected1 = "{"
+ " \"pool\": \"192.0.2.7-192.0.2.17\", "
+ " \"option-data\": [ ] "
+ "}";
+ isc::test::runToElementTest<Pool4>(expected1, pool1);
+
+ Pool4 pool2(IOAddress("192.0.2.128"), 28);
+ std::string expected2 = "{"
+ " \"pool\": \"192.0.2.128/28\", "
+ " \"option-data\": [ ] "
+ "}";
+ isc::test::runToElementTest<Pool4>(expected2, pool2);
+
+ Pool4 pool3(IOAddress("192.0.2.0"), IOAddress("192.0.2.127"));
+ std::string expected3 = "{"
+ " \"pool\": \"192.0.2.0/25\", "
+ " \"option-data\": [ ] "
+ "}";
+ isc::test::runToElementTest<Pool4>(expected3, pool3);
+}
+
+// This test checks that it is possible to specify pool specific options.
+TEST(Pool4Test, addOptions) {
+ // Create a pool to add options to it.
+ Pool4Ptr pool(new Pool4(IOAddress("192.0.2.0"),
+ IOAddress("192.0.2.255")));
+
+ // Differentiate options by their codes (100-109)
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V4, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(pool->getCfgOption()->add(option, false, "dhcp4"));
+ }
+
+ // Add 7 options to another option space. The option codes partially overlap
+ // with option codes that we have added to dhcp4 option space.
+ for (uint16_t code = 105; code < 112; ++code) {
+ OptionPtr option(new Option(Option::V4, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(pool->getCfgOption()->add(option, false, "isc"));
+ }
+
+ // Get options from the pool and check if all 10 are there.
+ OptionContainerPtr options = pool->getCfgOption()->getAll("dhcp4");
+ ASSERT_TRUE(options);
+ ASSERT_EQ(10, options->size());
+
+ // Validate codes of options added to dhcp4 option space.
+ uint16_t expected_code = 100;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ options = pool->getCfgOption()->getAll("isc");
+ ASSERT_TRUE(options);
+ ASSERT_EQ(7, options->size());
+
+ // Validate codes of options added to isc option space.
+ expected_code = 105;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ // Try to get options from a non-existing option space.
+ options = pool->getCfgOption()->getAll("abcd");
+ ASSERT_TRUE(options);
+ EXPECT_TRUE(options->empty());
+}
+
+// This test checks that handling for user-context is valid.
+TEST(Pool4Test, userContext) {
+ // Create a pool to add options to it.
+ Pool4Ptr pool(new Pool4(IOAddress("192.0.2.0"),
+ IOAddress("192.0.2.255")));
+
+ // Context should be empty until explicitly set.
+ EXPECT_FALSE(pool->getContext());
+
+ // When set, should be returned properly.
+ ElementPtr ctx = Element::create("{ \"comment\": \"foo\" }");
+ EXPECT_NO_THROW(pool->setContext(ctx));
+ ASSERT_TRUE(pool->getContext());
+ EXPECT_EQ(ctx->str(), pool->getContext()->str());
+}
+
+// This test checks that handling for client-class is valid.
+TEST(Pool4Test, clientClass) {
+ // 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");
+
+ // This client belongs to foo, bar and baz classes.
+ isc::dhcp::ClientClasses three_classes;
+ three_classes.insert("foo");
+ three_classes.insert("bar");
+ three_classes.insert("baz");
+
+ // No class restrictions defined, any client should be supported
+ EXPECT_TRUE(pool->getClientClass().empty());
+ EXPECT_TRUE(pool->clientSupported(no_class));
+ EXPECT_TRUE(pool->clientSupported(foo_class));
+ EXPECT_TRUE(pool->clientSupported(bar_class));
+ EXPECT_TRUE(pool->clientSupported(three_classes));
+
+ // Let's allow only clients belonging to "bar" class.
+ pool->allowClientClass("bar");
+ EXPECT_EQ("bar", pool->getClientClass());
+
+ EXPECT_FALSE(pool->clientSupported(no_class));
+ EXPECT_FALSE(pool->clientSupported(foo_class));
+ EXPECT_TRUE(pool->clientSupported(bar_class));
+ EXPECT_TRUE(pool->clientSupported(three_classes));
+}
+
+// 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.
+ IOAddress first("192.0.2.0");
+ Pool4Ptr pool(new Pool4(first, IOAddress("192.0.2.255")));
+
+ // Initial values are first invalid.
+ EXPECT_EQ(first.toText(), pool->getLastAllocated().toText());
+ EXPECT_FALSE(pool->isLastAllocatedValid());
+
+ // Now set last allocated
+ IOAddress addr("192.0.2.100");
+ EXPECT_NO_THROW(pool->setLastAllocated(addr));
+ EXPECT_EQ(addr.toText(), pool->getLastAllocated().toText());
+ EXPECT_TRUE(pool->isLastAllocatedValid());
+
+ // Reset makes it invalid and does not touch address
+ pool->resetLastAllocated();
+ EXPECT_EQ(addr.toText(), pool->getLastAllocated().toText());
+ EXPECT_FALSE(pool->isLastAllocatedValid());
+}
+
+TEST(Pool6Test, constructor_first_last) {
+
+ // let's construct 2001:db8:1:: - 2001:db8:1::ffff:ffff:ffff:ffff pool
+ Pool6 pool1(Lease::TYPE_NA, IOAddress("2001:db8:1::"),
+ IOAddress("2001:db8:1::ffff:ffff:ffff:ffff"));
+
+ EXPECT_EQ(Lease::TYPE_NA, pool1.getType());
+ EXPECT_EQ(IOAddress("2001:db8:1::"), pool1.getFirstAddress());
+ EXPECT_EQ(IOAddress("2001:db8:1::ffff:ffff:ffff:ffff"),
+ pool1.getLastAddress());
+
+ // This is Pool6, IPv4 addresses do not belong here
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("2001:db8::1"),
+ IOAddress("192.168.0.5")), BadValue);
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("192.168.0.2"),
+ IOAddress("2001:db8::1")), BadValue);
+
+ // Should throw. Range should be 2001:db8::1 - 2001:db8::2, not
+ // the other way around.
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("2001:db8::2"),
+ IOAddress("2001:db8::1")), BadValue);
+}
+
+TEST(Pool6Test, constructor_prefix_len) {
+
+ // let's construct 2001:db8:1::/96 pool
+ Pool6 pool1(Lease::TYPE_NA, IOAddress("2001:db8:1::"), 96);
+
+ EXPECT_EQ(Lease::TYPE_NA, pool1.getType());
+ EXPECT_EQ("2001:db8:1::", pool1.getFirstAddress().toText());
+ EXPECT_EQ("2001:db8:1::ffff:ffff", pool1.getLastAddress().toText());
+
+ // No such thing as /130 prefix
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("2001:db8::"), 130),
+ BadValue);
+
+ // /0 prefix does not make sense
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("2001:db8::"), 0),
+ BadValue);
+
+ // This is Pool6, IPv4 addresses do not belong here
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("192.168.0.2"), 96),
+ BadValue);
+
+ // Delegated prefix length for addresses must be /128
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::"), 96, 125),
+ BadValue);
+}
+
+TEST(Pool6Test, in_range) {
+ Pool6 pool1(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ IOAddress("2001:db8:1::f"));
+
+ EXPECT_FALSE(pool1.inRange(IOAddress("2001:db8:1::")));
+ EXPECT_TRUE(pool1.inRange(IOAddress("2001:db8:1::1")));
+ EXPECT_TRUE(pool1.inRange(IOAddress("2001:db8:1::7")));
+ EXPECT_TRUE(pool1.inRange(IOAddress("2001:db8:1::f")));
+ EXPECT_FALSE(pool1.inRange(IOAddress("2001:db8:1::10")));
+ EXPECT_FALSE(pool1.inRange(IOAddress("::")));
+}
+
+// Checks that Prefix Delegation pools are handled properly
+TEST(Pool6Test, PD) {
+
+ // Let's construct 2001:db8:1::/96 PD pool, split into /112 prefixes
+ Pool6 pool1(Lease::TYPE_PD, IOAddress("2001:db8:1::"), 96, 112);
+
+ EXPECT_EQ(Lease::TYPE_PD, pool1.getType());
+ EXPECT_EQ(112, pool1.getLength());
+ EXPECT_EQ("2001:db8:1::", pool1.getFirstAddress().toText());
+ EXPECT_EQ("2001:db8:1::ffff:ffff", pool1.getLastAddress().toText());
+
+ // Check that it's not possible to have min-max range for PD
+ EXPECT_THROW(Pool6 pool2(Lease::TYPE_PD, IOAddress("2001:db8:1::1"),
+ IOAddress("2001:db8:1::f")), BadValue);
+
+ // Check that it's not allowed to delegate bigger prefix than the pool
+ // Let's try to split /64 prefix into /56 chunks (should be impossible)
+ EXPECT_THROW(Pool6 pool3(Lease::TYPE_PD, IOAddress("2001:db8:1::"),
+ 64, 56), BadValue);
+
+ // It should be possible to have a pool split into just a single chunk
+ // Let's try to split 2001:db8:1::/77 into a single /77 delegated prefix
+ EXPECT_NO_THROW(Pool6 pool4(Lease::TYPE_PD, IOAddress("2001:db8:1::"),
+ 77, 77));
+}
+
+// Checks that prefix pools with excluded prefixes are handled properly.
+TEST(Pool6Test, PDExclude) {
+ Pool6Ptr pool;
+
+ // Create a pool with a good excluded prefix. The good excluded prefix
+ // is the one for which is a sub-prefix of the main prefix.
+ ASSERT_NO_THROW(pool.reset(new Pool6(IOAddress("2001:db8:1::"), 96, 112,
+ IOAddress("2001:db8:1::2000"), 120)));
+
+ // Verify pool properties.
+ EXPECT_EQ(Lease::TYPE_PD, pool->getType());
+ EXPECT_EQ(112, pool->getLength());
+ EXPECT_EQ("2001:db8:1::", pool->getFirstAddress().toText());
+ EXPECT_EQ("2001:db8:1::ffff:ffff", pool->getLastAddress().toText());
+
+ // It should include Prefix Exclude option.
+ Option6PDExcludePtr pd_exclude_option = pool->getPrefixExcludeOption();
+ ASSERT_TRUE(pd_exclude_option);
+ EXPECT_EQ("2001:db8:1::2:2000", pd_exclude_option->
+ getExcludedPrefix(IOAddress("2001:db8:1:0:0:0:2::"), 112).toText());
+ EXPECT_EQ(120, static_cast<unsigned>(pd_exclude_option->getExcludedPrefixLength()));
+
+ // Create another pool instance, but with the excluded prefix being
+ // "unspecified".
+ ASSERT_NO_THROW(pool.reset(new Pool6(IOAddress("2001:db8:1::"), 96, 112,
+ IOAddress::IPV6_ZERO_ADDRESS(), 0)));
+
+ EXPECT_EQ(Lease::TYPE_PD, pool->getType());
+ EXPECT_EQ(112, pool->getLength());
+ EXPECT_EQ("2001:db8:1::", pool->getFirstAddress().toText());
+ EXPECT_EQ("2001:db8:1::ffff:ffff", pool->getLastAddress().toText());
+
+ ASSERT_FALSE(pool->getPrefixExcludeOption());
+
+ // Excluded prefix length must be greater than the main prefix length.
+ EXPECT_THROW(Pool6(IOAddress("2001:db8:1::"), 96, 112,
+ IOAddress("2001:db8:1::"), 112),
+ BadValue);
+
+ // Again, the excluded prefix length must be greater than main prefix
+ // length.
+ EXPECT_THROW(Pool6(IOAddress("2001:db8:1::"), 96, 112,
+ IOAddress("2001:db8:1::"), 104),
+ BadValue);
+
+ // The "unspecified" excluded prefix must have both values set to 0.
+ EXPECT_THROW(Pool6(IOAddress("2001:db8:1::"), 48, 64,
+ IOAddress("2001:db8:1::"), 0),
+ BadValue);
+
+ // Similar case as above, but the prefix value is 0 and the length
+ // is non zero.
+ EXPECT_THROW(Pool6(IOAddress("2001:db8:1::"), 48, 64,
+ IOAddress::IPV6_ZERO_ADDRESS(), 72),
+ BadValue);
+
+ // Excluded prefix must be an IPv6 prefix.
+ EXPECT_THROW(Pool6(IOAddress("10::"), 8, 16,
+ IOAddress("10.0.0.0"), 24),
+ BadValue);
+
+ // Excluded prefix length must not be greater than 128.
+ EXPECT_THROW(Pool6(IOAddress("2001:db8:1::"), 48, 64,
+ IOAddress("2001:db8:1::"), 129),
+ BadValue);
+}
+
+// Checks that temporary address pools are handled properly
+TEST(Pool6Test, TA) {
+ // Note: since we defined TA pool types during PD work, we can test it
+ // now. Although the configuration to take advantage of it is not
+ // planned for now, we will support it some day.
+
+ // Let's construct 2001:db8:1::/96 temporary addresses
+ Pool6Ptr pool1;
+ EXPECT_NO_THROW(pool1.reset(new Pool6(Lease::TYPE_TA,
+ IOAddress("2001:db8:1::"), 96)));
+
+ // Check that TA range can be only defined for single addresses
+ EXPECT_THROW(Pool6(Lease::TYPE_TA, IOAddress("2001:db8:1::"), 96, 127),
+ BadValue);
+
+ ASSERT_TRUE(pool1);
+ EXPECT_EQ(Lease::TYPE_TA, pool1->getType());
+ EXPECT_EQ(128, pool1->getLength()); // singular addresses, not prefixes
+ EXPECT_EQ("2001:db8:1::", pool1->getFirstAddress().toText());
+ EXPECT_EQ("2001:db8:1::ffff:ffff", pool1->getLastAddress().toText());
+
+ // Check that it's possible to have min-max range for TA
+ Pool6Ptr pool2;
+ EXPECT_NO_THROW(pool2.reset(new Pool6(Lease::TYPE_TA,
+ IOAddress("2001:db8:1::1"),
+ IOAddress("2001:db8:1::f"))));
+ ASSERT_TRUE(pool2);
+ EXPECT_EQ(Lease::TYPE_TA, pool2->getType());
+ EXPECT_EQ(128, pool2->getLength()); // singular addresses, not prefixes
+ EXPECT_EQ("2001:db8:1::1", pool2->getFirstAddress().toText());
+ EXPECT_EQ("2001:db8:1::f", pool2->getLastAddress().toText());
+}
+
+// This test creates 100 pools and verifies that their IDs are unique.
+TEST(Pool6Test, unique_id) {
+
+ const int num_pools = 100;
+ std::vector<Pool6Ptr> pools;
+
+ for (int i = 0; i < num_pools; ++i) {
+ pools.push_back(Pool6Ptr(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::"),
+ IOAddress("2001:db8:1::ffff:ffff:ffff:ffff"))));
+ }
+
+ for (int i = 0; i < num_pools; ++i) {
+ for (int j = i + 1; j < num_pools; ++j) {
+ if (pools[i]->getId() == pools[j]->getId()) {
+ FAIL() << "Pool-ids must be unique";
+ }
+ }
+ }
+
+}
+
+// Simple check if toText returns reasonable values
+TEST(Pool6Test, toText) {
+ Pool6 pool1(Lease::TYPE_NA, IOAddress("2001:db8::1"),
+ IOAddress("2001:db8::2"));
+ EXPECT_EQ("type=IA_NA, 2001:db8::1-2001:db8::2, delegated_len=128",
+ pool1.toText());
+
+ Pool6 pool2(Lease::TYPE_PD, IOAddress("2001:db8:1::"), 96, 112);
+ EXPECT_EQ("type=IA_PD, 2001:db8:1::-2001:db8:1::ffff:ffff, delegated_len=112",
+ pool2.toText());
+
+ Pool6 pool3(IOAddress("2001:db8:1::"), 96, 112,
+ IOAddress("2001:db8:1::1000"), 120);
+ EXPECT_EQ("type=IA_PD, 2001:db8:1::-2001:db8:1::ffff:ffff, delegated_len=112,"
+ " excluded_prefix_len=120",
+ pool3.toText());
+
+ Pool6 pool4(Lease::TYPE_NA, IOAddress("2001:db8::"),
+ IOAddress("2001:db8::ffff"));
+ EXPECT_EQ("type=IA_NA, 2001:db8::-2001:db8::ffff, delegated_len=128",
+ pool4.toText());
+}
+
+// Simple check if toElement returns reasonable values
+TEST(Pool6Test, toElement) {
+ Pool6 pool1(Lease::TYPE_NA, IOAddress("2001:db8::1"),
+ IOAddress("2001:db8::2"));
+ std::string expected1 = "{"
+ " \"pool\": \"2001:db8::1-2001:db8::2\", "
+ " \"option-data\": [ ] "
+ "}";
+ isc::test::runToElementTest<Pool6>(expected1, pool1);
+
+ Pool6 pool2(Lease::TYPE_PD, IOAddress("2001:db8:1::"), 96, 112);
+ std::string expected2 = "{"
+ " \"prefix\": \"2001:db8:1::\", "
+ " \"prefix-len\": 96, "
+ " \"delegated-len\": 112, "
+ " \"option-data\": [ ] "
+ "}";
+ isc::test::runToElementTest<Pool6>(expected2, pool2);
+
+ Pool6 pool3(IOAddress("2001:db8:1::"), 96, 112,
+ IOAddress("2001:db8:1::1000"), 120);
+ std::string expected3 = "{"
+ " \"prefix\": \"2001:db8:1::\", "
+ " \"prefix-len\": 96, "
+ " \"delegated-len\": 112, "
+ " \"excluded-prefix\": \"2001:db8:1::1000\", "
+ " \"excluded-prefix-len\": 120, "
+ " \"option-data\": [ ] "
+ "}";
+ isc::test::runToElementTest<Pool6>(expected3, pool3);
+
+ Pool6 pool4(Lease::TYPE_NA, IOAddress("2001:db8::"),
+ IOAddress("2001:db8::ffff"));
+ std::string expected4 = "{"
+ " \"pool\": \"2001:db8::/112\", "
+ " \"option-data\": [ ] "
+ "}";
+ isc::test::runToElementTest<Pool6>(expected4, pool4);
+}
+
+// Checks if the number of possible leases in range is reported correctly.
+TEST(Pool6Test, leasesCount) {
+ Pool6 pool1(Lease::TYPE_NA, IOAddress("2001:db8::1"),
+ IOAddress("2001:db8::2"));
+ EXPECT_EQ(2, pool1.getCapacity());
+
+ Pool6 pool2(Lease::TYPE_PD, IOAddress("2001:db8:1::"), 96, 112);
+ EXPECT_EQ(65536, pool2.getCapacity());
+}
+
+// This test checks that it is possible to specify pool specific options.
+TEST(Pool6Test, addOptions) {
+ // Create a pool to add options to it.
+ Pool6Ptr pool(new Pool6(Lease::TYPE_PD, IOAddress("3000::"), 64, 128));
+
+ // Differentiate options by their codes (100-109)
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(pool->getCfgOption()->add(option, false, "dhcp6"));
+ }
+
+ // Add 7 options to another option space. The option codes partially overlap
+ // with option codes that we have added to dhcp6 option space.
+ for (uint16_t code = 105; code < 112; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(pool->getCfgOption()->add(option, false, "isc"));
+ }
+
+ // Get options from the pool and check if all 10 are there.
+ OptionContainerPtr options = pool->getCfgOption()->getAll("dhcp6");
+ ASSERT_TRUE(options);
+ ASSERT_EQ(10, options->size());
+
+ // Validate codes of options added to dhcp6 option space.
+ uint16_t expected_code = 100;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ options = pool->getCfgOption()->getAll("isc");
+ ASSERT_TRUE(options);
+ ASSERT_EQ(7, options->size());
+
+ // Validate codes of options added to isc option space.
+ expected_code = 105;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ // Try to get options from a non-existing option space.
+ options = pool->getCfgOption()->getAll("abcd");
+ ASSERT_TRUE(options);
+ EXPECT_TRUE(options->empty());
+}
+
+// This test checks that handling for user-context is valid.
+TEST(Pool6Test, userContext) {
+ // Create a pool to add options to it.
+ Pool6 pool(Lease::TYPE_NA, IOAddress("2001:db8::1"),
+ IOAddress("2001:db8::2"));
+
+ // Context should be empty until explicitly set.
+ EXPECT_FALSE(pool.getContext());
+
+ // When set, should be returned properly.
+ ElementPtr ctx = Element::create("{ \"comment\": \"foo\" }");
+ EXPECT_NO_THROW(pool.setContext(ctx));
+ ASSERT_TRUE(pool.getContext());
+ EXPECT_EQ(ctx->str(), pool.getContext()->str());
+}
+
+// This test checks that handling for client-class is valid.
+TEST(Pool6Test, clientClass) {
+ // 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");
+
+ // This client belongs to foo, bar and baz classes.
+ isc::dhcp::ClientClasses three_classes;
+ three_classes.insert("foo");
+ three_classes.insert("bar");
+ three_classes.insert("baz");
+
+ // No class restrictions defined, any client should be supported
+ EXPECT_TRUE(pool.getClientClass().empty());
+ EXPECT_TRUE(pool.clientSupported(no_class));
+ EXPECT_TRUE(pool.clientSupported(foo_class));
+ EXPECT_TRUE(pool.clientSupported(bar_class));
+ EXPECT_TRUE(pool.clientSupported(three_classes));
+
+ // Let's allow only clients belonging to "bar" class.
+ pool.allowClientClass("bar");
+ EXPECT_EQ("bar", pool.getClientClass());
+
+ EXPECT_FALSE(pool.clientSupported(no_class));
+ EXPECT_FALSE(pool.clientSupported(foo_class));
+ EXPECT_TRUE(pool.clientSupported(bar_class));
+ EXPECT_TRUE(pool.clientSupported(three_classes));
+}
+
+// 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.
+ IOAddress first("2001:db8::1");
+ Pool6 pool(Lease::TYPE_NA, first, IOAddress("2001:db8::200"));
+
+ // Initial values are first invalid.
+ EXPECT_EQ(first.toText(), pool.getLastAllocated().toText());
+ EXPECT_FALSE(pool.isLastAllocatedValid());
+
+ // Now set last allocated
+ IOAddress addr("2001:db8::100");
+ EXPECT_NO_THROW(pool.setLastAllocated(addr));
+ EXPECT_EQ(addr.toText(), pool.getLastAllocated().toText());
+ EXPECT_TRUE(pool.isLastAllocatedValid());
+
+ // Reset makes it invalid and does not touch address
+ pool.resetLastAllocated();
+ EXPECT_EQ(addr.toText(), pool.getLastAllocated().toText());
+ EXPECT_FALSE(pool.isLastAllocatedValid());
+}
+
+}; // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/resource_handler_unittest.cc b/src/lib/dhcpsrv/tests/resource_handler_unittest.cc
new file mode 100644
index 0000000..41f4d69
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/resource_handler_unittest.cc
@@ -0,0 +1,509 @@
+// Copyright (C) 2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcpsrv/resource_handler.h>
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+
+namespace {
+
+// Verifies behavior with empty block.
+TEST(ResourceHandleTest, empty) {
+ try {
+ // Get a resource handler.
+ ResourceHandler resource_handler;
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies behavior with empty block (v4).
+TEST(ResourceHandleTest, empty4) {
+ try {
+ // Get a resource handler.
+ ResourceHandler4 resource_handler;
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies behavior with one handler.
+TEST(ResourceHandleTest, one) {
+ IOAddress addr("2001:db8::1");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock(Lease::TYPE_NA, addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies behavior with one IPv4 handler.
+TEST(ResourceHandleTest, one4) {
+ IOAddress addr("192.0.2.1");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler4 resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock4(addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies behavior with two handlers.
+TEST(ResourceHandleTest, two) {
+ IOAddress addr("2001:db8::");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock(Lease::TYPE_PD, addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // Get a second resource handler.
+ ResourceHandler resource_handler2;
+
+ // Try to lock it.
+ EXPECT_NO_THROW(busy = !resource_handler2.tryLock(Lease::TYPE_PD, addr));
+
+ // Should return true (busy);
+ EXPECT_TRUE(busy);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies behavior with two IPv4 handlers.
+TEST(ResourceHandleTest, two4) {
+ IOAddress addr("192.0.2.1");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler4 resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock4(addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // Get a second resource handler.
+ ResourceHandler4 resource_handler2;
+
+ // Try to lock it.
+ EXPECT_NO_THROW(busy = !resource_handler2.tryLock4(addr));
+
+ // Should return true (busy);
+ EXPECT_TRUE(busy);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies behavior with two handlers in different blocks (sequence).
+TEST(ResourceHandleTest, sequence) {
+ IOAddress addr("2001:db8::1");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock(Lease::TYPE_NA, addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+
+ try {
+ // Get a second resource handler.
+ ResourceHandler resource_handler2;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler2.tryLock(Lease::TYPE_NA, addr));
+
+ // Should return false (free)
+ EXPECT_FALSE(busy);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies behavior with two IPv4 handlers in different blocks (sequence).
+TEST(ResourceHandleTest, sequence4) {
+ IOAddress addr("192.0.2.1");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler4 resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock4(addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+
+ try {
+ // Get a second resource handler.
+ ResourceHandler4 resource_handler2;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler2.tryLock4(addr));
+
+ // Should return false (free)
+ EXPECT_FALSE(busy);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies behavior with two handlers for different addresses.
+TEST(ResourceHandleTest, differentAddress) {
+ IOAddress addr("2001:db8::1");
+ IOAddress addr2("2001:db8::2");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock(Lease::TYPE_NA, addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // Get a second resource handler.
+ ResourceHandler resource_handler2;
+
+ // Try to lock it.
+ EXPECT_NO_THROW(busy = !resource_handler2.tryLock(Lease::TYPE_NA, addr2));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies behavior with two IPv4 handlers for different addresses.
+TEST(ResourceHandleTest, differentAddress4) {
+ IOAddress addr("192.0.2.1");
+ IOAddress addr2("192.0.2.2");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler4 resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock4(addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // Get a second resource handler.
+ ResourceHandler4 resource_handler2;
+
+ // Try to lock it.
+ EXPECT_NO_THROW(busy = !resource_handler2.tryLock4(addr2));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies behavior with two handlers for different types.
+TEST(ResourceHandleTest, differentTypes) {
+ IOAddress addr("2001:db8::");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock(Lease::TYPE_NA, addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // Get a second resource handler.
+ ResourceHandler resource_handler2;
+
+ // Try to lock it.
+ EXPECT_NO_THROW(busy = !resource_handler2.tryLock(Lease::TYPE_PD, addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies behavior of the isLocked predicate.
+TEST(ResourceHandleTest, isLocked) {
+ IOAddress addr("2001:db8::1");
+ IOAddress addr2("2001:db8::2");
+ IOAddress addr3("2001:db8::3");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock(Lease::TYPE_NA, addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // Get a second resource handler.
+ ResourceHandler resource_handler2;
+
+ // Try to lock it.
+ EXPECT_NO_THROW(busy = !resource_handler2.tryLock(Lease::TYPE_NA, addr2));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // Check ownership.
+ EXPECT_TRUE(resource_handler.isLocked(Lease::TYPE_NA, addr));
+ EXPECT_FALSE(resource_handler.isLocked(Lease::TYPE_NA, addr2));
+ EXPECT_FALSE(resource_handler.isLocked(Lease::TYPE_NA, addr3));
+ EXPECT_FALSE(resource_handler.isLocked(Lease::TYPE_PD, addr));
+ EXPECT_FALSE(resource_handler2.isLocked(Lease::TYPE_NA, addr));
+ EXPECT_TRUE(resource_handler2.isLocked(Lease::TYPE_NA, addr2));
+ EXPECT_FALSE(resource_handler2.isLocked(Lease::TYPE_NA, addr3));
+ EXPECT_FALSE(resource_handler2.isLocked(Lease::TYPE_PD, addr2));
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies behavior with two IPv4 handlers.
+TEST(ResourceHandleTest, isLocked4) {
+ IOAddress addr("192.0.2.1");
+ IOAddress addr2("192.0.2.2");
+ IOAddress addr3("192.0.2.3");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler4 resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock4(addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // Get a second resource handler.
+ ResourceHandler4 resource_handler2;
+
+ // Try to lock it.
+ EXPECT_NO_THROW(busy = !resource_handler2.tryLock4(addr2));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // Check ownership.
+ EXPECT_TRUE(resource_handler.isLocked4(addr));
+ EXPECT_FALSE(resource_handler.isLocked4(addr2));
+ EXPECT_FALSE(resource_handler.isLocked4(addr3));
+ EXPECT_FALSE(resource_handler2.isLocked4(addr));
+ EXPECT_TRUE(resource_handler2.isLocked4(addr2));
+ EXPECT_FALSE(resource_handler2.isLocked4(addr3));
+
+ // ResourceHandler4 derives from ResourceHandler.
+ EXPECT_FALSE(resource_handler.isLocked(Lease::TYPE_NA, addr));
+ EXPECT_FALSE(resource_handler2.isLocked(Lease::TYPE_NA, addr2));
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies that double tryLock call for the same resource returns busy.
+TEST(ResourceHandleTest, doubleTryLock) {
+ IOAddress addr("2001:db8::");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock(Lease::TYPE_PD, addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // Try to lock it again.
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock(Lease::TYPE_PD, addr));
+
+ // Should return true (busy);
+ EXPECT_TRUE(busy);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies that double tryLock call for the same resource returns busy (v4).
+TEST(ResourceHandleTest, doubleTryLock4) {
+ IOAddress addr("192.0.2.1");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler4 resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock4(addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // Try to lock it again.
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock4(addr));
+
+ // Should return true (busy);
+ EXPECT_TRUE(busy);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies behavior of the unLock method.
+TEST(ResourceHandleTest, unLock) {
+ IOAddress addr("2001:db8::1");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock(Lease::TYPE_NA, addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // The resource is owned by us.
+ EXPECT_TRUE(resource_handler.isLocked(Lease::TYPE_NA, addr));
+
+ // Try to unlock it.
+ EXPECT_NO_THROW(resource_handler.unLock(Lease::TYPE_NA, addr));
+
+ // The resource is no longer owned by us.
+ EXPECT_FALSE(resource_handler.isLocked(Lease::TYPE_NA, addr));
+
+ // Get a second resource handler.
+ ResourceHandler resource_handler2;
+
+ // Try to lock it by the second handler.
+ EXPECT_NO_THROW(busy = !resource_handler2.tryLock(Lease::TYPE_NA, addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // The resource is owned by the second handler.
+ EXPECT_FALSE(resource_handler.isLocked(Lease::TYPE_NA, addr));
+ EXPECT_TRUE(resource_handler2.isLocked(Lease::TYPE_NA, addr));
+
+ // Only the owner is allowed to release a resource.
+ EXPECT_THROW(resource_handler.unLock(Lease::TYPE_NA, addr), NotFound);
+ EXPECT_NO_THROW(resource_handler2.unLock(Lease::TYPE_NA, addr));
+ // Once.
+ EXPECT_THROW(resource_handler2.unLock(Lease::TYPE_NA, addr), NotFound);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+// Verifies behavior of the unLock method.
+TEST(ResourceHandleTest, unLock4) {
+ IOAddress addr("192.0.2.1");
+
+ try {
+ // Get a resource handler.
+ ResourceHandler4 resource_handler;
+
+ // Try to lock it.
+ bool busy = false;
+ EXPECT_NO_THROW(busy = !resource_handler.tryLock4(addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // The resource is owned by us.
+ EXPECT_TRUE(resource_handler.isLocked4(addr));
+
+ // Try to unlock it.
+ EXPECT_NO_THROW(resource_handler.unLock4(addr));
+
+ // The resource is no longer owned by us.
+ EXPECT_FALSE(resource_handler.isLocked4(addr));
+
+ // Get a second resource handler
+ ResourceHandler4 resource_handler2;
+
+ // Try to lock it by the second handler.
+ EXPECT_NO_THROW(busy = !resource_handler2.tryLock4(addr));
+
+ // Should return false (free).
+ EXPECT_FALSE(busy);
+
+ // The resource is owned by the second handler.
+ EXPECT_FALSE(resource_handler.isLocked4(addr));
+ EXPECT_TRUE(resource_handler2.isLocked4(addr));
+
+ // Only the owner is allowed to release a resource.
+ EXPECT_THROW(resource_handler.unLock4(addr), NotFound);
+ EXPECT_NO_THROW(resource_handler2.unLock4(addr));
+ // Once.
+ EXPECT_THROW(resource_handler2.unLock4(addr), NotFound);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ }
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/run_unittests.cc b/src/lib/dhcpsrv/tests/run_unittests.cc
new file mode 100644
index 0000000..76b2ebf
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/run_unittests.cc
@@ -0,0 +1,20 @@
+// Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <log/logger_support.h>
+
+#include <gtest/gtest.h>
+
+int
+main(int argc, char* argv[]) {
+ ::testing::InitGoogleTest(&argc, argv);
+ isc::log::initLogger();
+
+ int result = RUN_ALL_TESTS();
+
+ return (result);
+}
diff --git a/src/lib/dhcpsrv/tests/sanity_checks_unittest.cc b/src/lib/dhcpsrv/tests/sanity_checks_unittest.cc
new file mode 100644
index 0000000..cb305d6
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/sanity_checks_unittest.cc
@@ -0,0 +1,378 @@
+// Copyright (C) 2018-2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcpsrv/cfg_consistency.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/parsers/sanity_checks_parser.h>
+#include <dhcpsrv/srv_config.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/subnet.h>
+#include <dhcpsrv/sanity_checker.h>
+#include <dhcpsrv/testutils/test_utils.h>
+#include <util/range_utilities.h>
+#include <cc/data.h>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc::data;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+
+class SanityChecksTest : public ::testing::Test {
+public:
+
+ SanityChecksTest() {
+ LeaseMgrFactory::destroy();
+ }
+
+ void startLeaseBackend(bool v6) {
+ std::ostringstream s;
+ s << "type=memfile " << (v6 ? "universe=6 " : "universe=4 ")
+ << "persist=false lfc-interval=0";
+ LeaseMgrFactory::create(s.str());
+ }
+
+ void setLeaseCheck(CfgConsistency::LeaseSanity sanity) {
+ CfgMgr::instance().getCurrentCfg()->getConsistency()->setLeaseSanityCheck(sanity);
+ }
+
+ ~SanityChecksTest() {
+ CfgMgr::instance().clear();
+ LeaseMgrFactory::destroy();
+ }
+
+ /// @brief Generates a simple IPv4 lease.
+ ///
+ /// The HW address is randomly generated, subnet_id is specified.
+ ///
+ /// @param address Lease address.
+ /// @param subnet_id ID of the subnet to use.
+ ///
+ /// @return new lease with random content
+ Lease4Ptr newLease4(const IOAddress& address, SubnetID subnet_id) {
+
+ // Randomize HW address.
+ vector<uint8_t> mac(6);
+ isc::util::fillRandom(mac.begin(), mac.end());
+ HWAddrPtr hwaddr(new HWAddr(mac, HTYPE_ETHER));
+
+ vector<uint8_t> clientid(1);
+
+ time_t timestamp = time(NULL) - 86400 + random()%86400;
+
+ // Return created lease.
+ return (Lease4Ptr(new Lease4(address, hwaddr,
+ &clientid[0], 0, // no client-id
+ 1200, // valid
+ timestamp, subnet_id, false, false, "")));
+ }
+
+ /// @brief Generates a simple IPv6 lease.
+ ///
+ /// The DUID and IAID are randomly generated, subnet_id is specified.
+ ///
+ /// @param address Lease address.
+ /// @param subnet_id ID of the subnet to use.
+ ///
+ /// @return new lease with random content
+ Lease6Ptr newLease6(const IOAddress& address, SubnetID subnet_id) {
+ // Let's generate DUID of random length.
+ std::vector<uint8_t> duid_vec(8 + random()%20);
+ // And then fill it with random value.
+ isc::util::fillRandom(duid_vec.begin(), duid_vec.end());
+ DuidPtr duid(new DUID(duid_vec));
+
+ Lease::Type lease_type = Lease::TYPE_NA;
+ uint32_t iaid = 1 + random()%100;
+
+ std::ostringstream hostname;
+ hostname << "hostname" << (random() % 2048);
+
+ // Return created lease.
+ Lease6Ptr lease(new Lease6(lease_type, address, duid, iaid,
+ 1000, 1200, // pref, valid
+ subnet_id,
+ false, false, "")); // fqdn fwd, rev, hostname
+ return (lease);
+ }
+
+ Subnet4Ptr createSubnet4(string subnet_txt, SubnetID id) {
+ size_t pos = subnet_txt.find("/");
+ isc::asiolink::IOAddress addr(subnet_txt.substr(0, pos));
+ size_t len = boost::lexical_cast<unsigned int>(subnet_txt.substr(pos + 1));
+
+ return (Subnet4Ptr(new Subnet4(addr, len, 1000, 2000, 3000, id)));
+ }
+
+ Subnet6Ptr createSubnet6(string subnet_txt, SubnetID id) {
+ size_t pos = subnet_txt.find("/");
+ isc::asiolink::IOAddress addr(subnet_txt.substr(0, pos));
+ size_t len = boost::lexical_cast<unsigned int>(subnet_txt.substr(pos + 1));
+
+ return (Subnet6Ptr(new Subnet6(addr, len, 1000, 2000, 3000, 4000, id)));
+ }
+
+ void
+ parserCheck(SrvConfig& cfg, const string& txt, bool exp_throw,
+ CfgConsistency::LeaseSanity exp_sanity) {
+
+ SanityChecksParser parser;
+
+ ElementPtr json;
+ EXPECT_NO_THROW(json = Element::fromJSON(txt));
+
+ if (exp_throw) {
+ EXPECT_THROW(parser.parse(cfg, json), DhcpConfigError);
+
+ return;
+ }
+
+ // Should not throw.
+ EXPECT_NO_THROW(parser.parse(cfg, json));
+
+ EXPECT_EQ(cfg.getConsistency()->getLeaseSanityCheck(), exp_sanity);
+ }
+
+};
+
+// Verify whether configuration parser is able to understand the values
+// that are valid and reject those that are not.
+TEST_F(SanityChecksTest, leaseCheck) {
+
+ // These are valid and should be accepted.
+ string valid1 = "{ \"lease-checks\": \"none\" }";
+ string valid2 = "{ \"lease-checks\": \"warn\" }";
+ string valid3 = "{ \"lease-checks\": \"fix\" }";
+ string valid4 = "{ \"lease-checks\": \"fix-del\" }";
+ string valid5 = "{ \"lease-checks\": \"del\" }";
+
+ // These are not valid values or types.
+ string bogus1 = "{ \"lease-checks\": \"sanitize\" }";
+ string bogus2 = "{ \"lease-checks\": \"ignore\" }";
+ string bogus3 = "{ \"lease-checks\": true }";
+ string bogus4 = "{ \"lease-checks\": 42 }";
+
+ SrvConfig cfg;
+
+ // The default should be to none.
+ EXPECT_EQ(cfg.getConsistency()->getLeaseSanityCheck(),
+ CfgConsistency::LEASE_CHECK_NONE);
+
+ parserCheck(cfg, valid1, false, CfgConsistency::LEASE_CHECK_NONE);
+ parserCheck(cfg, valid2, false, CfgConsistency::LEASE_CHECK_WARN);
+ parserCheck(cfg, valid3, false, CfgConsistency::LEASE_CHECK_FIX);
+ parserCheck(cfg, valid4, false, CfgConsistency::LEASE_CHECK_FIX_DEL);
+ parserCheck(cfg, valid5, false, CfgConsistency::LEASE_CHECK_DEL);
+
+ parserCheck(cfg, bogus1, true, CfgConsistency::LEASE_CHECK_NONE);
+ parserCheck(cfg, bogus2, true, CfgConsistency::LEASE_CHECK_NONE);
+ parserCheck(cfg, bogus3, true, CfgConsistency::LEASE_CHECK_NONE);
+ parserCheck(cfg, bogus4, true, CfgConsistency::LEASE_CHECK_NONE);
+}
+
+// Verify whether sanity checker works as expected (valid v4).
+TEST_F(SanityChecksTest, valid4) {
+ // Create network and lease.
+ CfgMgr::instance().setFamily(AF_INET);
+ Subnet4Ptr subnet = createSubnet4("192.168.1.0/24", 1);
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->add(subnet);
+ IOAddress addr("192.168.1.1");
+ Lease4Ptr lease = newLease4(addr, 1);
+
+ // Check the lease.
+ setLeaseCheck(CfgConsistency::LEASE_CHECK_FIX_DEL);
+ SanityChecker checker;
+ checker.checkLease(lease);
+
+ // Verify the lease is still here in the same subnet.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(subnet->getID(), lease->subnet_id_);
+}
+
+// Verify whether sanity checker works as expected (valid v6).
+TEST_F(SanityChecksTest, valid6) {
+ // Create network and lease.
+ CfgMgr::instance().setFamily(AF_INET6);
+ Subnet6Ptr subnet = createSubnet6("2001:db8:1::/64", 1);
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->add(subnet);
+ IOAddress addr("2001:db8:1::1");
+ Lease6Ptr lease = newLease6(addr, 1);
+
+ // Check the lease.
+ setLeaseCheck(CfgConsistency::LEASE_CHECK_FIX_DEL);
+ SanityChecker checker;
+ checker.checkLease(lease);
+
+ // Verify the lease is still here in the same subnet.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(subnet->getID(), lease->subnet_id_);
+}
+
+// Verify whether sanity checker works as expected (wrong subnet v4).
+TEST_F(SanityChecksTest, wrongSubnet4) {
+ // Create networks and lease in the second and wrong subnet.
+ CfgMgr::instance().setFamily(AF_INET);
+ Subnet4Ptr subnet1 = createSubnet4("192.168.1.0/24", 1);
+ Subnet4Ptr subnet2 = createSubnet4("192.168.2.0/24", 2);
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->add(subnet1);
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->add(subnet2);
+ IOAddress addr("192.168.1.1");
+ Lease4Ptr lease = newLease4(addr, 2);
+
+ // Check the lease.
+ setLeaseCheck(CfgConsistency::LEASE_CHECK_FIX_DEL);
+ SanityChecker checker;
+ checker.checkLease(lease);
+
+ // Verify the lease is still here but was moved to the first and right subnet.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(subnet1->getID(), lease->subnet_id_);
+}
+
+// Verify whether sanity checker works as expected (wrong subnet v6).
+TEST_F(SanityChecksTest, wrongSubnet6) {
+ // Create networks and lease in the second and wrong subnet.
+ CfgMgr::instance().setFamily(AF_INET6);
+ Subnet6Ptr subnet1 = createSubnet6("2001:db8:1::/64", 1);
+ Subnet6Ptr subnet2 = createSubnet6("2001:db8:2::/64", 2);
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->add(subnet1);
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->add(subnet2);
+ IOAddress addr("2001:db8:1::1");
+ Lease6Ptr lease = newLease6(addr, 2);
+
+ // Check the lease.
+ setLeaseCheck(CfgConsistency::LEASE_CHECK_FIX_DEL);
+ SanityChecker checker;
+ checker.checkLease(lease);
+
+ // Verify the lease is still here but was moved to the first and right subnet.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(subnet1->getID(), lease->subnet_id_);
+}
+
+// Verify whether sanity checker works as expected (no subnet v4).
+TEST_F(SanityChecksTest, noSubnet4) {
+ // Create network and lease in a wrong subnet.
+ CfgMgr::instance().setFamily(AF_INET);
+ Subnet4Ptr subnet = createSubnet4("192.168.2.0/24", 1);
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->add(subnet);
+ IOAddress addr("192.168.1.1");
+ Lease4Ptr lease = newLease4(addr, 1);
+
+ // Check the lease.
+ setLeaseCheck(CfgConsistency::LEASE_CHECK_FIX_DEL);
+ SanityChecker checker;
+ checker.checkLease(lease);
+
+ // Verify the lease was removed because its subnet does not exist,
+ EXPECT_FALSE(lease);
+}
+
+// Verify whether sanity checker works as expected (no subnet v6).
+TEST_F(SanityChecksTest, noSubnet6) {
+ // Create network and lease in a wrong subnet.
+ CfgMgr::instance().setFamily(AF_INET6);
+ Subnet6Ptr subnet = createSubnet6("2001:db8:2::/64", 1);
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->add(subnet);
+ IOAddress addr("2001:db8:1::1");
+ Lease6Ptr lease = newLease6(addr, 1);
+
+ // Check the lease.
+ setLeaseCheck(CfgConsistency::LEASE_CHECK_FIX_DEL);
+ SanityChecker checker;
+ checker.checkLease(lease);
+
+ // Verify the lease was removed because its subnet does not exist,
+ EXPECT_FALSE(lease);
+}
+
+// Verify whether sanity checker works as expected (guard v4).
+TEST_F(SanityChecksTest, guard4) {
+ // Create networks and lease in the first and guarded subnet.
+ CfgMgr::instance().setFamily(AF_INET);
+ Subnet4Ptr subnet1 = createSubnet4("192.168.1.0/24", 1);
+ subnet1->allowClientClass("foo");
+ Subnet4Ptr subnet2 = createSubnet4("192.168.1.100/24", 2);
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->add(subnet1);
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->add(subnet2);
+ IOAddress addr("192.168.1.1");
+ Lease4Ptr lease = newLease4(addr, 1);
+
+ // Check the lease.
+ setLeaseCheck(CfgConsistency::LEASE_CHECK_FIX_DEL);
+ SanityChecker checker;
+ checker.checkLease(lease);
+
+ // Verify the lease is still here and in the guarded subnet.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(subnet1->getID(), lease->subnet_id_);
+}
+
+// Verify whether sanity checker works as expected (guard v6).
+TEST_F(SanityChecksTest, guard6) {
+ // Create networks and lease in the first and guarded subnet.
+ CfgMgr::instance().setFamily(AF_INET6);
+ Subnet6Ptr subnet1 = createSubnet6("2001:db8:1::/64", 1);
+ subnet1->allowClientClass("foo");
+ Subnet6Ptr subnet2 = createSubnet6("2001:db8:2::100/64", 2);
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->add(subnet1);
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->add(subnet2);
+ IOAddress addr("2001:db8:1::1");
+ Lease6Ptr lease = newLease6(addr, 1);
+
+ // Check the lease.
+ setLeaseCheck(CfgConsistency::LEASE_CHECK_FIX_DEL);
+ SanityChecker checker;
+ checker.checkLease(lease);
+
+ // Verify the lease is still here and in the guarded subnet.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(subnet1->getID(), lease->subnet_id_);
+}
+
+// Verify whether sanity checker works as expected (guard only v4).
+TEST_F(SanityChecksTest, guardOnly4) {
+ // Create guarded network and lease.
+ CfgMgr::instance().setFamily(AF_INET);
+ Subnet4Ptr subnet = createSubnet4("192.168.1.0/24", 1);
+ subnet->allowClientClass("foo");
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->add(subnet);
+ IOAddress addr("192.168.1.1");
+ Lease4Ptr lease = newLease4(addr, 1);
+
+ // Check the lease.
+ setLeaseCheck(CfgConsistency::LEASE_CHECK_FIX_DEL);
+ SanityChecker checker;
+ checker.checkLease(lease);
+
+ // Verify the lease is still here in the same subnet.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(subnet->getID(), lease->subnet_id_);
+}
+
+// Verify whether sanity checker works as expected (valid v6).
+TEST_F(SanityChecksTest, guardOnly6) {
+ // Create guarded network and lease.
+ CfgMgr::instance().setFamily(AF_INET6);
+ Subnet6Ptr subnet = createSubnet6("2001:db8:1::/64", 1);
+ subnet->allowClientClass("foo");
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->add(subnet);
+ IOAddress addr("2001:db8:1::1");
+ Lease6Ptr lease = newLease6(addr, 1);
+
+ // Check the lease.
+ setLeaseCheck(CfgConsistency::LEASE_CHECK_FIX_DEL);
+ SanityChecker checker;
+ checker.checkLease(lease);
+
+ // Verify the lease is still here in the same subnet.
+ ASSERT_TRUE(lease);
+ EXPECT_EQ(subnet->getID(), lease->subnet_id_);
+}
+
diff --git a/src/lib/dhcpsrv/tests/shared_network_parser_unittest.cc b/src/lib/dhcpsrv/tests/shared_network_parser_unittest.cc
new file mode 100644
index 0000000..4af574a
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/shared_network_parser_unittest.cc
@@ -0,0 +1,1023 @@
+// Copyright (C) 2017-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/option.h>
+#include <dhcp/option4_addrlst.h>
+#include <dhcp/option6_addrlst.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfg_option.h>
+#include <dhcpsrv/parsers/shared_network_parser.h>
+#include <testutils/gtest_utils.h>
+#include <gtest/gtest.h>
+#include <string>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+
+namespace {
+class SharedNetworkParserTest : public ::testing::Test {
+public:
+
+ /// @brief Structure for describing a single relay test scenario
+ struct RelayTest {
+ /// @brief Description of the test scenario, used for logging
+ std::string description_;
+ /// @brief JSON configuration body of the "relay" element
+ std::string json_content_;
+ /// @brief indicates if parsing should pass or fail
+ bool should_parse_;
+ /// @brief list of addresses expected after parsing
+ IOAddressList addresses_;
+ };
+
+ /// @brief virtual destructor
+ virtual ~SharedNetworkParserTest(){};
+
+ /// @brief Fetch valid shared network configuration JSON text
+ virtual std::string getWorkingConfig() const = 0;
+ ElementPtr makeTestConfig(const std::string& name, const std::string& json_content) {
+ // Create working config element tree
+ ElementPtr config = Element::fromJSON(getWorkingConfig());
+
+ // Create test element contents
+ ElementPtr content = Element::fromJSON(json_content);
+
+ // Add the test element to working config
+ config->set(name, content);
+ return (config);
+ }
+
+ /// @brief Executes a single "relay" parsing scenario
+ ///
+ /// Each test pass consists of the following steps:
+ /// -# Attempt to parse the given JSON text
+ /// -# If parsing is expected to fail and it does return otherwise fatal fail
+ /// -# If parsing is expected to succeed, fatal fail if it does not
+ /// -# Verify the network's relay address list matches the expected list
+ /// in size and content.
+ ///
+ /// @param test RelayTest which describes the test to conduct
+ void relayTest(const RelayTest& test) {
+ ElementPtr test_config;
+ ASSERT_NO_THROW(test_config =
+ makeTestConfig("relay", test.json_content_));
+
+ // Init our ref to a place holder
+ Network4 dummy;
+ Network& network = dummy;
+
+ // If parsing should fail, call parse expecting a throw.
+ if (!test.should_parse_) {
+ ASSERT_THROW(network = parseIntoNetwork(test_config), DhcpConfigError);
+ // No throw so test outcome is correct, nothing else to do.
+ return;
+ }
+
+ // Should parse without error, let's see if it does.
+ ASSERT_NO_THROW(network = parseIntoNetwork(test_config));
+
+ // It parsed, are the number of entries correct?
+ ASSERT_EQ(test.addresses_.size(), network.getRelayAddresses().size());
+
+ // Are the expected addresses in the list?
+ for (auto exp_address = test.addresses_.begin(); exp_address != test.addresses_.end();
+ ++exp_address) {
+ EXPECT_TRUE(network.hasRelayAddress(*exp_address))
+ << " expected address: " << (*exp_address).toText() << " not found" ;
+ }
+ }
+
+ /// @brief Attempts to parse the given configuration into a shared network
+ ///
+ /// Virtual function used by relayTest() to parse a test configuration.
+ /// Implementation should not catch parsing exceptions.
+ ///
+ /// @param test_config JSON configuration text to parse
+ /// @return A reference to the Network created if parsing is successful
+ virtual Network& parseIntoNetwork(ConstElementPtr test_config) = 0;
+};
+
+
+/// @brief Test fixture class for SharedNetwork4Parser class.
+class SharedNetwork4ParserTest : public SharedNetworkParserTest {
+public:
+
+ /// @brief Creates valid shared network configuration.
+ ///
+ /// @return Valid shared network configuration.
+ virtual std::string getWorkingConfig() const {
+ std::string config = "{"
+ " \"authoritative\": true,"
+ " \"boot-file-name\": \"/dev/null\","
+ " \"client-class\": \"srv1\","
+ " \"interface\": \"eth1961\","
+ " \"match-client-id\": true,"
+ " \"name\": \"bird\","
+ " \"next-server\": \"10.0.0.1\","
+ " \"rebind-timer\": 199,"
+ " \"relay\": { \"ip-addresses\": [ \"10.1.1.1\" ] },"
+ " \"renew-timer\": 99,"
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": true,"
+ " \"server-hostname\": \"example.org\","
+ " \"require-client-classes\": [ \"runner\" ],"
+ " \"user-context\": { \"comment\": \"example\" },"
+ " \"valid-lifetime\": 399,"
+ " \"min-valid-lifetime\": 299,"
+ " \"max-valid-lifetime\": 499,"
+ " \"calculate-tee-times\": true,"
+ " \"t1-percent\": 0.345,"
+ " \"t2-percent\": 0.721,"
+ " \"ddns-send-updates\": true,"
+ " \"ddns-override-no-update\": true,"
+ " \"ddns-override-client-update\": true,"
+ " \"ddns-replace-client-name\": \"always\","
+ " \"ddns-generated-prefix\": \"prefix\","
+ " \"ddns-qualifying-suffix\": \"example.com.\","
+ " \"hostname-char-set\": \"[^A-Z]\","
+ " \"hostname-char-replacement\": \"x\","
+ " \"store-extended-info\": true,"
+ " \"cache-threshold\": 0.123,"
+ " \"cache-max-age\": 123,"
+ " \"ddns-update-on-renew\": true,"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"domain-name-servers\","
+ " \"data\": \"192.0.2.3\""
+ " }"
+ " ],"
+ " \"subnet4\": ["
+ " {"
+ " \"id\": 1,"
+ " \"subnet\": \"10.1.2.0/24\","
+ " \"interface\": \"\","
+ " \"renew-timer\": 100,"
+ " \"rebind-timer\": 200,"
+ " \"valid-lifetime\": 300,"
+ " \"min-valid-lifetime\": 200,"
+ " \"max-valid-lifetime\": 400,"
+ " \"match-client-id\": false,"
+ " \"authoritative\": false,"
+ " \"next-server\": \"\","
+ " \"server-hostname\": \"\","
+ " \"boot-file-name\": \"\","
+ " \"client-class\": \"\","
+ " \"require-client-classes\": []\n,"
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": false,"
+ " \"4o6-interface\": \"\","
+ " \"4o6-interface-id\": \"\","
+ " \"4o6-subnet\": \"\","
+ " \"calculate-tee-times\": true,"
+ " \"t1-percent\": .45,"
+ " \"t2-percent\": .65,"
+ " \"hostname-char-set\": \"\","
+ " \"cache-threshold\": .20,"
+ " \"cache-max-age\": 50"
+ " },"
+ " {"
+ " \"id\": 2,"
+ " \"subnet\": \"192.0.2.0/24\","
+ " \"interface\": \"\","
+ " \"renew-timer\": 10,"
+ " \"rebind-timer\": 20,"
+ " \"valid-lifetime\": 30,"
+ " \"match-client-id\": false,"
+ " \"authoritative\": false,"
+ " \"next-server\": \"\","
+ " \"server-hostname\": \"\","
+ " \"boot-file-name\": \"\","
+ " \"client-class\": \"\","
+ " \"require-client-classes\": []\n,"
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": false,"
+ " \"4o6-interface\": \"\","
+ " \"4o6-interface-id\": \"\","
+ " \"4o6-subnet\": \"\","
+ " \"calculate-tee-times\": false,"
+ " \"t1-percent\": .40,"
+ " \"t2-percent\": .80,"
+ " \"cache-threshold\": .15,"
+ " \"cache-max-age\": 5"
+ " }"
+ " ]"
+ "}";
+
+ return (config);
+ }
+
+ virtual Network& parseIntoNetwork(ConstElementPtr test_config) {
+ // Parse configuration.
+ SharedNetwork4Parser parser;
+ network_ = parser.parse(test_config);
+ return (*network_);
+ }
+
+private:
+ SharedNetwork4Ptr network_;
+};
+
+// This test verifies that shared network parser for IPv4 works properly
+// in a positive test scenario.
+TEST_F(SharedNetwork4ParserTest, parse) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ // Basic configuration for shared network. A bunch of parameters
+ // have to be specified for subnets because subnet parsers expect
+ // that default and global values are set.
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse configuration specified above.
+ SharedNetwork4Parser parser;
+ SharedNetwork4Ptr network;
+
+ ASSERT_NO_THROW_LOG(network = parser.parse(config_element));
+ ASSERT_TRUE(network);
+
+ // Check basic parameters.
+ EXPECT_TRUE(network->getAuthoritative());
+ EXPECT_EQ("srv1", network->getClientClass().get());
+ EXPECT_EQ("bird", network->getName());
+ EXPECT_EQ("eth1961", network->getIface().get());
+ EXPECT_EQ(99, network->getT1());
+ EXPECT_EQ(199, network->getT2());
+ EXPECT_EQ(399, network->getValid());
+ EXPECT_EQ(299, network->getValid().getMin());
+ EXPECT_EQ(499, network->getValid().getMax());
+ EXPECT_TRUE(network->getCalculateTeeTimes());
+ EXPECT_EQ(0.345, network->getT1Percent());
+ EXPECT_EQ(0.721, network->getT2Percent());
+ EXPECT_EQ("/dev/null", network->getFilename().get());
+ EXPECT_EQ("10.0.0.1", network->getSiaddr().get().toText());
+ EXPECT_EQ("example.org", network->getSname().get());
+ EXPECT_FALSE(network->getReservationsGlobal());
+ EXPECT_TRUE(network->getReservationsInSubnet());
+ EXPECT_TRUE(network->getReservationsOutOfPool());
+ EXPECT_TRUE(network->getDdnsSendUpdates().get());
+ EXPECT_TRUE(network->getDdnsOverrideNoUpdate().get());
+ EXPECT_TRUE(network->getDdnsOverrideClientUpdate().get());
+ EXPECT_EQ(D2ClientConfig::RCM_ALWAYS, network->getDdnsReplaceClientNameMode().get());
+ EXPECT_EQ("prefix", network->getDdnsGeneratedPrefix().get());
+ EXPECT_EQ("example.com.", network->getDdnsQualifyingSuffix().get());
+ EXPECT_EQ("[^A-Z]", network->getHostnameCharSet().get());
+ EXPECT_EQ("x", network->getHostnameCharReplacement().get());
+ EXPECT_TRUE(network->getStoreExtendedInfo().get());
+ EXPECT_EQ(0.123, network->getCacheThreshold());
+ EXPECT_EQ(123, network->getCacheMaxAge());
+ EXPECT_TRUE(network->getDdnsUpdateOnRenew().get());
+
+ // Relay information.
+ auto relay_info = network->getRelayInfo();
+ EXPECT_EQ(1, relay_info.getAddresses().size());
+ EXPECT_TRUE(relay_info.containsAddress(IOAddress("10.1.1.1")));
+
+ // Required client classes.
+ auto required = network->getRequiredClasses();
+ ASSERT_EQ(1, required.size());
+ EXPECT_EQ("runner", *required.cbegin());
+
+ // Check user context.
+ ConstElementPtr context = network->getContext();
+ ASSERT_TRUE(context);
+ EXPECT_TRUE(context->get("comment"));
+
+ // Subnet with id 1
+ Subnet4Ptr subnet = network->getSubnet(SubnetID(1));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ("10.1.2.0", subnet->get().first.toText());
+ EXPECT_EQ(300, subnet->getValid());
+ EXPECT_EQ(200, subnet->getValid().getMin());
+ EXPECT_EQ(400, subnet->getValid().getMax());
+ EXPECT_FALSE(subnet->getHostnameCharSet().unspecified());
+ EXPECT_EQ("", subnet->getHostnameCharSet().get());
+
+ // Subnet with id 2
+ subnet = network->getSubnet(SubnetID(2));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ("192.0.2.0", subnet->get().first.toText());
+ EXPECT_EQ(30, subnet->getValid());
+ EXPECT_EQ(30, subnet->getValid().getMin());
+ EXPECT_EQ(30, subnet->getValid().getMax());
+ EXPECT_EQ("[^A-Z]", subnet->getHostnameCharSet().get());
+ EXPECT_EQ("x", subnet->getHostnameCharReplacement().get());
+
+ // DHCP options
+ ConstCfgOptionPtr cfg_option = network->getCfgOption();
+ ASSERT_TRUE(cfg_option);
+ OptionDescriptor opt_dns_servers = cfg_option->get("dhcp4",
+ DHO_DOMAIN_NAME_SERVERS);
+ ASSERT_TRUE(opt_dns_servers.option_);
+ Option4AddrLstPtr dns_servers = boost::dynamic_pointer_cast<
+ Option4AddrLst>(opt_dns_servers.option_);
+ ASSERT_TRUE(dns_servers);
+ Option4AddrLst::AddressContainer addresses = dns_servers->getAddresses();
+ ASSERT_EQ(1, addresses.size());
+ EXPECT_EQ("192.0.2.3", addresses[0].toText());
+}
+
+// This test verifies that parser throws an exception when mandatory parameter
+// "name" is not specified.
+TEST_F(SharedNetwork4ParserTest, missingName) {
+ // Remove a name parameter from the valid configuration.
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+ ASSERT_NO_THROW(config_element->remove("name"));
+
+ // Parse configuration specified above.
+ SharedNetwork4Parser parser;
+ SharedNetwork4Ptr network;
+ ASSERT_THROW(network = parser.parse(config_element), DhcpConfigError);
+}
+
+// This test verifies that it's possible to specify client-class,
+// match-client-id, and authoritative on shared-network level.
+TEST_F(SharedNetwork4ParserTest, clientClassMatchClientIdAuthoritative) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+
+ config_element->set("authoritative", Element::create(true));
+ config_element->set("match-client-id", Element::create(false));
+ config_element->set("client-class", Element::create("alpha"));
+
+ // Parse configuration specified above.
+ SharedNetwork4Parser parser;
+ SharedNetwork4Ptr network;
+ network = parser.parse(config_element);
+ ASSERT_TRUE(network);
+
+ EXPECT_EQ("alpha", network->getClientClass().get());
+
+ EXPECT_FALSE(network->getMatchClientId());
+
+ EXPECT_TRUE(network->getAuthoritative());
+}
+
+// This test verifies that parsing of the "relay" element.
+// It checks both valid and invalid scenarios.
+TEST_F(SharedNetwork4ParserTest, relayInfoTests) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ // Create the vector of test scenarios.
+ std::vector<RelayTest> tests = {
+ {
+ "valid ip-address #1",
+ "{ \"ip-address\": \"192.168.2.1\" }",
+ true,
+ { asiolink::IOAddress("192.168.2.1") }
+ },
+ {
+ "invalid ip-address #1",
+ "{ \"ip-address\": \"not an address\" }",
+ false,
+ { }
+ },
+ {
+ "invalid ip-address #2",
+ "{ \"ip-address\": \"2001:db8::1\" }",
+ false,
+ { }
+ },
+ {
+ "valid ip-addresses #1",
+ "{ \"ip-addresses\": [ ] }",
+ true,
+ {}
+ },
+ {
+ "valid ip-addresses #2",
+ "{ \"ip-addresses\": [ \"192.168.2.1\" ] }",
+ true,
+ { asiolink::IOAddress("192.168.2.1") }
+ },
+ {
+ "valid ip-addresses #3",
+ "{ \"ip-addresses\": [ \"192.168.2.1\", \"192.168.2.2\" ] }",
+ true,
+ { asiolink::IOAddress("192.168.2.1"), asiolink::IOAddress("192.168.2.2") }
+ },
+ {
+ "invalid ip-addresses #1",
+ "{ \"ip-addresses\": [ \"not an address\" ] }",
+ false,
+ { }
+ },
+ {
+ "invalid ip-addresses #2",
+ "{ \"ip-addresses\": [ \"2001:db8::1\" ] }",
+ false,
+ { }
+ },
+ {
+ "invalid both ip-address and ip-addresses",
+ "{"
+ " \"ip-address\": \"192.168.2.1\", "
+ " \"ip-addresses\": [ \"192.168.2.1\", \"192.168.2.2\" ]"
+ " }",
+ false,
+ { }
+ },
+ {
+ "invalid neither ip-address nor ip-addresses",
+ "{}",
+ false,
+ { }
+ }
+ };
+
+ // Iterate over the test scenarios, verifying each prescribed
+ // outcome.
+ for (auto test = tests.begin(); test != tests.end(); ++test) {
+ {
+ SCOPED_TRACE((*test).description_);
+ relayTest(*test);
+ }
+ }
+}
+
+// This test verifies that the optional interface check works as expected.
+TEST_F(SharedNetwork4ParserTest, iface) {
+ // Basic configuration for shared network.
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse configuration specified above.
+
+ // The interface check can be disabled.
+ SharedNetwork4Parser parser_no_check(false);
+ SharedNetwork4Ptr network;
+ EXPECT_NO_THROW(network = parser_no_check.parse(config_element));
+ ASSERT_TRUE(network);
+ EXPECT_FALSE(network->getIface().unspecified());
+ EXPECT_EQ("eth1961", network->getIface().get());
+
+ // Retry with the interface check enabled.
+ SharedNetwork4Parser parser;
+ EXPECT_THROW(parser.parse(config_element), DhcpConfigError);
+
+ // Configure default test interfaces.
+ IfaceMgrTestConfig ifmgr(true);
+
+ EXPECT_NO_THROW(network = parser_no_check.parse(config_element));
+ ASSERT_TRUE(network);
+ EXPECT_FALSE(network->getIface().unspecified());
+ EXPECT_EQ("eth1961", network->getIface().get());
+
+ EXPECT_NO_THROW(network = parser.parse(config_element));
+ ASSERT_TRUE(network);
+ EXPECT_FALSE(network->getIface().unspecified());
+ EXPECT_EQ("eth1961", network->getIface().get());
+}
+
+// This test verifies that shared network parser for IPv4 works properly
+// when using invalid renew and rebind timers.
+TEST_F(SharedNetwork4ParserTest, parseWithInvalidRenewRebind) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ // Basic configuration for shared network.
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+ ConstElementPtr valid_element = config_element->get("rebind-timer");
+ int64_t value = valid_element->intValue();
+ valid_element = config_element->get("renew-timer");
+ ElementPtr mutable_element = boost::const_pointer_cast<Element>(valid_element);
+ mutable_element->setValue(value + 1);
+
+ // Parse configuration specified above.
+ SharedNetwork4Parser parser;
+ SharedNetwork4Ptr network;
+
+ ASSERT_THROW(network = parser.parse(config_element), DhcpConfigError);
+ ASSERT_FALSE(network);
+}
+
+// This test verifies that shared network parser for IPv4 works properly
+// when renew and rebind timers are equal.
+TEST_F(SharedNetwork4ParserTest, parseValidWithEqualRenewRebind) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ // Basic configuration for shared network.
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+ ConstElementPtr valid_element = config_element->get("rebind-timer");
+ int64_t value = valid_element->intValue();
+ valid_element = config_element->get("renew-timer");
+ ElementPtr mutable_element = boost::const_pointer_cast<Element>(valid_element);
+ mutable_element->setValue(value);
+
+ // Parse configuration specified above.
+ SharedNetwork4Parser parser;
+ SharedNetwork4Ptr network;
+
+ ASSERT_NO_THROW(network = parser.parse(config_element));
+ ASSERT_TRUE(network);
+}
+
+/// @brief Test fixture class for SharedNetwork6Parser class.
+class SharedNetwork6ParserTest : public SharedNetworkParserTest {
+public:
+
+ /// @brief Constructor.
+ SharedNetwork6ParserTest()
+ : SharedNetworkParserTest(), network_(), use_iface_id_(false) {
+ }
+
+ /// @brief Creates valid shared network configuration.
+ ///
+ /// @return Valid shared network configuration.
+ virtual std::string getWorkingConfig() const {
+ std::string config = "{"
+ " \"client-class\": \"srv1\","
+ + std::string(use_iface_id_ ? "\"interface-id\": " : "\"interface\": ") +
+ "\"eth1961\","
+ " \"name\": \"bird\","
+ " \"preferred-lifetime\": 211,"
+ " \"min-preferred-lifetime\": 111,"
+ " \"max-preferred-lifetime\": 311,"
+ " \"rapid-commit\": true,"
+ " \"rebind-timer\": 199,"
+ " \"relay\": { \"ip-addresses\": [ \"2001:db8:1::1\" ] },"
+ " \"renew-timer\": 99,"
+ " \"require-client-classes\": [ \"runner\" ],"
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": true,"
+ " \"user-context\": { },"
+ " \"valid-lifetime\": 399,"
+ " \"min-valid-lifetime\": 299,"
+ " \"max-valid-lifetime\": 499,"
+ " \"calculate-tee-times\": true,"
+ " \"t1-percent\": 0.345,"
+ " \"t2-percent\": 0.721,"
+ " \"ddns-send-updates\": true,"
+ " \"ddns-override-no-update\": true,"
+ " \"ddns-override-client-update\": true,"
+ " \"ddns-replace-client-name\": \"always\","
+ " \"ddns-generated-prefix\": \"prefix\","
+ " \"ddns-qualifying-suffix\": \"example.com.\","
+ " \"hostname-char-set\": \"[^A-Z]\","
+ " \"hostname-char-replacement\": \"x\","
+ " \"store-extended-info\": true,"
+ " \"cache-threshold\": 0.123,"
+ " \"cache-max-age\": 123,"
+ " \"ddns-update-on-renew\": true,"
+ " \"option-data\": ["
+ " {"
+ " \"name\": \"dns-servers\","
+ " \"data\": \"2001:db8:1::cafe\""
+ " }"
+ " ],"
+ " \"subnet6\": ["
+ " {"
+ " \"id\": 1,"
+ " \"subnet\": \"3000::/16\","
+ " \"interface\": \"\","
+ " \"interface-id\": \"\","
+ " \"renew-timer\": 100,"
+ " \"rebind-timer\": 200,"
+ " \"preferred-lifetime\": 300,"
+ " \"min-preferred-lifetime\": 200,"
+ " \"max-preferred-lifetime\": 400,"
+ " \"valid-lifetime\": 400,"
+ " \"min-valid-lifetime\": 300,"
+ " \"max-valid-lifetime\": 500,"
+ " \"client-class\": \"\","
+ " \"require-client-classes\": []\n,"
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": false,"
+ " \"rapid-commit\": false,"
+ " \"hostname-char-set\": \"\""
+ " },"
+ " {"
+ " \"id\": 2,"
+ " \"subnet\": \"2001:db8:1::/64\","
+ " \"interface\": \"\","
+ " \"interface-id\": \"\","
+ " \"renew-timer\": 10,"
+ " \"rebind-timer\": 20,"
+ " \"preferred-lifetime\": 30,"
+ " \"valid-lifetime\": 40,"
+ " \"client-class\": \"\","
+ " \"require-client-classes\": []\n,"
+ " \"reservations-global\": false,"
+ " \"reservations-in-subnet\": true,"
+ " \"reservations-out-of-pool\": false,"
+ " \"rapid-commit\": false"
+ " }"
+ " ]"
+ "}";
+
+ return (config);
+ }
+
+ virtual Network& parseIntoNetwork(ConstElementPtr test_config) {
+ // Parse configuration.
+ SharedNetwork6Parser parser;
+ network_ = parser.parse(test_config);
+ return (*network_);
+ }
+
+public:
+
+ SharedNetwork6Ptr network_;
+
+ /// Boolean flag indicating if the interface-id should be used instead
+ /// of interface.
+ bool use_iface_id_;
+};
+
+// This test verifies that shared network parser for IPv6 works properly
+// in a positive test scenario.
+TEST_F(SharedNetwork6ParserTest, parse) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ // Basic configuration for shared network. A bunch of parameters
+ // have to be specified for subnets because subnet parsers expect
+ // that default and global values are set.
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse configuration specified above.
+ SharedNetwork6Parser parser;
+ SharedNetwork6Ptr network;
+ ASSERT_NO_THROW(network = parser.parse(config_element));
+ ASSERT_TRUE(network);
+
+ // Check basic parameters.
+ EXPECT_EQ("srv1", network->getClientClass().get());
+ EXPECT_EQ("bird", network->getName());
+ EXPECT_EQ("eth1961", network->getIface().get());
+ EXPECT_EQ(211, network->getPreferred());
+ EXPECT_EQ(111, network->getPreferred().getMin());
+ EXPECT_EQ(311, network->getPreferred().getMax());
+ EXPECT_TRUE(network->getRapidCommit());
+ EXPECT_EQ(99, network->getT1());
+ EXPECT_EQ(199, network->getT2());
+ EXPECT_EQ(399, network->getValid());
+ EXPECT_EQ(299, network->getValid().getMin());
+ EXPECT_EQ(499, network->getValid().getMax());
+ EXPECT_TRUE(network->getCalculateTeeTimes());
+ EXPECT_EQ(0.345, network->getT1Percent());
+ EXPECT_EQ(0.721, network->getT2Percent());
+ EXPECT_TRUE(network->getDdnsSendUpdates().get());
+ EXPECT_TRUE(network->getDdnsOverrideNoUpdate().get());
+ EXPECT_TRUE(network->getDdnsOverrideClientUpdate().get());
+ EXPECT_EQ(D2ClientConfig::RCM_ALWAYS, network->getDdnsReplaceClientNameMode().get());
+ EXPECT_EQ("prefix", network->getDdnsGeneratedPrefix().get());
+ EXPECT_EQ("example.com.", network->getDdnsQualifyingSuffix().get());
+ EXPECT_EQ("[^A-Z]", network->getHostnameCharSet().get());
+ EXPECT_EQ("x", network->getHostnameCharReplacement().get());
+ EXPECT_TRUE(network->getStoreExtendedInfo().get());
+ EXPECT_EQ(0.123, network->getCacheThreshold());
+ EXPECT_EQ(123, network->getCacheMaxAge());
+ EXPECT_TRUE(network->getDdnsUpdateOnRenew().get());
+
+ // Relay information.
+ auto relay_info = network->getRelayInfo();
+ EXPECT_EQ(1, relay_info.getAddresses().size());
+ EXPECT_TRUE(relay_info.containsAddress(IOAddress("2001:db8:1::1")));
+
+ // Required client classes.
+ auto required = network->getRequiredClasses();
+ ASSERT_EQ(1, required.size());
+ EXPECT_EQ("runner", *required.cbegin());
+
+ // Check user context.
+ ConstElementPtr context = network->getContext();
+ ASSERT_TRUE(context);
+ EXPECT_EQ(0, context->size());
+
+ // Subnet with id 1
+ Subnet6Ptr subnet = network->getSubnet(SubnetID(1));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ("3000::", subnet->get().first.toText());
+ EXPECT_EQ(300, subnet->getPreferred());
+ EXPECT_EQ(200, subnet->getPreferred().getMin());
+ EXPECT_EQ(400, subnet->getPreferred().getMax());
+ EXPECT_EQ(400, subnet->getValid());
+ EXPECT_EQ(300, subnet->getValid().getMin());
+ EXPECT_EQ(500, subnet->getValid().getMax());
+ EXPECT_FALSE(subnet->getHostnameCharSet().unspecified());
+ EXPECT_EQ("", subnet->getHostnameCharSet().get());
+
+ // Subnet with id 2
+ subnet = network->getSubnet(SubnetID(2));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ("2001:db8:1::", subnet->get().first.toText());
+ EXPECT_EQ(30, subnet->getPreferred());
+ EXPECT_EQ(30, subnet->getPreferred().getMin());
+ EXPECT_EQ(30, subnet->getPreferred().getMax());
+ EXPECT_EQ(40, subnet->getValid());
+ EXPECT_EQ(40, subnet->getValid().getMin());
+ EXPECT_EQ(40, subnet->getValid().getMax());
+ EXPECT_EQ("[^A-Z]", subnet->getHostnameCharSet().get());
+ EXPECT_EQ("x", subnet->getHostnameCharReplacement().get());
+
+ // DHCP options
+ ConstCfgOptionPtr cfg_option = network->getCfgOption();
+ ASSERT_TRUE(cfg_option);
+ OptionDescriptor opt_dns_servers = cfg_option->get("dhcp6",
+ D6O_NAME_SERVERS);
+ ASSERT_TRUE(opt_dns_servers.option_);
+ Option6AddrLstPtr dns_servers = boost::dynamic_pointer_cast<
+ Option6AddrLst>(opt_dns_servers.option_);
+ ASSERT_TRUE(dns_servers);
+ Option6AddrLst::AddressContainer addresses = dns_servers->getAddresses();
+ ASSERT_EQ(1, addresses.size());
+ EXPECT_EQ("2001:db8:1::cafe", addresses[0].toText());
+}
+
+// This test verifies that shared network parser for IPv6 works properly
+// in a positive test scenario.
+TEST_F(SharedNetwork6ParserTest, parseWithInterfaceId) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ // Use the configuration with interface-id instead of interface parameter.
+ use_iface_id_ = true;
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse configuration specified above.
+ SharedNetwork6Parser parser;
+ SharedNetwork6Ptr network;
+ ASSERT_NO_THROW(network = parser.parse(config_element));
+ ASSERT_TRUE(network);
+
+ // Check that interface-id has been parsed.
+ auto opt_iface_id = network->getInterfaceId();
+ ASSERT_TRUE(opt_iface_id);
+}
+
+// This test verifies that shared network parser for IPv6 works properly
+// when using invalid renew and rebind timers.
+TEST_F(SharedNetwork6ParserTest, parseWithInvalidRenewRebind) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ // Use the configuration with interface-id instead of interface parameter.
+ use_iface_id_ = true;
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+ ConstElementPtr valid_element = config_element->get("rebind-timer");
+ int64_t value = valid_element->intValue();
+ valid_element = config_element->get("renew-timer");
+ ElementPtr mutable_element = boost::const_pointer_cast<Element>(valid_element);
+ mutable_element->setValue(value + 1);
+
+ // Parse configuration specified above.
+ SharedNetwork6Parser parser;
+ SharedNetwork6Ptr network;
+
+ ASSERT_THROW(network = parser.parse(config_element), DhcpConfigError);
+ ASSERT_FALSE(network);
+}
+
+// This test verifies that shared network parser for IPv6 works properly
+// when renew and rebind timers are equal.
+TEST_F(SharedNetwork6ParserTest, parseValidWithEqualRenewRebind) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ // Use the configuration with interface-id instead of interface parameter.
+ use_iface_id_ = true;
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+ ConstElementPtr valid_element = config_element->get("rebind-timer");
+ int64_t value = valid_element->intValue();
+ valid_element = config_element->get("renew-timer");
+ ElementPtr mutable_element = boost::const_pointer_cast<Element>(valid_element);
+ mutable_element->setValue(value);
+
+ // Parse configuration specified above.
+ SharedNetwork6Parser parser;
+ SharedNetwork6Ptr network;
+
+ ASSERT_NO_THROW(network = parser.parse(config_element));
+ ASSERT_TRUE(network);
+}
+
+// This test verifies that error is returned when trying to configure a
+// shared network with both interface and interface id.
+TEST_F(SharedNetwork6ParserTest, mutuallyExclusiveInterfaceId) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ // Use the configuration with interface-id instead of interface parameter.
+ use_iface_id_ = true;
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Add interface which is mutually exclusive with interface-id
+ config_element->set("interface", Element::create("eth1"));
+
+ // Parse configuration specified above.
+ SharedNetwork6Parser parser;
+ EXPECT_THROW(parser.parse(config_element), DhcpConfigError);
+}
+
+// This test verifies that it's possible to specify client-class
+// on shared-network level.
+TEST_F(SharedNetwork6ParserTest, clientClass) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+
+ config_element->set("client-class", Element::create("alpha"));
+
+ // Parse configuration specified above.
+ SharedNetwork6Parser parser;
+ SharedNetwork6Ptr network;
+ network = parser.parse(config_element);
+ ASSERT_TRUE(network);
+
+ EXPECT_EQ("alpha", network->getClientClass().get());
+}
+
+// This test verifies that it's possible to specify require-client-classes
+// on shared-network level.
+TEST_F(SharedNetwork6ParserTest, evalClientClasses) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+
+ ElementPtr class_list = Element::createList();
+ class_list->add(Element::create("alpha"));
+ class_list->add(Element::create("beta"));
+ config_element->set("require-client-classes", class_list);
+
+ // Parse configuration specified above.
+ SharedNetwork6Parser parser;
+ SharedNetwork6Ptr network;
+ network = parser.parse(config_element);
+ ASSERT_TRUE(network);
+
+ const ClientClasses& classes = network->getRequiredClasses();
+ EXPECT_EQ(2, classes.size());
+ EXPECT_EQ("alpha, beta", classes.toText());
+}
+
+// This test verifies that bad require-client-classes configs raise
+// expected errors.
+TEST_F(SharedNetwork6ParserTest, badEvalClientClasses) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Element of the list must be strings.
+ ElementPtr class_list = Element::createList();
+ class_list->add(Element::create("alpha"));
+ class_list->add(Element::create(1234));
+ config_element->set("require-client-classes", class_list);
+
+ // Parse configuration specified above.
+ SharedNetwork6Parser parser;
+ SharedNetwork6Ptr network;
+ EXPECT_THROW(network = parser.parse(config_element), DhcpConfigError);
+
+ // Empty class name is forbidden.
+ class_list = Element::createList();
+ class_list->add(Element::create("alpha"));
+ class_list->add(Element::create(""));
+ EXPECT_THROW(network = parser.parse(config_element), DhcpConfigError);
+
+ // And of course the list must be a list even the parser can only
+ // trigger the previous error case...
+ class_list = Element::createMap();
+ EXPECT_THROW(network = parser.parse(config_element), DhcpConfigError);
+}
+
+// This test verifies that v6 parsing of the "relay" element.
+// It checks both valid and invalid scenarios.
+TEST_F(SharedNetwork6ParserTest, relayInfoTests) {
+ IfaceMgrTestConfig ifmgr(true);
+
+
+ // Create the vector of test scenarios.
+ std::vector<RelayTest> tests = {
+ {
+ "valid ip-address #1",
+ "{ \"ip-address\": \"2001:db8::1\" }",
+ true,
+ { asiolink::IOAddress("2001:db8::1") }
+ },
+ {
+ "invalid ip-address #1",
+ "{ \"ip-address\": \"not an address\" }",
+ false,
+ { }
+ },
+ {
+ "invalid ip-address #2",
+ "{ \"ip-address\": \"192.168.2.1\" }",
+ false,
+ { }
+ },
+ {
+ "valid ip-addresses #1",
+ "{ \"ip-addresses\": [ ] }",
+ true,
+ {}
+ },
+ {
+ "valid ip-addresses #2",
+ "{ \"ip-addresses\": [ \"2001:db8::1\" ] }",
+ true,
+ { asiolink::IOAddress("2001:db8::1") }
+ },
+ {
+ "valid ip-addresses #3",
+ "{ \"ip-addresses\": [ \"2001:db8::1\", \"2001:db8::2\" ] }",
+ true,
+ { asiolink::IOAddress("2001:db8::1"), asiolink::IOAddress("2001:db8::2") }
+ },
+ {
+ "invalid ip-addresses #1",
+ "{ \"ip-addresses\": [ \"not an address\" ] }",
+ false,
+ { }
+ },
+ {
+ "invalid ip-addresses #2",
+ "{ \"ip-addresses\": [ \"192.168.1.1\" ] }",
+ false,
+ { }
+ },
+ {
+ "invalid both ip-address and ip-addresses",
+ "{"
+ " \"ip-address\": \"2001:db8::1\", "
+ " \"ip-addresses\": [ \"2001:db8::1\", \"2001:db8::2\" ]"
+ " }",
+ false,
+ { }
+ },
+ {
+ "invalid neither ip-address nor ip-addresses",
+ "{}",
+ false,
+ { }
+ }
+ };
+
+ // Iterate over the test scenarios, verifying each prescribed
+ // outcome.
+ for (auto test = tests.begin(); test != tests.end(); ++test) {
+ {
+ SCOPED_TRACE((*test).description_);
+ relayTest(*test);
+ }
+ }
+}
+
+// This test verifies that the optional interface check works as expected.
+TEST_F(SharedNetwork6ParserTest, iface) {
+ // Basic configuration for shared network.
+ std::string config = getWorkingConfig();
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // Parse configuration specified above.
+
+ // The interface check can be disabled.
+ SharedNetwork6Parser parser_no_check(false);
+ SharedNetwork6Ptr network;
+ EXPECT_NO_THROW(network = parser_no_check.parse(config_element));
+ ASSERT_TRUE(network);
+ EXPECT_FALSE(network->getIface().unspecified());
+ EXPECT_EQ("eth1961", network->getIface().get());
+
+ // Retry with the interface check enabled.
+ SharedNetwork6Parser parser;
+ EXPECT_THROW(parser.parse(config_element), DhcpConfigError);
+
+ // Configure default test interfaces.
+ IfaceMgrTestConfig ifmgr(true);
+
+ EXPECT_NO_THROW(network = parser_no_check.parse(config_element));
+ ASSERT_TRUE(network);
+ EXPECT_FALSE(network->getIface().unspecified());
+ EXPECT_EQ("eth1961", network->getIface().get());
+
+ EXPECT_NO_THROW(network = parser.parse(config_element));
+ ASSERT_TRUE(network);
+ EXPECT_FALSE(network->getIface().unspecified());
+ EXPECT_EQ("eth1961", network->getIface().get());
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/shared_network_unittest.cc b/src/lib/dhcpsrv/tests/shared_network_unittest.cc
new file mode 100644
index 0000000..f9378e7
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/shared_network_unittest.cc
@@ -0,0 +1,1527 @@
+// Copyright (C) 2017-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/option.h>
+#include <dhcpsrv/shared_network.h>
+#include <dhcpsrv/subnet.h>
+#include <dhcpsrv/subnet_id.h>
+#include <util/triplet.h>
+#include <exceptions/exceptions.h>
+#include <testutils/test_to_element.h>
+#include <testutils/multi_threading_utils.h>
+
+#include <gtest/gtest.h>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::test;
+
+namespace {
+
+// This test verifies that the SharedNetwork4 factory function creates a
+// valid shared network instance.
+TEST(SharedNetwork4Test, create) {
+ auto network = SharedNetwork4::create("frog");
+ ASSERT_TRUE(network);
+ EXPECT_EQ("frog", network->getName());
+}
+
+// This test verifies the default values set for the shared
+// networks and verifies that the optional values are unspecified.
+TEST(SharedNetwork4Test, defaults) {
+ SharedNetwork4Ptr network(new SharedNetwork4("frog"));
+ EXPECT_TRUE(network->getIface().unspecified());
+ EXPECT_TRUE(network->getIface().empty());
+
+ EXPECT_TRUE(network->getClientClass().unspecified());
+ EXPECT_TRUE(network->getClientClass().empty());
+
+ EXPECT_TRUE(network->getValid().unspecified());
+ EXPECT_EQ(0, network->getValid().get());
+
+ EXPECT_TRUE(network->getT1().unspecified());
+ EXPECT_EQ(0, network->getT1().get());
+
+ EXPECT_TRUE(network->getT2().unspecified());
+ EXPECT_EQ(0, network->getT2().get());
+
+ EXPECT_TRUE(network->getReservationsGlobal().unspecified());
+ EXPECT_FALSE(network->getReservationsGlobal().get());
+
+ EXPECT_TRUE(network->getReservationsInSubnet().unspecified());
+ EXPECT_TRUE(network->getReservationsInSubnet().get());
+
+ EXPECT_TRUE(network->getReservationsOutOfPool().unspecified());
+ EXPECT_FALSE(network->getReservationsOutOfPool().get());
+
+ EXPECT_TRUE(network->getCalculateTeeTimes().unspecified());
+ EXPECT_FALSE(network->getCalculateTeeTimes().get());
+
+ EXPECT_TRUE(network->getT1Percent().unspecified());
+ EXPECT_EQ(0.0, network->getT1Percent().get());
+
+ EXPECT_TRUE(network->getT2Percent().unspecified());
+ EXPECT_EQ(0.0, network->getT2Percent().get());
+
+ EXPECT_TRUE(network->getMatchClientId().unspecified());
+ EXPECT_TRUE(network->getMatchClientId().get());
+
+ EXPECT_TRUE(network->getAuthoritative().unspecified());
+ EXPECT_FALSE(network->getAuthoritative().get());
+
+ EXPECT_TRUE(network->getDdnsSendUpdates().unspecified());
+ EXPECT_FALSE(network->getDdnsSendUpdates().get());
+
+ EXPECT_TRUE(network->getDdnsOverrideNoUpdate().unspecified());
+ EXPECT_FALSE(network->getDdnsOverrideNoUpdate().get());
+
+ EXPECT_TRUE(network->getDdnsOverrideClientUpdate().unspecified());
+ EXPECT_FALSE(network->getDdnsOverrideClientUpdate().get());
+
+ EXPECT_TRUE(network->getDdnsReplaceClientNameMode().unspecified());
+ EXPECT_EQ(D2ClientConfig::RCM_NEVER, network->getDdnsReplaceClientNameMode().get());
+
+ EXPECT_TRUE(network->getDdnsGeneratedPrefix().unspecified());
+ EXPECT_TRUE(network->getDdnsGeneratedPrefix().empty());
+
+ EXPECT_TRUE(network->getDdnsQualifyingSuffix().unspecified());
+ EXPECT_TRUE(network->getDdnsQualifyingSuffix().empty());
+
+ EXPECT_TRUE(network->getHostnameCharSet().unspecified());
+ EXPECT_TRUE(network->getHostnameCharSet().empty());
+
+ EXPECT_TRUE(network->getHostnameCharReplacement().unspecified());
+ EXPECT_TRUE(network->getHostnameCharReplacement().empty());
+
+ EXPECT_TRUE(network->getDdnsUpdateOnRenew().unspecified());
+ EXPECT_FALSE(network->getDdnsSendUpdates().get());
+}
+
+// This test verifies that shared network can be given a name and that
+// this name can be retrieved.
+TEST(SharedNetwork4Test, getName) {
+ // Create shared network with an initial name "dog".
+ SharedNetwork4Ptr network(new SharedNetwork4("frog"));
+ EXPECT_EQ("frog", network->getName());
+
+ // Override the name.
+ network->setName("dog");
+ EXPECT_EQ("dog", network->getName());
+}
+
+// This test verifies that an IPv4 subnet can be added to a shared network.
+// It also verifies that two subnets with the same ID can't be added to
+// a shared network and that a single subnet can't be added to two different
+// shared subnets.
+TEST(SharedNetwork4Test, addSubnet4) {
+ // First, create a network.
+ SharedNetwork4Ptr network(new SharedNetwork4("frog"));
+
+ // Try to add null pointer. It should throw.
+ Subnet4Ptr subnet;
+ ASSERT_THROW(network->add(subnet), BadValue);
+
+ // Create a valid subnet. It should now be added successfully.
+ subnet.reset(new Subnet4(IOAddress("10.0.0.0"), 8, 10, 20, 30,
+ SubnetID(15)));
+ ASSERT_NO_THROW(network->add(subnet));
+ ASSERT_EQ(1, network->getAllSubnets()->size());
+
+ // Retrieve the subnet from the network and make sure it is returned
+ // as expected.
+ ASSERT_FALSE(network->getAllSubnets()->empty());
+ Subnet4Ptr returned_subnet = *network->getAllSubnets()->begin();
+ ASSERT_TRUE(returned_subnet);
+ EXPECT_EQ(subnet->getID(), returned_subnet->getID());
+ SharedNetwork4Ptr network1;
+ subnet->getSharedNetwork(network1);
+ ASSERT_TRUE(network1);
+ EXPECT_TRUE(network1 == network);
+
+ // Create another subnet with the same ID. Adding a network with the
+ // same ID should cause an error.
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"), 24, 10, 20, 30,
+ SubnetID(15)));
+ ASSERT_THROW(network->add(subnet2), DuplicateSubnetID);
+
+ // Create another subnet with the same prefix. Adding a network with the
+ // same prefix should cause an error.
+ subnet2.reset(new Subnet4(IOAddress("10.0.0.0"), 8, 10, 20, 30,
+ SubnetID(1234)));
+ ASSERT_THROW(network->add(subnet2), DuplicateSubnetID);
+
+ // Create another network and try to add a subnet to it. It should fail
+ // because the subnet is already associated with the first network.
+ SharedNetwork4Ptr network2(new SharedNetwork4("dog"));
+ ASSERT_THROW(network2->add(subnet), InvalidOperation);
+}
+
+// This test verifies that an IPv4 subnet can be replaced in a shared network.
+// It does the same tests than for addSubnet4 (at the exception of conflicts)
+// and check the random order is kept.
+TEST(SharedNetwork4Test, replaceSubnet4) {
+ // First, create a network.
+ SharedNetwork4Ptr network(new SharedNetwork4("frog"));
+
+ // Try to replace null pointer. It should throw.
+ Subnet4Ptr subnet;
+ ASSERT_THROW(network->replace(subnet), BadValue);
+
+ // Create some valid subnets. they should now be added successfully.
+ subnet.reset(new Subnet4(IOAddress("10.0.0.0"), 8, 10, 20, 30,
+ SubnetID(15)));
+ ASSERT_NO_THROW(network->add(subnet));
+ subnet.reset(new Subnet4(IOAddress("192.168.0.0"), 24, 10, 20, 30,
+ SubnetID(1)));
+ ASSERT_NO_THROW(network->add(subnet));
+ subnet.reset(new Subnet4(IOAddress("192.168.1.0"), 24, 10, 20, 30,
+ SubnetID(10)));
+ ASSERT_NO_THROW(network->add(subnet));
+ ASSERT_EQ(3, network->getAllSubnets()->size());
+
+ // Create another subnet with another ID. Replace should return false.
+ subnet.reset(new Subnet4(IOAddress("192.168.2.0"), 24, 10, 20, 30,
+ SubnetID(2)));
+ EXPECT_FALSE(network->replace(subnet));
+
+ // Subnets did not changed.
+ ASSERT_EQ(3, network->getAllSubnets()->size());
+ auto returned_it = network->getAllSubnets()->begin();
+ Subnet4Ptr returned_subnet = *returned_it;
+ ASSERT_TRUE(returned_subnet);
+ EXPECT_EQ(1, returned_subnet->getID());
+ ++returned_it;
+ returned_subnet = *returned_it;
+ ASSERT_TRUE(returned_subnet);
+ EXPECT_EQ(10, returned_subnet->getID());
+ ++returned_it;
+ returned_subnet = *returned_it;
+ ASSERT_TRUE(returned_subnet);
+ EXPECT_EQ(15, returned_subnet->getID());
+
+ // Reset the returned subnet to the subnet with subnet id 1.
+ returned_subnet = *network->getAllSubnets()->begin();
+ ASSERT_TRUE(returned_subnet);
+ ASSERT_EQ(1, returned_subnet->getID());
+
+ // Create another subnet with the same ID than the second subnet.
+ subnet.reset(new Subnet4(IOAddress("192.168.0.0"), 24, 100, 200, 300,
+ SubnetID(1)));
+ EXPECT_TRUE(network->replace(subnet));
+
+ // Second subnet was updated.
+ EXPECT_EQ(10, returned_subnet->getT1());
+ EXPECT_EQ(20, returned_subnet->getT2());
+ EXPECT_EQ(30, returned_subnet->getValid());
+ SharedNetwork4Ptr network1;
+ returned_subnet->getSharedNetwork(network1);
+ EXPECT_FALSE(network1);
+
+ ASSERT_EQ(3, network->getAllSubnets()->size());
+ returned_subnet = *network->getAllSubnets()->begin();
+ ASSERT_TRUE(returned_subnet);
+ ASSERT_EQ(1, returned_subnet->getID());
+ EXPECT_EQ(100, returned_subnet->getT1());
+ EXPECT_EQ(200, returned_subnet->getT2());
+ EXPECT_EQ(300, returned_subnet->getValid());
+ returned_subnet->getSharedNetwork(network1);
+ EXPECT_TRUE(network1);
+ EXPECT_TRUE(network == network1);
+
+ // Other subnets did not changed.
+ returned_it = network->getAllSubnets()->begin();
+ returned_subnet = *++returned_it;
+ ASSERT_TRUE(returned_subnet);
+ EXPECT_EQ(10, returned_subnet->getID());
+ returned_subnet = *++returned_it;
+ ASSERT_TRUE(returned_subnet);
+ EXPECT_EQ(15, returned_subnet->getID());
+
+ // Create another network and try to replace a subnet to it. It should fail
+ // because the subnet is already associated with the first network.
+ SharedNetwork4Ptr network2(new SharedNetwork4("dog"));
+ ASSERT_THROW(network2->replace(subnet), InvalidOperation);
+
+ // Try to change the prefix. Not recommended but should work.
+ subnet.reset(new Subnet4(IOAddress("192.168.10.0"), 24, 100, 200, 300,
+ SubnetID(1)));
+ EXPECT_TRUE(network->replace(subnet));
+ ASSERT_EQ(3, network->getAllSubnets()->size());
+ returned_subnet = *network->getAllSubnets()->begin();
+ ASSERT_TRUE(returned_subnet);
+ ASSERT_EQ(1, returned_subnet->getID());
+ EXPECT_EQ("192.168.10.0/24", returned_subnet->toText());
+
+ // but not if the prefix already exists for another subnet.
+ subnet.reset(new Subnet4(IOAddress("192.168.1.0"), 24, 100, 200, 300,
+ SubnetID(1)));
+ EXPECT_FALSE(network->replace(subnet));
+ ASSERT_EQ(3, network->getAllSubnets()->size());
+ returned_subnet = *network->getAllSubnets()->begin();
+ ASSERT_TRUE(returned_subnet);
+ ASSERT_EQ(1, returned_subnet->getID());
+ EXPECT_EQ("192.168.10.0/24", returned_subnet->toText());
+}
+
+// This test verifies that it is possible to remove a specified subnet.
+TEST(SharedNetwork4Test, delSubnet4) {
+ // Create two subnets and add them to the shared network.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("10.0.0.0"), 8, 10, 20, 30,
+ SubnetID(1)));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"), 24, 10, 20, 30,
+ SubnetID(2)));
+
+ SharedNetwork4Ptr network(new SharedNetwork4("frog"));
+ ASSERT_NO_THROW(network->add(subnet1));
+ ASSERT_NO_THROW(network->add(subnet2));
+
+ // Make sure they have been added successfully.
+ ASSERT_EQ(2, network->getAllSubnets()->size());
+
+ // Try to remove a subnet that doesn't exist in this shared network.
+ // It should cause an error.
+ ASSERT_THROW(network->del(SubnetID(5)), BadValue);
+
+ // Now delete the subnet that exists.
+ ASSERT_NO_THROW(network->del(subnet1->getID()));
+ // We should be left with only one subnet.
+ ASSERT_EQ(1, network->getAllSubnets()->size());
+ Subnet4Ptr subnet_returned = *network->getAllSubnets()->begin();
+ ASSERT_TRUE(subnet_returned);
+ EXPECT_EQ(subnet2->getID(), subnet_returned->getID());
+
+ // Check that shared network has been cleared for the removed subnet.
+ SharedNetwork4Ptr network1;
+ subnet1->getSharedNetwork(network1);
+ EXPECT_FALSE(network1);
+
+ // Remove another subnet and make sure there are no subnets left.
+ ASSERT_NO_THROW(network->del(subnet2->getID()));
+ EXPECT_EQ(0, network->getAllSubnets()->size());
+
+ // The network pointer should be cleared for this second subnet too.
+ SharedNetwork4Ptr network2;
+ subnet1->getSharedNetwork(network2);
+ EXPECT_FALSE(network2);
+}
+
+// This test verifies that it is possible to iterate over the subnets
+// associated with a particular shared network.
+TEST(SharedNetwork4Test, getNextSubnet) {
+ SharedNetwork4Ptr network(new SharedNetwork4("frog"));
+
+ // Create three subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("10.0.0.0"), 8, 10, 20, 30,
+ SubnetID(1)));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"), 24, 10, 20, 30,
+ SubnetID(2)));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("172.16.25.0"), 24, 10, 20, 30,
+ SubnetID(3)));
+ std::vector<Subnet4Ptr> subnets;
+ subnets.push_back(subnet1);
+ subnets.push_back(subnet2);
+ subnets.push_back(subnet3);
+
+ // Subnets have unique IDs so they should successfully be added to the
+ // network.
+ for (auto i = 0; i < subnets.size(); ++i) {
+ ASSERT_NO_THROW(network->add(subnets[i]))
+ << "failed to add subnet with id " << subnets[i]->getID()
+ << " to shared network";
+ }
+
+ // Collect networks associated with our subnets in the vector.
+ std::vector<SharedNetwork4Ptr> networks;
+ for (auto i = 0; i < subnets.size(); ++i) {
+ SharedNetwork4Ptr network;
+ subnets[i]->getSharedNetwork(network);
+ ASSERT_TRUE(network) << "failed to retrieve shared network for a"
+ << " subnet id " << subnets[i]->getID();
+ networks.push_back(network);
+ }
+
+ // All subnets should be associated with the same network.
+ for (auto i = 1; i < networks.size(); ++i) {
+ EXPECT_TRUE(networks[0] == networks[i]);
+ }
+
+ // Perform the test 3 times where each subnet belonging to the shared
+ // network is treated as a "first" subnet in the call to getNextSubnet.
+ for (auto i = 0; i < subnets.size(); ++i) {
+ Subnet4Ptr s = subnets[i];
+
+ // Iterate over the subnets starting from the subnet with index i.
+ for (auto j = 0; j < subnets.size(); ++j) {
+ // Get next subnet (following the one currently in s).
+ s = networks[0]->getNextSubnet(subnets[i], s->getID());
+ // The last iteration should return empty pointer to indicate end of
+ // the subnets within shared network. If we're not at last iteration
+ // check that the subnet identifier of the returned subnet is valid.
+ if (j < subnets.size() - 1) {
+ ASSERT_TRUE(s) << "retrieving next subnet failed for pair of"
+ " indexes (i, j) = (" << i << ", " << j << ")";
+ const auto expected_subnet_id = (i + j + 1) % subnets.size() + 1;
+ EXPECT_EQ(expected_subnet_id, s->getID());
+ } else {
+ // Null subnet returned for a last iteration.
+ ASSERT_FALSE(s) << "expected null pointer to be returned as"
+ " next subnet for pair of indexes (i, j) = ("
+ << i << ", " << j << ")";
+ }
+ }
+ }
+}
+
+// This test verifies that preferred subnet is returned based on the timestamp
+// when the subnet was last used and allowed client classes.
+TEST(SharedNetwork4Test, getPreferredSubnet) {
+ SharedNetwork4Ptr network(new SharedNetwork4("frog"));
+
+ // Create four subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("10.0.0.0"), 8, 10, 20, 30,
+ SubnetID(1)));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"), 24, 10, 20, 30,
+ SubnetID(2)));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("172.16.25.0"), 24, 10, 20, 30,
+ SubnetID(3)));
+ Subnet4Ptr subnet4(new Subnet4(IOAddress("172.16.28.0"), 24, 10, 20, 30,
+ SubnetID(4)));
+
+ // Associate first two subnets with classes.
+ subnet1->allowClientClass("class1");
+ subnet2->allowClientClass("class1");
+
+ std::vector<Subnet4Ptr> subnets;
+ subnets.push_back(subnet1);
+ subnets.push_back(subnet2);
+ subnets.push_back(subnet3);
+ subnets.push_back(subnet4);
+
+ // Subnets have unique IDs so they should successfully be added to the
+ // network.
+ for (auto i = 0; i < subnets.size(); ++i) {
+ ASSERT_NO_THROW(network->add(subnets[i]))
+ << "failed to add subnet with id " << subnets[i]->getID()
+ << " to shared network";
+ }
+
+ Subnet4Ptr preferred;
+
+ // Initially, for every subnet we should get the same subnet as the preferred
+ // one, because none of them have been used.
+ for (auto i = 0; i < subnets.size(); ++i) {
+ preferred = network->getPreferredSubnet(subnets[i]);
+ EXPECT_EQ(subnets[i]->getID(), preferred->getID());
+ }
+
+ // Allocating an address from subnet2 updates the last allocated timestamp
+ // for this subnet, which makes this subnet preferred over subnet1.
+ subnet2->setLastAllocated(Lease::TYPE_V4, IOAddress("192.0.2.25"));
+ preferred = network->getPreferredSubnet(subnet1);
+ EXPECT_EQ(subnet2->getID(), preferred->getID());
+
+ // If selected is subnet2, the same is returned.
+ preferred = network->getPreferredSubnet(subnet2);
+ EXPECT_EQ(subnet2->getID(), preferred->getID());
+
+ // Even though the subnet1 has been most recently used, the preferred
+ // subnet is subnet3 in this case, because of the client class
+ // mismatch.
+ preferred = network->getPreferredSubnet(subnet3);
+ EXPECT_EQ(subnet3->getID(), preferred->getID());
+
+ // Same for subnet4.
+ preferred = network->getPreferredSubnet(subnet4);
+ EXPECT_EQ(subnet4->getID(), preferred->getID());
+
+ // Allocate an address from the subnet3. This makes it preferred to
+ // subnet4.
+ subnet3->setLastAllocated(Lease::TYPE_V4, IOAddress("172.16.25.23"));
+
+ // If the selected is subnet1, the preferred subnet is subnet2, because
+ // it has the same set of classes as subnet1. The subnet3 can't be
+ // preferred here because of the client class mismatch.
+ preferred = network->getPreferredSubnet(subnet1);
+ EXPECT_EQ(subnet2->getID(), preferred->getID());
+
+ // If we select subnet4, the preferred subnet is subnet3 because
+ // it was used more recently.
+ preferred = network->getPreferredSubnet(subnet4);
+ EXPECT_EQ(subnet3->getID(), preferred->getID());
+
+ // Repeat the test for subnet3 being a selected subnet.
+ preferred = network->getPreferredSubnet(subnet3);
+ EXPECT_EQ(subnet3->getID(), preferred->getID());
+}
+
+// This test verifies that preferred subnet is returned based on the timestamp
+// when the subnet was last used and allowed client classes.
+TEST(SharedNetwork4Test, getPreferredSubnetMultiThreading) {
+ MultiThreadingTest mt(true);
+ SharedNetwork4Ptr network(new SharedNetwork4("frog"));
+
+ // Create four subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("10.0.0.0"), 8, 10, 20, 30,
+ SubnetID(1)));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"), 24, 10, 20, 30,
+ SubnetID(2)));
+ Subnet4Ptr subnet3(new Subnet4(IOAddress("172.16.25.0"), 24, 10, 20, 30,
+ SubnetID(3)));
+ Subnet4Ptr subnet4(new Subnet4(IOAddress("172.16.28.0"), 24, 10, 20, 30,
+ SubnetID(4)));
+
+ // Associate first two subnets with classes.
+ subnet1->allowClientClass("class1");
+ subnet2->allowClientClass("class1");
+
+ std::vector<Subnet4Ptr> subnets;
+ subnets.push_back(subnet1);
+ subnets.push_back(subnet2);
+ subnets.push_back(subnet3);
+ subnets.push_back(subnet4);
+
+ // Subnets have unique IDs so they should successfully be added to the
+ // network.
+ for (auto i = 0; i < subnets.size(); ++i) {
+ ASSERT_NO_THROW(network->add(subnets[i]))
+ << "failed to add subnet with id " << subnets[i]->getID()
+ << " to shared network";
+ }
+
+ Subnet4Ptr preferred;
+
+ // Initially, for every subnet we should get the same subnet as the preferred
+ // one, because none of them have been used.
+ for (auto i = 0; i < subnets.size(); ++i) {
+ preferred = network->getPreferredSubnet(subnets[i]);
+ EXPECT_EQ(subnets[i]->getID(), preferred->getID());
+ }
+
+ // Allocating an address from subnet2 updates the last allocated timestamp
+ // for this subnet, which makes this subnet preferred over subnet1.
+ subnet2->setLastAllocated(Lease::TYPE_V4, IOAddress("192.0.2.25"));
+ preferred = network->getPreferredSubnet(subnet1);
+ EXPECT_EQ(subnet2->getID(), preferred->getID());
+
+ // If selected is subnet2, the same is returned.
+ preferred = network->getPreferredSubnet(subnet2);
+ EXPECT_EQ(subnet2->getID(), preferred->getID());
+
+ // Even though the subnet1 has been most recently used, the preferred
+ // subnet is subnet3 in this case, because of the client class
+ // mismatch.
+ preferred = network->getPreferredSubnet(subnet3);
+ EXPECT_EQ(subnet3->getID(), preferred->getID());
+
+ // Same for subnet4.
+ preferred = network->getPreferredSubnet(subnet4);
+ EXPECT_EQ(subnet4->getID(), preferred->getID());
+
+ // Allocate an address from the subnet3. This makes it preferred to
+ // subnet4.
+ subnet3->setLastAllocated(Lease::TYPE_V4, IOAddress("172.16.25.23"));
+
+ // If the selected is subnet1, the preferred subnet is subnet2, because
+ // it has the same set of classes as subnet1. The subnet3 can't be
+ // preferred here because of the client class mismatch.
+ preferred = network->getPreferredSubnet(subnet1);
+ EXPECT_EQ(subnet2->getID(), preferred->getID());
+
+ // If we select subnet4, the preferred subnet is subnet3 because
+ // it was used more recently.
+ preferred = network->getPreferredSubnet(subnet4);
+ EXPECT_EQ(subnet3->getID(), preferred->getID());
+
+ // Repeat the test for subnet3 being a selected subnet.
+ preferred = network->getPreferredSubnet(subnet3);
+ EXPECT_EQ(subnet3->getID(), preferred->getID());
+}
+
+// This test verifies that subnetsIncludeMatchClientId() works as expected.
+TEST(SharedNetwork4Test, subnetsIncludeMatchClientId) {
+ SharedNetwork4Ptr network(new SharedNetwork4("frog"));
+ ClientClasses classes;
+
+ // Create a subnet and add it to the shared network.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("10.0.0.0"), 8, 10, 20, 30,
+ SubnetID(1)));
+ subnet1->setMatchClientId(false);
+ ASSERT_NO_THROW(network->add(subnet1));
+
+ // The subnet does not match client id.
+ EXPECT_FALSE(SharedNetwork4::subnetsIncludeMatchClientId(subnet1, classes));
+
+ // Create a second subnet and add it.
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"), 24, 10, 20, 30,
+ SubnetID(2)));
+ ASSERT_NO_THROW(network->add(subnet2));
+
+ // Default is to match client id.
+ EXPECT_TRUE(SharedNetwork4::subnetsIncludeMatchClientId(subnet1, classes));
+
+ // Add a class.
+ classes.insert("class1");
+
+ //The second subnet is not guarded so matches.
+ EXPECT_TRUE(SharedNetwork4::subnetsIncludeMatchClientId(subnet1, classes));
+
+ // Put the second subnet in another class
+ subnet2->allowClientClass("class2");
+ EXPECT_FALSE(SharedNetwork4::subnetsIncludeMatchClientId(subnet1, classes));
+
+ // Put the second subnet in the class.
+ subnet2->allowClientClass("class1");
+ EXPECT_TRUE(SharedNetwork4::subnetsIncludeMatchClientId(subnet1, classes));
+}
+
+// This test verifies operations on the network's relay list
+TEST(SharedNetwork4Test, relayInfoList) {
+ SharedNetwork4Ptr network(new SharedNetwork4("frog"));
+
+ EXPECT_FALSE(network->hasRelays());
+ EXPECT_FALSE(network->hasRelayAddress(IOAddress("192.168.2.1")));
+
+ // Add relay addresses to the network.
+ network->addRelayAddress(IOAddress("192.168.2.1"));
+ network->addRelayAddress(IOAddress("192.168.2.2"));
+ network->addRelayAddress(IOAddress("192.168.2.3"));
+
+ // Verify we believe we have relays and we can match them accordingly.
+ EXPECT_TRUE(network->hasRelays());
+ EXPECT_TRUE(network->hasRelayAddress(IOAddress("192.168.2.1")));
+ EXPECT_TRUE(network->hasRelayAddress(IOAddress("192.168.2.2")));
+ EXPECT_TRUE(network->hasRelayAddress(IOAddress("192.168.2.3")));
+ EXPECT_FALSE(network->hasRelayAddress(IOAddress("192.168.2.4")));
+}
+
+// This test verifies that unparsing shared network returns valid structure.
+TEST(SharedNetwork4Test, unparse) {
+ SharedNetwork4Ptr network(new SharedNetwork4("frog"));
+
+ // Set interface name.
+ network->setIface("eth1");
+
+ network->setT1(100);
+ network->setT2(150);
+ 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");
+ network->addRelayAddress(IOAddress("192.168.2.1"));
+ network->setAuthoritative(false);
+ network->setMatchClientId(false);
+ network->setReservationsGlobal(false);
+ network->setReservationsInSubnet(true);
+ network->setReservationsOutOfPool(false);
+
+ // Add several subnets.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("10.0.0.0"), 8, 10, 20, 30,
+ SubnetID(1)));
+ subnet1->addRelayAddress(IOAddress("10.0.0.1"));
+ subnet1->addRelayAddress(IOAddress("10.0.0.2"));
+
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"), 24, 10, 20, 30,
+ SubnetID(2)));
+ network->add(subnet1);
+ network->add(subnet2);
+
+ std::string expected = "{\n"
+ " \"authoritative\": false,\n"
+ " \"interface\": \"eth1\",\n"
+ " \"match-client-id\": false,\n"
+ " \"name\": \"frog\",\n"
+ " \"option-data\": [ ],\n"
+ " \"rebind-timer\": 150,\n"
+ " \"relay\": {\n"
+ " \"ip-addresses\": [ \"192.168.2.1\" ]\n"
+ " },\n"
+ " \"renew-timer\": 100,\n"
+ " \"require-client-classes\": [ \"foo\" ],\n"
+ " \"reservations-global\": false,\n"
+ " \"reservations-in-subnet\": true,\n"
+ " \"reservations-out-of-pool\": false,\n"
+ " \"subnet4\": [\n"
+ " {\n"
+ " \"4o6-interface\": \"\",\n"
+ " \"4o6-interface-id\": \"\",\n"
+ " \"4o6-subnet\": \"\",\n"
+ " \"id\": 1,\n"
+ " \"option-data\": [ ],\n"
+ " \"pools\": [ ],\n"
+ " \"rebind-timer\": 20,\n"
+ " \"relay\": {\n"
+ " \"ip-addresses\": [ \"10.0.0.1\", \"10.0.0.2\" ]\n"
+ " },\n"
+ " \"renew-timer\": 10,\n"
+ " \"subnet\": \"10.0.0.0/8\",\n"
+ " \"valid-lifetime\": 30,\n"
+ " \"min-valid-lifetime\": 30,\n"
+ " \"max-valid-lifetime\": 30\n"
+ " },\n"
+ " {\n"
+ " \"4o6-interface\": \"\",\n"
+ " \"4o6-interface-id\": \"\",\n"
+ " \"4o6-subnet\": \"\",\n"
+ " \"id\": 2,\n"
+ " \"option-data\": [ ],\n"
+ " \"pools\": [ ],\n"
+ " \"rebind-timer\": 20,\n"
+ " \"relay\": {\n"
+ " \"ip-addresses\": [ ]\n"
+ " },\n"
+ " \"renew-timer\": 10,\n"
+ " \"subnet\": \"192.0.2.0/24\",\n"
+ " \"valid-lifetime\": 30,\n"
+ " \"min-valid-lifetime\": 30,\n"
+ " \"max-valid-lifetime\": 30\n"
+ " }\n"
+ " ],\n"
+ " \"user-context\": { \"comment\": \"bar\", \"foo\": 1 },\n"
+ " \"valid-lifetime\": 200,\n"
+ " \"min-valid-lifetime\": 200,\n"
+ " \"max-valid-lifetime\": 200\n"
+ "}\n";
+
+ test::runToElementTest<SharedNetwork4>(expected, *network);
+}
+
+// This test verifies that when the shared network object is destroyed,
+// the subnets belonging to this shared network will not hold the pointer
+// to the destroyed network.
+TEST(SharedNetwork4Test, destructSharedNetwork) {
+ // Create a network and add a subnet to it.
+ SharedNetwork4Ptr network(new SharedNetwork4("frog"));
+ Subnet4Ptr subnet(new Subnet4(IOAddress("10.0.0.0"), 8, 10, 20, 30,
+ SubnetID(1)));
+ ASSERT_NO_THROW(network->add(subnet));
+
+ // Get the pointer to the network from subnet.
+ SharedNetwork4Ptr subnet_to_network;
+ subnet->getSharedNetwork(subnet_to_network);
+ ASSERT_TRUE(subnet_to_network);
+
+ // Reset the pointer to not hold the reference to the shared network.
+ subnet_to_network.reset();
+
+ // Destroy the network object.
+ network.reset();
+
+ // The reference to the network from the subnet should be lost.
+ subnet->getSharedNetwork(subnet_to_network);
+ ASSERT_FALSE(subnet_to_network);
+}
+
+// This test verifies that it is possible to remove all subnets.
+TEST(SharedNetwork4Test, delAll) {
+ // Create two subnets and add them to the shared network.
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("10.0.0.0"), 8, 10, 20, 30,
+ SubnetID(1)));
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"), 24, 10, 20, 30,
+ SubnetID(2)));
+
+ SharedNetwork4Ptr network(new SharedNetwork4("frog"));
+ ASSERT_NO_THROW(network->add(subnet1));
+ ASSERT_NO_THROW(network->add(subnet2));
+
+ // Make sure they have been added successfully.
+ ASSERT_EQ(2, network->getAllSubnets()->size());
+
+ ASSERT_NO_THROW(network->delAll());
+
+ // Now check that there are no subnets.
+ ASSERT_EQ(0, network->getAllSubnets()->size());
+}
+
+// This test verifies that the SharedNetwork6 factory function creates a
+// valid shared network instance.
+TEST(SharedNetwork6Test, create) {
+ auto network = SharedNetwork6::create("frog");
+ ASSERT_TRUE(network);
+ EXPECT_EQ("frog", network->getName());
+}
+
+// This test verifies the default values set for the shared
+// networks and verifies that the optional values are unspecified.
+TEST(SharedNetwork6Test, defaults) {
+ SharedNetwork6Ptr network(new SharedNetwork6("frog"));
+ EXPECT_TRUE(network->getIface().unspecified());
+ EXPECT_TRUE(network->getIface().empty());
+
+ EXPECT_TRUE(network->getClientClass().unspecified());
+ EXPECT_TRUE(network->getClientClass().empty());
+
+ EXPECT_TRUE(network->getValid().unspecified());
+ EXPECT_EQ(0, network->getValid().get());
+
+ EXPECT_TRUE(network->getT1().unspecified());
+ EXPECT_EQ(0, network->getT1().get());
+
+ EXPECT_TRUE(network->getT2().unspecified());
+ EXPECT_EQ(0, network->getT2().get());
+
+ EXPECT_TRUE(network->getReservationsGlobal().unspecified());
+ EXPECT_FALSE(network->getReservationsGlobal().get());
+
+ EXPECT_TRUE(network->getReservationsInSubnet().unspecified());
+ EXPECT_TRUE(network->getReservationsInSubnet().get());
+
+ EXPECT_TRUE(network->getReservationsOutOfPool().unspecified());
+ EXPECT_FALSE(network->getReservationsOutOfPool().get());
+
+ EXPECT_TRUE(network->getCalculateTeeTimes().unspecified());
+ EXPECT_FALSE(network->getCalculateTeeTimes().get());
+
+ EXPECT_TRUE(network->getT1Percent().unspecified());
+ EXPECT_EQ(0.0, network->getT1Percent().get());
+
+ EXPECT_TRUE(network->getT2Percent().unspecified());
+ EXPECT_EQ(0.0, network->getT2Percent().get());
+
+ EXPECT_TRUE(network->getPreferred().unspecified());
+ EXPECT_EQ(0, network->getPreferred().get());
+
+ EXPECT_TRUE(network->getRapidCommit().unspecified());
+ EXPECT_FALSE(network->getRapidCommit().get());
+
+ EXPECT_TRUE(network->getDdnsSendUpdates().unspecified());
+ EXPECT_FALSE(network->getDdnsSendUpdates().get());
+
+ EXPECT_TRUE(network->getDdnsOverrideNoUpdate().unspecified());
+ EXPECT_FALSE(network->getDdnsOverrideNoUpdate().get());
+
+ EXPECT_TRUE(network->getDdnsOverrideClientUpdate().unspecified());
+ EXPECT_FALSE(network->getDdnsOverrideClientUpdate().get());
+
+ EXPECT_TRUE(network->getDdnsReplaceClientNameMode().unspecified());
+ EXPECT_EQ(D2ClientConfig::RCM_NEVER, network->getDdnsReplaceClientNameMode().get());
+
+ EXPECT_TRUE(network->getDdnsGeneratedPrefix().unspecified());
+ EXPECT_TRUE(network->getDdnsGeneratedPrefix().empty());
+
+ EXPECT_TRUE(network->getDdnsQualifyingSuffix().unspecified());
+ EXPECT_TRUE(network->getDdnsQualifyingSuffix().empty());
+
+ EXPECT_TRUE(network->getHostnameCharSet().unspecified());
+ EXPECT_TRUE(network->getHostnameCharSet().empty());
+
+ EXPECT_TRUE(network->getHostnameCharReplacement().unspecified());
+ EXPECT_TRUE(network->getHostnameCharReplacement().empty());
+}
+
+// This test verifies that shared network can be given a name and that
+// this name can be retrieved.
+TEST(SharedNetwork6Test, getName) {
+ // Create shared network with an initial name "frog".
+ SharedNetwork6Ptr network(new SharedNetwork6("frog"));
+ EXPECT_EQ("frog", network->getName());
+
+ // Override the name.
+ network->setName("dog");
+ EXPECT_EQ("dog", network->getName());
+}
+
+// This test verifies that an IPv6 subnet can be added to a shared network.
+// It also verifies that two subnets with the same ID can't be added to
+// a shared network and that a single subnet can't be added to two different
+// shared subnets.
+TEST(SharedNetwork6Test, addSubnet6) {
+ // First, create a network.
+ SharedNetwork6Ptr network(new SharedNetwork6("frog"));
+
+ // Try to add null pointer. It should throw.
+ Subnet6Ptr subnet;
+ ASSERT_THROW(network->add(subnet), BadValue);
+
+ // Create a valid subnet. It should now be added successfully.
+ subnet.reset(new Subnet6(IOAddress("2001:db8:1::"), 64, 10, 20, 30, 40,
+ SubnetID(15)));
+ ASSERT_NO_THROW(network->add(subnet));
+ ASSERT_EQ(1, network->getAllSubnets()->size());
+
+ // Retrieve the subnet from the network and make sure it is returned
+ // as expected.
+ Subnet6Ptr returned_subnet = *network->getAllSubnets()->begin();
+ ASSERT_TRUE(returned_subnet);
+ EXPECT_EQ(subnet->getID(), returned_subnet->getID());
+ SharedNetwork6Ptr network1;
+ subnet->getSharedNetwork(network1);
+ ASSERT_TRUE(network1);
+ EXPECT_TRUE(network1 == network);
+
+ // Create another subnet with the same ID. Adding a network with the
+ // same ID should cause an error.
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 16, 10, 20, 30, 40,
+ SubnetID(15)));
+ ASSERT_THROW(network->add(subnet2), DuplicateSubnetID);
+
+ // Create another subnet with the same prefix. Adding a network with the
+ // same prefix should cause an error.
+ subnet2.reset(new Subnet6(IOAddress("2001:db8:1::"), 64, 10, 20, 30, 40,
+ SubnetID(1234)));
+ ASSERT_THROW(network->add(subnet2), DuplicateSubnetID);
+
+ // Create another network and try to add a subnet to it. It should fail
+ // because the subnet is already associated with the first network.
+ SharedNetwork6Ptr network2(new SharedNetwork6("dog"));
+ ASSERT_THROW(network2->add(subnet), InvalidOperation);
+}
+
+// This test verifies that an IPv6 subnet can be replaced in a shared network.
+// It does the same tests than for addSubnet6 (at the exception of conflicts)
+// and check the random order is kept.
+TEST(SharedNetwork6Test, replaceSubnet6) {
+ // First, create a network.
+ SharedNetwork6Ptr network(new SharedNetwork6("frog"));
+
+ // Try to replace null pointer. It should throw.
+ Subnet6Ptr subnet;
+ ASSERT_THROW(network->replace(subnet), BadValue);
+
+ // Create some valid subnets. they should now be added successfully.
+ subnet.reset(new Subnet6(IOAddress("2001:db8:1::"), 48, 10, 20, 30, 40,
+ SubnetID(15)));
+ ASSERT_NO_THROW(network->add(subnet));
+ subnet.reset(new Subnet6(IOAddress("2001:db8:2::"), 64, 10, 20, 30, 40,
+ SubnetID(1)));
+ ASSERT_NO_THROW(network->add(subnet));
+ subnet.reset(new Subnet6(IOAddress("2001:db8:3::"), 64, 10, 20, 30, 40,
+ SubnetID(10)));
+ ASSERT_NO_THROW(network->add(subnet));
+ ASSERT_EQ(3, network->getAllSubnets()->size());
+
+ // Create another subnet with another ID. Replace should return false.
+ subnet.reset(new Subnet6(IOAddress("2001:db8:4::1"), 64, 10, 20, 30, 40,
+ SubnetID(2)));
+ EXPECT_FALSE(network->replace(subnet));
+
+ // Subnets did not changed.
+ ASSERT_EQ(3, network->getAllSubnets()->size());
+ auto returned_it = network->getAllSubnets()->begin();
+ Subnet6Ptr returned_subnet = *returned_it;
+ ASSERT_TRUE(returned_subnet);
+ EXPECT_EQ(1, returned_subnet->getID());
+ ++returned_it;
+ returned_subnet = *returned_it;
+ ASSERT_TRUE(returned_subnet);
+ EXPECT_EQ(10, returned_subnet->getID());
+ ++returned_it;
+ returned_subnet = *returned_it;
+ ASSERT_TRUE(returned_subnet);
+ EXPECT_EQ(15, returned_subnet->getID());
+
+ // Reset the returned subnet to the subnet with subnet id 1.
+ returned_subnet = *network->getAllSubnets()->begin();
+ ASSERT_TRUE(returned_subnet);
+ ASSERT_EQ(1, returned_subnet->getID());
+
+ // Create another subnet with the same ID than the second subnet.
+ subnet.reset(new Subnet6(IOAddress("2001:db8:2::"), 64, 100, 200, 300, 400,
+ SubnetID(1)));
+ EXPECT_TRUE(network->replace(subnet));
+
+ // Second subnet was updated.
+ EXPECT_EQ(10, returned_subnet->getT1());
+ EXPECT_EQ(20, returned_subnet->getT2());
+ EXPECT_EQ(30, returned_subnet->getPreferred());
+ EXPECT_EQ(40, returned_subnet->getValid());
+ SharedNetwork6Ptr network1;
+ returned_subnet->getSharedNetwork(network1);
+ EXPECT_FALSE(network1);
+
+ ASSERT_EQ(3, network->getAllSubnets()->size());
+ returned_subnet = *network->getAllSubnets()->begin();
+ ASSERT_TRUE(returned_subnet);
+ ASSERT_EQ(1, returned_subnet->getID());
+ EXPECT_EQ(100, returned_subnet->getT1());
+ EXPECT_EQ(200, returned_subnet->getT2());
+ EXPECT_EQ(300, returned_subnet->getPreferred());
+ EXPECT_EQ(400, returned_subnet->getValid());
+ returned_subnet->getSharedNetwork(network1);
+ EXPECT_TRUE(network1);
+ EXPECT_TRUE(network == network1);
+
+ // Other subnets did not changed.
+ returned_it = network->getAllSubnets()->begin();
+ returned_subnet = *++returned_it;
+ ASSERT_TRUE(returned_subnet);
+ EXPECT_EQ(10, returned_subnet->getID());
+ returned_subnet = *++returned_it;
+ ASSERT_TRUE(returned_subnet);
+ EXPECT_EQ(15, returned_subnet->getID());
+
+ // Create another network and try to replace a subnet to it. It should fail
+ // because the subnet is already associated with the first network.
+ SharedNetwork6Ptr network2(new SharedNetwork6("dog"));
+ ASSERT_THROW(network2->replace(subnet), InvalidOperation);
+
+ // Try to change the prefix. Not recommended but should work.
+ subnet.reset(new Subnet6(IOAddress("2001:db8:10::"), 64, 100, 200, 300,
+ 400, SubnetID(1)));
+ EXPECT_TRUE(network->replace(subnet));
+ ASSERT_EQ(3, network->getAllSubnets()->size());
+ returned_subnet = *network->getAllSubnets()->begin();
+ ASSERT_TRUE(returned_subnet);
+ ASSERT_EQ(1, returned_subnet->getID());
+ EXPECT_EQ("2001:db8:10::/64", returned_subnet->toText());
+
+ // but not if the prefix already exists for another subnet.
+ subnet.reset(new Subnet6(IOAddress("2001:db8:3::"), 64, 100, 200, 300, 400,
+ SubnetID(1)));
+ EXPECT_FALSE(network->replace(subnet));
+ ASSERT_EQ(3, network->getAllSubnets()->size());
+ returned_subnet = *network->getAllSubnets()->begin();
+ ASSERT_TRUE(returned_subnet);
+ ASSERT_EQ(1, returned_subnet->getID());
+ EXPECT_EQ("2001:db8:10::/64", returned_subnet->toText());
+}
+
+// This test verifies that it is possible to remove a specified subnet.
+TEST(SharedNetwork6Test, delSubnet6) {
+ // Create two subnets and add them to the shared network.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 64, 10, 20, 30,
+ 40, SubnetID(1)));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 16, 10, 20, 30, 40,
+ SubnetID(2)));
+
+ SharedNetwork6Ptr network(new SharedNetwork6("frog"));
+ ASSERT_NO_THROW(network->add(subnet1));
+ ASSERT_NO_THROW(network->add(subnet2));
+
+ // Make sure they have been added successfully.
+ ASSERT_EQ(2, network->getAllSubnets()->size());
+
+ // Try to remove a subnet that doesn't exist in this shared network.
+ // It should cause an error.
+ ASSERT_THROW(network->del(SubnetID(5)), BadValue);
+
+ // Now delete the subnet that exists.
+ ASSERT_NO_THROW(network->del(subnet1->getID()));
+ // We should be left with only one subnet.
+ ASSERT_EQ(1, network->getAllSubnets()->size());
+ Subnet6Ptr subnet_returned = *network->getAllSubnets()->begin();
+ ASSERT_TRUE(subnet_returned);
+ EXPECT_EQ(subnet2->getID(), subnet_returned->getID());
+
+ // Check that shared network has been cleared for the removed subnet.
+ SharedNetwork6Ptr network1;
+ subnet1->getSharedNetwork(network1);
+ EXPECT_FALSE(network1);
+
+ // Remove another subnet and make sure there are no subnets left.
+ ASSERT_NO_THROW(network->del(subnet2->getID()));
+ EXPECT_EQ(0, network->getAllSubnets()->size());
+
+ // The network pointer should be cleared for this second subnet too.
+ SharedNetwork6Ptr network2;
+ subnet1->getSharedNetwork(network2);
+ EXPECT_FALSE(network2);
+}
+
+// This test verifies that it is possible to iterate over the subnets
+// associated with a particular shared network.
+TEST(SharedNetwork6Test, getNextSubnet) {
+ SharedNetwork6Ptr network(new SharedNetwork6("frog"));
+
+ // Create three subnets.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 64, 10, 20, 30,
+ 40, SubnetID(1)));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 16, 10, 20, 30, 40,
+ SubnetID(2)));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:2::"), 64, 10, 20, 30,
+ 40, SubnetID(3)));
+ std::vector<Subnet6Ptr> subnets;
+ subnets.push_back(subnet1);
+ subnets.push_back(subnet2);
+ subnets.push_back(subnet3);
+
+ // Subnets have unique IDs so they should successfully be added to the
+ // network.
+ for (auto i = 0; i < subnets.size(); ++i) {
+ ASSERT_NO_THROW(network->add(subnets[i]))
+ << "failed to add subnet with id " << subnets[i]->getID()
+ << " to shared network";
+ }
+
+ // Collect networks associated with our subnets in the vector.
+ std::vector<SharedNetwork6Ptr> networks;
+ for (auto i = 0; i < subnets.size(); ++i) {
+ SharedNetwork6Ptr network;
+ subnets[i]->getSharedNetwork(network);
+ ASSERT_TRUE(network) << "failed to retrieve shared network for a"
+ << " subnet id " << subnets[i]->getID();
+ networks.push_back(network);
+ }
+
+ // All subnets should be associated with the same network.
+ for (auto i = 1; i < networks.size(); ++i) {
+ EXPECT_TRUE(networks[0] == networks[i]);
+ }
+
+ // Perform the test 3 times where each subnet belonging to the shared
+ // network is treated as a "first" subnet in the call to getNextSubnet.
+ for (auto i = 0; i < subnets.size(); ++i) {
+ Subnet6Ptr s = subnets[i];
+
+ // Iterate over the subnets starting from the subnet with index i.
+ for (auto j = 0; j < subnets.size(); ++j) {
+ // Get next subnet (following the one currently in s).
+ s = networks[0]->getNextSubnet(subnets[i], s->getID());
+ // The last iteration should return empty pointer to indicate end of
+ // the subnets within shared network. If we're not at last iteration
+ // check that the subnet identifier of the returned subnet is valid.
+ if (j < subnets.size() - 1) {
+ ASSERT_TRUE(s) << "retrieving next subnet failed for pair of"
+ " indexes (i, j) = (" << i << ", " << j << ")";
+ const auto expected_subnet_id = (i + j + 1) % subnets.size() + 1;
+ EXPECT_EQ(expected_subnet_id, s->getID());
+ } else {
+ // Null subnet returned for a last iteration.
+ ASSERT_FALSE(s) << "expected null pointer to be returned as"
+ " next subnet for pair of indexes (i, j) = ("
+ << i << ", " << j << ")";
+ }
+ }
+ }
+}
+
+// This test verifies that preferred subnet is returned based on the timestamp
+// when the subnet was last used and allowed client classes.
+TEST(SharedNetwork6Test, getPreferredSubnet) {
+ SharedNetwork6Ptr network(new SharedNetwork6("frog"));
+
+ // Create four subnets.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 64, 10, 20, 30,
+ 40, SubnetID(1)));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 16, 10, 20, 30, 40,
+ SubnetID(2)));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:2::"), 64, 10, 20, 30,
+ 40, SubnetID(3)));
+ Subnet6Ptr subnet4(new Subnet6(IOAddress("3000:1::"), 64, 10, 20, 30,
+ 40, SubnetID(4)));
+
+ // Associate first two subnets with classes.
+ subnet1->allowClientClass("class1");
+ subnet2->allowClientClass("class1");
+
+ std::vector<Subnet6Ptr> subnets;
+ subnets.push_back(subnet1);
+ subnets.push_back(subnet2);
+ subnets.push_back(subnet3);
+ subnets.push_back(subnet4);
+
+ // Subnets have unique IDs so they should successfully be added to the
+ // network.
+ for (auto i = 0; i < subnets.size(); ++i) {
+ ASSERT_NO_THROW(network->add(subnets[i]))
+ << "failed to add subnet with id " << subnets[i]->getID()
+ << " to shared network";
+ }
+
+ Subnet6Ptr preferred;
+
+ // Initially, for every subnet we should get the same subnet as the preferred
+ // one, because none of them have been used.
+ for (auto i = 0; i < subnets.size(); ++i) {
+ preferred = network->getPreferredSubnet(subnets[i], Lease::TYPE_NA);
+ EXPECT_EQ(subnets[i]->getID(), preferred->getID());
+ preferred = network->getPreferredSubnet(subnets[i], Lease::TYPE_TA);
+ EXPECT_EQ(subnets[i]->getID(), preferred->getID());
+ preferred = network->getPreferredSubnet(subnets[i], Lease::TYPE_PD);
+ EXPECT_EQ(subnets[i]->getID(), preferred->getID());
+ }
+
+ // Allocating an address from subnet2 updates the last allocated timestamp
+ // for this subnet, which makes this subnet preferred over subnet1.
+ subnet2->setLastAllocated(Lease::TYPE_NA, IOAddress("2001:db8:1:2::"));
+ preferred = network->getPreferredSubnet(subnet1, Lease::TYPE_NA);
+ EXPECT_EQ(subnet2->getID(), preferred->getID());
+
+ // If selected is subnet2, the same is returned.
+ preferred = network->getPreferredSubnet(subnet2, Lease::TYPE_NA);
+ EXPECT_EQ(subnet2->getID(), preferred->getID());
+
+ // The preferred subnet is dependent on the lease type. For the PD
+ // we should get the same subnet as selected.
+ preferred = network->getPreferredSubnet(subnet1, Lease::TYPE_PD);
+ EXPECT_EQ(subnet1->getID(), preferred->getID());
+
+ // Although, if we pick a prefix from the subnet2, we should get the
+ // subnet2 as preferred instead.
+ subnet2->setLastAllocated(Lease::TYPE_PD, IOAddress("3000:1234::"));
+ preferred = network->getPreferredSubnet(subnet1, Lease::TYPE_PD);
+ EXPECT_EQ(subnet2->getID(), preferred->getID());
+
+ // Even though the subnet1 has been most recently used, the preferred
+ // subnet is subnet3 in this case, because of the client class
+ // mismatch.
+ preferred = network->getPreferredSubnet(subnet3, Lease::TYPE_NA);
+ EXPECT_EQ(subnet3->getID(), preferred->getID());
+
+ // Same for subnet4.
+ preferred = network->getPreferredSubnet(subnet4, Lease::TYPE_NA);
+ EXPECT_EQ(subnet4->getID(), preferred->getID());
+
+ // Allocate an address from the subnet3. This makes it preferred to
+ // subnet4.
+ subnet3->setLastAllocated(Lease::TYPE_NA, IOAddress("2001:db8:2:1234::"));
+
+ // If the selected is subnet1, the preferred subnet is subnet2, because
+ // it has the same set of classes as subnet1. The subnet3 can't be
+ // preferred here because of the client class mismatch.
+ preferred = network->getPreferredSubnet(subnet1, Lease::TYPE_NA);
+ EXPECT_EQ(subnet2->getID(), preferred->getID());
+
+ // If we select subnet4, the preferred subnet is subnet3 because
+ // it was used more recently.
+ preferred = network->getPreferredSubnet(subnet4, Lease::TYPE_NA);
+ EXPECT_EQ(subnet3->getID(), preferred->getID());
+
+ // Repeat the test for subnet3 being a selected subnet.
+ preferred = network->getPreferredSubnet(subnet3, Lease::TYPE_NA);
+ EXPECT_EQ(subnet3->getID(), preferred->getID());
+}
+
+// This test verifies that preferred subnet is returned based on the timestamp
+// when the subnet was last used and allowed client classes.
+TEST(SharedNetwork6Test, getPreferredSubnetMultiThreading) {
+ MultiThreadingTest mt(true);
+ SharedNetwork6Ptr network(new SharedNetwork6("frog"));
+
+ // Create four subnets.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 64, 10, 20, 30,
+ 40, SubnetID(1)));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 16, 10, 20, 30, 40,
+ SubnetID(2)));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:2::"), 64, 10, 20, 30,
+ 40, SubnetID(3)));
+ Subnet6Ptr subnet4(new Subnet6(IOAddress("3000:1::"), 64, 10, 20, 30,
+ 40, SubnetID(4)));
+
+ // Associate first two subnets with classes.
+ subnet1->allowClientClass("class1");
+ subnet2->allowClientClass("class1");
+
+ std::vector<Subnet6Ptr> subnets;
+ subnets.push_back(subnet1);
+ subnets.push_back(subnet2);
+ subnets.push_back(subnet3);
+ subnets.push_back(subnet4);
+
+ // Subnets have unique IDs so they should successfully be added to the
+ // network.
+ for (auto i = 0; i < subnets.size(); ++i) {
+ ASSERT_NO_THROW(network->add(subnets[i]))
+ << "failed to add subnet with id " << subnets[i]->getID()
+ << " to shared network";
+ }
+
+ Subnet6Ptr preferred;
+
+ // Initially, for every subnet we should get the same subnet as the preferred
+ // one, because none of them have been used.
+ for (auto i = 0; i < subnets.size(); ++i) {
+ preferred = network->getPreferredSubnet(subnets[i], Lease::TYPE_NA);
+ EXPECT_EQ(subnets[i]->getID(), preferred->getID());
+ preferred = network->getPreferredSubnet(subnets[i], Lease::TYPE_TA);
+ EXPECT_EQ(subnets[i]->getID(), preferred->getID());
+ preferred = network->getPreferredSubnet(subnets[i], Lease::TYPE_PD);
+ EXPECT_EQ(subnets[i]->getID(), preferred->getID());
+ }
+
+ // Allocating an address from subnet2 updates the last allocated timestamp
+ // for this subnet, which makes this subnet preferred over subnet1.
+ subnet2->setLastAllocated(Lease::TYPE_NA, IOAddress("2001:db8:1:2::"));
+ preferred = network->getPreferredSubnet(subnet1, Lease::TYPE_NA);
+ EXPECT_EQ(subnet2->getID(), preferred->getID());
+
+ // If selected is subnet2, the same is returned.
+ preferred = network->getPreferredSubnet(subnet2, Lease::TYPE_NA);
+ EXPECT_EQ(subnet2->getID(), preferred->getID());
+
+ // The preferred subnet is dependent on the lease type. For the PD
+ // we should get the same subnet as selected.
+ preferred = network->getPreferredSubnet(subnet1, Lease::TYPE_PD);
+ EXPECT_EQ(subnet1->getID(), preferred->getID());
+
+ // Although, if we pick a prefix from the subnet2, we should get the
+ // subnet2 as preferred instead.
+ subnet2->setLastAllocated(Lease::TYPE_PD, IOAddress("3000:1234::"));
+ preferred = network->getPreferredSubnet(subnet1, Lease::TYPE_PD);
+ EXPECT_EQ(subnet2->getID(), preferred->getID());
+
+ // Even though the subnet1 has been most recently used, the preferred
+ // subnet is subnet3 in this case, because of the client class
+ // mismatch.
+ preferred = network->getPreferredSubnet(subnet3, Lease::TYPE_NA);
+ EXPECT_EQ(subnet3->getID(), preferred->getID());
+
+ // Same for subnet4.
+ preferred = network->getPreferredSubnet(subnet4, Lease::TYPE_NA);
+ EXPECT_EQ(subnet4->getID(), preferred->getID());
+
+ // Allocate an address from the subnet3. This makes it preferred to
+ // subnet4.
+ subnet3->setLastAllocated(Lease::TYPE_NA, IOAddress("2001:db8:2:1234::"));
+
+ // If the selected is subnet1, the preferred subnet is subnet2, because
+ // it has the same set of classes as subnet1. The subnet3 can't be
+ // preferred here because of the client class mismatch.
+ preferred = network->getPreferredSubnet(subnet1, Lease::TYPE_NA);
+ EXPECT_EQ(subnet2->getID(), preferred->getID());
+
+ // If we select subnet4, the preferred subnet is subnet3 because
+ // it was used more recently.
+ preferred = network->getPreferredSubnet(subnet4, Lease::TYPE_NA);
+ EXPECT_EQ(subnet3->getID(), preferred->getID());
+
+ // Repeat the test for subnet3 being a selected subnet.
+ preferred = network->getPreferredSubnet(subnet3, Lease::TYPE_NA);
+ EXPECT_EQ(subnet3->getID(), preferred->getID());
+}
+
+// This test verifies operations on the network's relay list
+TEST(SharedNetwork6Test, relayInfoList) {
+ SharedNetwork6Ptr network(new SharedNetwork6("frog"));
+
+ EXPECT_FALSE(network->hasRelays());
+ EXPECT_FALSE(network->hasRelayAddress(IOAddress("2001:db8:2::1")));
+
+ // Add relay addresses to the network.
+ network->addRelayAddress(IOAddress("2001:db8:2::1"));
+ network->addRelayAddress(IOAddress("2001:db8:2::2"));
+ network->addRelayAddress(IOAddress("2001:db8:2::3"));
+
+ // Verify we believe we have relays and we can match them accordingly.
+ EXPECT_TRUE(network->hasRelays());
+ EXPECT_TRUE(network->hasRelayAddress(IOAddress("2001:db8:2::1")));
+ EXPECT_TRUE(network->hasRelayAddress(IOAddress("2001:db8:2::2")));
+ EXPECT_TRUE(network->hasRelayAddress(IOAddress("2001:db8:2::3")));
+ EXPECT_FALSE(network->hasRelayAddress(IOAddress("2001:db8:2::4")));
+}
+
+// This test verifies that unparsing shared network returns valid structure.
+TEST(SharedNetwork6Test, unparse) {
+ SharedNetwork6Ptr network(new SharedNetwork6("frog"));
+ network->setIface("eth1");
+ network->setT1(100);
+ network->setT2(150);
+ 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");
+
+ network->addRelayAddress(IOAddress("2001:db8:1::7"));
+ network->addRelayAddress(IOAddress("2001:db8:1::8"));
+
+ network->setRapidCommit(true);
+ network->setReservationsGlobal(false);
+ network->setReservationsInSubnet(true);
+ network->setReservationsOutOfPool(false);
+
+ // Include interface-id at shared network level. After unparsing the
+ // network we should only see it at shared network level and not at
+ // the subnet level.
+ std::string iface_id_value = "vlan102";
+ OptionBuffer iface_id_buffer(iface_id_value.begin(), iface_id_value.end());
+ OptionPtr iface_id_opt(new Option(Option::V6, D6O_INTERFACE_ID, iface_id_buffer));
+ network->setInterfaceId(iface_id_opt);
+
+ // Add several subnets.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 64, 10, 20, 30,
+ 40, SubnetID(1)));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 16, 10, 20, 30, 40,
+ SubnetID(2)));
+ subnet2->addRelayAddress(IOAddress("2001:db8:1::8"));
+
+ // Set subnet specific interface-id for subnet2. This is to ensure that
+ // the subnet specific value is not overridden by shared network specific
+ // value.
+ std::string subnet_interface_id_value = "vlan222";
+ OptionBuffer subnet_iface_id_buffer(subnet_interface_id_value.begin(),
+ subnet_interface_id_value.end());
+ OptionPtr subnet_iface_id_opt(new Option(Option::V6, D6O_INTERFACE_ID, subnet_iface_id_buffer));
+ subnet2->setInterfaceId(subnet_iface_id_opt);
+
+ network->add(subnet1);
+ network->add(subnet2);
+
+ std::string expected = "{\n"
+ " \"interface\": \"eth1\",\n"
+ " \"interface-id\": \"vlan102\",\n"
+ " \"name\": \"frog\",\n"
+ " \"option-data\": [ ],\n"
+ " \"preferred-lifetime\": 200,\n"
+ " \"min-preferred-lifetime\": 200,\n"
+ " \"max-preferred-lifetime\": 200,\n"
+ " \"rapid-commit\": true,\n"
+ " \"rebind-timer\": 150,\n"
+ " \"relay\": {\n"
+ " \"ip-addresses\": [ \"2001:db8:1::7\", \"2001:db8:1::8\" ]\n"
+ " },\n"
+ " \"renew-timer\": 100,\n"
+ " \"require-client-classes\": [ \"foo\" ],\n"
+ " \"reservations-global\": false,\n"
+ " \"reservations-in-subnet\": true,\n"
+ " \"reservations-out-of-pool\": false,\n"
+ " \"subnet6\": [\n"
+ " {\n"
+ " \"id\": 1,\n"
+ " \"option-data\": [ ],\n"
+ " \"pd-pools\": [ ],\n"
+ " \"pools\": [ ],\n"
+ " \"preferred-lifetime\": 30,\n"
+ " \"min-preferred-lifetime\": 30,\n"
+ " \"max-preferred-lifetime\": 30,\n"
+ " \"rebind-timer\": 20,\n"
+ " \"relay\": {\n"
+ " \"ip-addresses\": [ ]\n"
+ " },\n"
+ " \"renew-timer\": 10,\n"
+ " \"subnet\": \"2001:db8:1::/64\",\n"
+ " \"valid-lifetime\": 40,\n"
+ " \"min-valid-lifetime\": 40,\n"
+ " \"max-valid-lifetime\": 40\n"
+ " },\n"
+ " {\n"
+ " \"id\": 2,\n"
+ " \"interface-id\": \"vlan222\",\n"
+ " \"option-data\": [ ],\n"
+ " \"pd-pools\": [ ],\n"
+ " \"pools\": [ ],\n"
+ " \"preferred-lifetime\": 30,\n"
+ " \"min-preferred-lifetime\": 30,\n"
+ " \"max-preferred-lifetime\": 30,\n"
+ " \"rebind-timer\": 20,\n"
+ " \"relay\": {\n"
+ " \"ip-addresses\": [ \"2001:db8:1::8\" ]\n"
+ " },\n"
+ " \"renew-timer\": 10,\n"
+ " \"subnet\": \"3000::/16\",\n"
+ " \"valid-lifetime\": 40,\n"
+ " \"min-valid-lifetime\": 40,\n"
+ " \"max-valid-lifetime\": 40\n"
+ " }\n"
+ " ],\n"
+ " \"user-context\": { \"foo\": \"bar\" },\n"
+ " \"valid-lifetime\": 300,\n"
+ " \"min-valid-lifetime\": 300,\n"
+ " \"max-valid-lifetime\": 300\n"
+ "}\n";
+
+ test::runToElementTest<SharedNetwork6>(expected, *network);
+}
+
+// This test verifies that when the shared network object is destroyed,
+// the subnets belonging to this shared network will not hold the pointer
+// to the destroyed network.
+TEST(SharedNetwork6Test, destructSharedNetwork) {
+ // Create a network and add a subnet to it.
+ SharedNetwork6Ptr network(new SharedNetwork6("frog"));
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 64, 10, 20, 30,
+ 40, SubnetID(1)));
+ ASSERT_NO_THROW(network->add(subnet));
+
+ // Get the pointer to the network from subnet.
+ SharedNetwork6Ptr subnet_to_network;
+ subnet->getSharedNetwork(subnet_to_network);
+ ASSERT_TRUE(subnet_to_network);
+
+ // Reset the pointer to not hold the reference to the shared network.
+ subnet_to_network.reset();
+
+ // Destroy the network object.
+ network.reset();
+
+ // The reference to the network from the subnet should be lost.
+ subnet->getSharedNetwork(subnet_to_network);
+ ASSERT_FALSE(subnet_to_network);
+}
+
+// This test verifies that it is possible to remove all subnets.
+TEST(SharedNetwork6Test, delAll) {
+ // Create two subnets and add them to the shared network.
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 64, 10, 20, 30,
+ 40, SubnetID(1)));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 16, 10, 20, 30, 40,
+ SubnetID(2)));
+
+ SharedNetwork6Ptr network(new SharedNetwork6("frog"));
+ ASSERT_NO_THROW(network->add(subnet1));
+ ASSERT_NO_THROW(network->add(subnet2));
+
+ // Make sure they have been added successfully.
+ ASSERT_EQ(2, network->getAllSubnets()->size());
+
+ ASSERT_NO_THROW(network->delAll());
+
+ // Now check that there are no subnets.
+ ASSERT_EQ(0, network->getAllSubnets()->size());
+}
+
+// This test verifies that the IPv4 shared network can be fetched by name.
+TEST(SharedNetworkFetcherTest, getSharedNetwork4ByName) {
+ SharedNetwork4Collection collection;
+
+ // Shared network hasn't been added to the collection. A null pointer should
+ // be returned.
+ auto network = SharedNetworkFetcher4::get(collection, "network1");
+ EXPECT_FALSE(network);
+
+ network.reset(new SharedNetwork4("network1"));
+ EXPECT_NO_THROW(collection.push_back(network));
+
+ network.reset(new SharedNetwork4("network2"));
+ EXPECT_NO_THROW(collection.push_back(network));
+
+ network = SharedNetworkFetcher4::get(collection, "network1");
+ ASSERT_TRUE(network);
+ EXPECT_EQ("network1", network->getName());
+
+ network = SharedNetworkFetcher4::get(collection, "network2");
+ ASSERT_TRUE(network);
+ EXPECT_EQ("network2", network->getName());
+}
+
+// This test verifies that the IPv6 shared network can be fetched by name.
+TEST(SharedNetworkFetcherTest, getSharedNetwork6ByName) {
+ SharedNetwork6Collection collection;
+
+ // Shared network hasn't been added to the collection. A null pointer should
+ // be returned.
+ auto network = SharedNetworkFetcher6::get(collection, "network1");
+ EXPECT_FALSE(network);
+
+ network.reset(new SharedNetwork6("network1"));
+ EXPECT_NO_THROW(collection.push_back(network));
+
+ network.reset(new SharedNetwork6("network2"));
+ EXPECT_NO_THROW(collection.push_back(network));
+
+ network = SharedNetworkFetcher6::get(collection, "network1");
+ ASSERT_TRUE(network);
+ EXPECT_EQ("network1", network->getName());
+
+ network = SharedNetworkFetcher6::get(collection, "network2");
+ ASSERT_TRUE(network);
+ EXPECT_EQ("network2", network->getName());
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/shared_networks_list_parser_unittest.cc b/src/lib/dhcpsrv/tests/shared_networks_list_parser_unittest.cc
new file mode 100644
index 0000000..cf7516f
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/shared_networks_list_parser_unittest.cc
@@ -0,0 +1,116 @@
+// Copyright (C) 2017-2020 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <cc/data.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfg_shared_networks.h>
+#include <dhcpsrv/shared_network.h>
+#include <dhcpsrv/parsers/shared_networks_list_parser.h>
+#include <gtest/gtest.h>
+#include <string>
+
+using namespace isc;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+
+namespace {
+
+// This is a basic test verifying that all shared networks are correctly
+// parsed.
+TEST(SharedNetworkListParserTest, parse) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ // Basic configuration with array of shared networks.
+ std::string config = "["
+ " {"
+ " \"name\": \"bird\","
+ " \"interface\": \"eth0\""
+ " },"
+ " {"
+ " \"name\": \"monkey\","
+ " \"interface\": \"eth1\","
+ " \"user-context\": { \"comment\": \"example\" }"
+ " }"
+ "]";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ SharedNetworks4ListParser parser;
+ CfgSharedNetworks4Ptr cfg(new CfgSharedNetworks4());
+ ASSERT_NO_THROW(parser.parse(cfg, config_element));
+
+ SharedNetwork4Ptr network1 = cfg->getByName("bird");
+ ASSERT_TRUE(network1);
+ EXPECT_EQ("bird", network1->getName());
+ EXPECT_EQ("eth0", network1->getIface().get());
+ EXPECT_FALSE(network1->getContext());
+
+ SharedNetwork4Ptr network2 = cfg->getByName("monkey");
+ ASSERT_TRUE(network2);
+ EXPECT_EQ("monkey", network2->getName());
+ EXPECT_EQ("eth1", network2->getIface().get());
+ ASSERT_TRUE(network2->getContext());
+ EXPECT_EQ(1, network2->getContext()->size());
+ EXPECT_TRUE(network2->getContext()->get("comment"));
+}
+
+// This test verifies that specifying two networks with the same name
+// yields an error.
+TEST(SharedNetworkListParserTest, duplicatedName) {
+ IfaceMgrTestConfig ifmgr(true);
+
+ // Basic configuration with two networks having the same name.
+ std::string config = "["
+ " {"
+ " \"name\": \"bird\","
+ " \"interface\": \"eth0\""
+ " },"
+ " {"
+ " \"name\": \"bird\","
+ " \"interface\": \"eth1\""
+ " }"
+ "]";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ SharedNetworks4ListParser parser;
+ CfgSharedNetworks4Ptr cfg(new CfgSharedNetworks4());
+ EXPECT_THROW(parser.parse(cfg, config_element), DhcpConfigError);
+}
+
+// This test verifies that the optional interface check works as expected.
+TEST(SharedNetworkListParserTest, iface) {
+ // Basic configuration with a shared network.
+ std::string config = "["
+ " {"
+ " \"name\": \"bird\","
+ " \"interface\": \"eth1961\""
+ " }"
+ "]";
+
+ ElementPtr config_element = Element::fromJSON(config);
+
+ // The interface check can be disabled.
+ SharedNetworks6ListParser parser_no_check(false);
+ CfgSharedNetworks6Ptr cfg(new CfgSharedNetworks6());
+ EXPECT_NO_THROW(parser_no_check.parse(cfg, config_element));
+ cfg.reset(new CfgSharedNetworks6());
+
+ // Retry with the interface check enabled.
+ SharedNetworks6ListParser parser;
+ EXPECT_THROW(parser.parse(cfg, config_element), DhcpConfigError);
+ cfg.reset(new CfgSharedNetworks6());
+
+ // Configure default test interfaces.
+ IfaceMgrTestConfig ifmgr(true);
+ EXPECT_NO_THROW(parser_no_check.parse(cfg, config_element));
+ cfg.reset(new CfgSharedNetworks6());
+ EXPECT_NO_THROW(parser.parse(cfg, config_element));
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/srv_config_unittest.cc b/src/lib/dhcpsrv/tests/srv_config_unittest.cc
new file mode 100644
index 0000000..1e90ae9
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/srv_config_unittest.cc
@@ -0,0 +1,2241 @@
+// Copyright (C) 2014-2022 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/client_class_def.h>
+#include <dhcpsrv/srv_config.h>
+#include <dhcpsrv/subnet.h>
+#include <process/logging_info.h>
+#include <testutils/gtest_utils.h>
+#include <testutils/test_to_element.h>
+
+#include <gtest/gtest.h>
+
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::data;
+using namespace isc::process;
+using namespace isc::util;
+
+// Those are the tests for SrvConfig storage. Right now they are minimal,
+// but the number is expected to grow significantly once we migrate more
+// parameters from CfgMgr storage to SrvConfig storage.
+
+namespace {
+
+/// @brief Derivation of the @c ConfigBase not being @c SrvConfig.
+///
+/// This is used to verify that the appropriate error is returned
+/// when other derivation of the @c ConfigBase than @c SrvConfig
+/// is used.
+class NonSrvConfig : public ConfigBase { };
+
+/// @brief Number of IPv4 and IPv6 subnets to be created for a test.
+const int TEST_SUBNETS_NUM = 3;
+
+/// @brief Test fixture class for testing configuration data storage.
+class SrvConfigTest : public ::testing::Test {
+public:
+ /// @brief Constructor.
+ ///
+ /// Creates IPv4 and IPv6 subnets for unit test. The number of subnets
+ /// is @c TEST_SUBNETS_NUM for IPv4 and IPv6 each.
+ SrvConfigTest()
+ : iface_mgr_test_config_(true),
+ ref_dictionary_(new ClientClassDictionary()) {
+
+ // Disable DDNS.
+ enableD2Client(false);
+
+ // Create IPv4 subnets.
+ for (int i = 0; i < TEST_SUBNETS_NUM; ++i) {
+ // Default triplet carried undefined value.
+ Triplet<uint32_t> def_triplet;
+ // Create a collection of subnets: 192.0.X.0/24 where X is
+ // 0, 1, 2 etc.
+ Subnet4Ptr subnet(new Subnet4(IOAddress(0xC0000000 | (i << 2)),
+ 24, def_triplet, def_triplet,
+ 4000));
+ test_subnets4_.insert(subnet);
+ }
+ // Create IPv6 subnets.
+ IOAddress prefix("2001:db8:1::");
+ for (int i = 0; i < TEST_SUBNETS_NUM; ++i) {
+ // This is a base prefix. All other prefixes will be created by
+ // modifying this one.
+ std::vector<uint8_t> prefix_bytes = prefix.toBytes();
+ // Modify 5th byte of the prefix, so 2001:db8:1::0 becomes
+ // 2001:db8:2::0 etc.
+ ++prefix_bytes[5];
+ prefix = IOAddress::fromBytes(prefix.getFamily(), &prefix_bytes[0]);
+ Subnet6Ptr subnet(new Subnet6(prefix, 64, 1000, 2000, 3000, 4000));
+ test_subnets6_.insert(subnet);
+ }
+
+ // Build our reference dictionary of client classes
+ ref_dictionary_->addClass("cc1", ExpressionPtr(),
+ "", false, false, CfgOptionPtr());
+ ref_dictionary_->addClass("cc2", ExpressionPtr(),
+ "", false, false, CfgOptionPtr());
+ ref_dictionary_->addClass("cc3", ExpressionPtr(),
+ "", false, false, CfgOptionPtr());
+ }
+
+
+ /// @brief Destructor.
+ virtual ~SrvConfigTest() {
+ }
+
+ /// @brief Convenience function which adds IPv4 subnet to the configuration.
+ ///
+ /// @param index Index of the subnet in the @c test_subnets4_ collection
+ /// which should be added to the configuration. The configuration is stored
+ /// in the @ conf_ member. This value must be lower than
+ /// @c TEST_SUBNETS_NUM.
+ ///
+ /// @todo Until the subnets configuration is migrated from the @c CfgMgr to
+ /// the @c SrvConfig object, this function adds the subnet to the
+ /// @c CfgMgr. Once, the subnet configuration is held in the
+ /// @c SrvConfig this function must be modified to store the subnets in
+ /// the @c conf_ object.
+ void addSubnet4(const unsigned int index);
+
+ /// @brief Convenience function which adds IPv6 subnet to the configuration.
+ ///
+ /// @param index Index of the subnet in the @c test_subnets6_ collection
+ /// which should be added to the configuration. The configuration is stored
+ /// in the @ conf_ member. This value must be lower than
+ /// @c TEST_SUBNETS_NUM.
+ ///
+ /// @todo Until the subnets configuration is migrated from the @c CfgMgr to
+ /// the @c SrvConfig object, this function adds the subnet to the
+ /// @c CfgMgr. Once, the subnet configuration is held in the
+ /// @c SrvConfig this function must be modified to store the subnets in
+ /// @c conf_ object.
+ void addSubnet6(const unsigned int index);
+
+ /// @brief Enable/disable DDNS.
+ ///
+ /// @param enable A boolean value indicating if the DDNS should be
+ /// enabled (true) or disabled (false).
+ void enableD2Client(const bool enable);
+
+ /// @brief Stores configuration.
+ SrvConfig conf_;
+ /// @brief A collection of IPv4 subnets used by unit tests.
+ Subnet4Collection test_subnets4_;
+ /// @brief A collection of IPv6 subnets used by unit tests.
+ Subnet6Collection test_subnets6_;
+ /// @brief Fakes interface configuration.
+ isc::dhcp::test::IfaceMgrTestConfig iface_mgr_test_config_;
+
+ /// @brief Client class dictionary with fixed content
+ ClientClassDictionaryPtr ref_dictionary_;
+};
+
+void
+SrvConfigTest::addSubnet4(const unsigned int index) {
+ if (index >= TEST_SUBNETS_NUM) {
+ FAIL() << "Subnet index " << index << "out of range (0.."
+ << TEST_SUBNETS_NUM << "): unable to add IPv4 subnet";
+ }
+ // std::advance is not available for this iterator.
+ auto it = test_subnets4_.begin();
+ for (unsigned int i = 0; i < index; ++i, ++it) {
+ ASSERT_FALSE(it == test_subnets4_.end());
+ }
+ conf_.getCfgSubnets4()->add(*it);
+}
+
+void
+SrvConfigTest::addSubnet6(const unsigned int index) {
+ if (index >= TEST_SUBNETS_NUM) {
+ FAIL() << "Subnet index " << index << "out of range (0.."
+ << TEST_SUBNETS_NUM << "): unable to add IPv6 subnet";
+ }
+ // std::advance is not available for this iterator.
+ auto it = test_subnets6_.begin();
+ for (unsigned int i = 0; i < index; ++i, ++it) {
+ ASSERT_FALSE(it == test_subnets6_.end());
+ }
+ conf_.getCfgSubnets6()->add(*it);
+}
+
+void
+SrvConfigTest::enableD2Client(const bool enable) {
+ const D2ClientConfigPtr& d2_config = conf_.getD2ClientConfig();
+ ASSERT_TRUE(d2_config);
+ d2_config->enableUpdates(enable);
+}
+
+// Check that by default there are no logging entries
+TEST_F(SrvConfigTest, basic) {
+ EXPECT_TRUE(conf_.getLoggingInfo().empty());
+}
+
+// Check that SrvConfig can store logging information.
+TEST_F(SrvConfigTest, loggingInfo) {
+ LoggingInfo log1;
+ log1.clearDestinations();
+ log1.name_ = "foo";
+ log1.severity_ = isc::log::WARN;
+ log1.debuglevel_ = 77;
+
+ LoggingDestination dest;
+ dest.output_ = "some-logfile.txt";
+ dest.maxver_ = 5;
+ dest.maxsize_ = 2097152;
+
+ log1.destinations_.push_back(dest);
+
+ conf_.addLoggingInfo(log1);
+
+ EXPECT_EQ("foo", conf_.getLoggingInfo()[0].name_);
+ EXPECT_EQ(isc::log::WARN, conf_.getLoggingInfo()[0].severity_);
+ EXPECT_EQ(77, conf_.getLoggingInfo()[0].debuglevel_);
+
+ EXPECT_EQ("some-logfile.txt", conf_.getLoggingInfo()[0].destinations_[0].output_);
+ EXPECT_EQ(5, conf_.getLoggingInfo()[0].destinations_[0].maxver_);
+ EXPECT_EQ(2097152, conf_.getLoggingInfo()[0].destinations_[0].maxsize_);
+}
+
+// Check that the configuration summary including information about the status
+// of DDNS is returned.
+TEST_F(SrvConfigTest, summaryDDNS) {
+ EXPECT_EQ("DDNS: disabled",
+ conf_.getConfigSummary(SrvConfig::CFGSEL_DDNS));
+
+ enableD2Client(true);
+ EXPECT_EQ("DDNS: enabled",
+ conf_.getConfigSummary(SrvConfig::CFGSEL_DDNS));
+
+ enableD2Client(false);
+ EXPECT_EQ("no IPv4 subnets!; no IPv6 subnets!; DDNS: disabled",
+ conf_.getConfigSummary(SrvConfig::CFGSEL_ALL));
+}
+
+// Check that the configuration summary including information about added
+// subnets is returned.
+TEST_F(SrvConfigTest, summarySubnets) {
+ EXPECT_EQ("no config details available",
+ conf_.getConfigSummary(SrvConfig::CFGSEL_NONE));
+
+ // Initially, there are no subnets added but it should be explicitly
+ // reported when we query for information about the subnets.
+ EXPECT_EQ("no IPv4 subnets!; no IPv6 subnets!",
+ conf_.getConfigSummary(SrvConfig::CFGSEL_SUBNET));
+
+ // If we just want information about IPv4 subnets, there should be no
+ // mention of IPv6 subnets, even though there are none added.
+ EXPECT_EQ("no IPv4 subnets!",
+ conf_.getConfigSummary(SrvConfig::CFGSEL_SUBNET4));
+
+ // If we just want information about IPv6 subnets, there should be no
+ // mention of IPv4 subnets, even though there are none added.
+ EXPECT_EQ("no IPv6 subnets!",
+ conf_.getConfigSummary(SrvConfig::CFGSEL_SUBNET6));
+
+ // Add IPv4 subnet and make sure it is reported.
+ addSubnet4(0);
+ EXPECT_EQ("added IPv4 subnets: 1",
+ conf_.getConfigSummary(SrvConfig::CFGSEL_SUBNET4));
+ EXPECT_EQ("added IPv4 subnets: 1; no IPv6 subnets!",
+ conf_.getConfigSummary(SrvConfig::CFGSEL_SUBNET));
+
+ // Add IPv6 subnet and make sure it is reported.
+ addSubnet6(0);
+ EXPECT_EQ("added IPv6 subnets: 1",
+ conf_.getConfigSummary(SrvConfig::CFGSEL_SUBNET6));
+ EXPECT_EQ("added IPv4 subnets: 1; added IPv6 subnets: 1",
+ conf_.getConfigSummary(SrvConfig::CFGSEL_SUBNET));
+
+ // Add one more subnet and make sure the bumped value is only
+ // for IPv4, but not for IPv6.
+ addSubnet4(1);
+ EXPECT_EQ("added IPv4 subnets: 2; added IPv6 subnets: 1",
+ conf_.getConfigSummary(SrvConfig::CFGSEL_SUBNET));
+ EXPECT_EQ("added IPv4 subnets: 2",
+ conf_.getConfigSummary(SrvConfig::CFGSEL_SUBNET4));
+
+ addSubnet6(1);
+ EXPECT_EQ("added IPv4 subnets: 2; added IPv6 subnets: 2",
+ conf_.getConfigSummary(SrvConfig::CFGSEL_SUBNET));
+}
+
+// Verifies that we can get and set the client class dictionary
+TEST_F(SrvConfigTest, classDictionaryBasics) {
+ ClientClassDictionaryPtr d1;
+ SrvConfig conf(32);
+
+ // Upon construction the dictionary should be empty.
+ ASSERT_TRUE(d1 = conf.getClientClassDictionary());
+ EXPECT_EQ(0, d1->getClasses()->size());
+
+ // Verify we can replace it with a new dictionary.
+ ASSERT_NO_THROW(conf.setClientClassDictionary(ref_dictionary_));
+ ASSERT_TRUE(d1 = conf.getClientClassDictionary());
+ EXPECT_EQ(ref_dictionary_->getClasses()->size(), d1->getClasses()->size());
+
+ // Verify const fetcher works too.
+ const ClientClassDictionaryPtr cd = conf.getClientClassDictionary();
+ ASSERT_TRUE(cd);
+ EXPECT_EQ(ref_dictionary_->getClasses()->size(), cd->getClasses()->size());
+}
+
+// This test verifies that RFC6842 (echo client-id) compatibility may be
+// configured.
+TEST_F(SrvConfigTest, echoClientId) {
+ SrvConfig conf;
+
+ // Check that the default is true
+ EXPECT_TRUE(conf.getEchoClientId());
+
+ // Check that it can be modified to false
+ conf.setEchoClientId(false);
+ EXPECT_FALSE(conf.getEchoClientId());
+
+ // Check that the default value can be restored
+ conf.setEchoClientId(true);
+ EXPECT_TRUE(conf.getEchoClientId());
+
+ // Check the other constructor has the same default
+ SrvConfig conf1(1);
+ EXPECT_TRUE(conf1.getEchoClientId());
+}
+
+// This test verifies that host reservations lookup first flag can be configured.
+TEST_F(SrvConfigTest, reservationsLookupFirst) {
+ SrvConfig conf;
+
+ // Check that the default is false
+ EXPECT_FALSE(conf.getReservationsLookupFirst());
+
+ // Check that it can be modified to true
+ conf.setReservationsLookupFirst(true);
+ EXPECT_TRUE(conf.getReservationsLookupFirst());
+
+ // Check that the default value can be restored
+ conf.setReservationsLookupFirst(false);
+ EXPECT_FALSE(conf.getReservationsLookupFirst());
+
+ // Check the other constructor has the same default
+ SrvConfig conf1(1);
+ EXPECT_FALSE(conf1.getReservationsLookupFirst());
+}
+
+// This test checks if entire configuration can be copied and that the sequence
+// number is not affected.
+TEST_F(SrvConfigTest, copy) {
+ // Create two configurations with different sequence numbers.
+ SrvConfig conf1(32);
+ SrvConfig conf2(64);
+
+ // Set logging information for conf1.
+ LoggingInfo info;
+ info.name_ = "foo";
+ info.severity_ = isc::log::DEBUG;
+ info.debuglevel_ = 64;
+ info.destinations_.push_back(LoggingDestination());
+
+ // Set interface configuration for conf1.
+ conf1.getCfgIface()->use(AF_INET, "eth0");
+ conf1.addLoggingInfo(info);
+
+ // Add option definition.
+ OptionDefinitionPtr def(new OptionDefinition("option-foo", 5, "isc", "string"));
+ conf1.getCfgOptionDef()->add(def);
+
+ // Add an option.
+ OptionPtr option(new Option(Option::V6, 1000, OptionBuffer(10, 0xFF)));
+ conf1.getCfgOption()->add(option, true, DHCP6_OPTION_SPACE);
+
+ // Add a class dictionary
+ conf1.setClientClassDictionary(ref_dictionary_);
+
+ // Make sure both configurations are different.
+ ASSERT_TRUE(conf1 != conf2);
+
+ // Copy conf1 to conf2.
+ ASSERT_NO_THROW(conf1.copy(conf2));
+
+ // Now they should be equal.
+ EXPECT_TRUE(conf1 == conf2);
+
+ // But, their sequence numbers should be unequal.
+ EXPECT_FALSE(conf1.sequenceEquals(conf2));
+}
+
+// This test checks that two configurations can be compared for (in)equality.
+TEST_F(SrvConfigTest, equality) {
+ SrvConfig conf1(32);
+ SrvConfig conf2(64);
+
+ // Initially, both objects should be equal, even though the configuration
+ // sequences are not matching.
+ EXPECT_TRUE(conf1 == conf2);
+ EXPECT_FALSE(conf1 != conf2);
+
+ // Differ by logging information.
+ LoggingInfo info1;
+ LoggingInfo info2;
+ info1.name_ = "foo";
+ info2.name_ = "bar";
+
+ conf1.addLoggingInfo(info1);
+ conf2.addLoggingInfo(info2);
+
+ EXPECT_FALSE(conf1 == conf2);
+ EXPECT_TRUE(conf1 != conf2);
+
+ conf1.addLoggingInfo(info2);
+ conf2.addLoggingInfo(info1);
+
+ EXPECT_TRUE(conf1 == conf2);
+ EXPECT_FALSE(conf1 != conf2);
+
+ // Differ by interface configuration.
+ conf1.getCfgIface()->use(AF_INET, "eth0");
+
+ EXPECT_FALSE(conf1 == conf2);
+ EXPECT_TRUE(conf1 != conf2);
+
+ conf2.getCfgIface()->use(AF_INET, "eth0");
+
+ EXPECT_TRUE(conf1 == conf2);
+ EXPECT_FALSE(conf1 != conf2);
+
+ // Differ by option definitions.
+ conf1.getCfgOptionDef()->
+ add(OptionDefinitionPtr(new OptionDefinition("option-foo", 123, "isc",
+ "uint16_t")));
+
+ EXPECT_FALSE(conf1 == conf2);
+ EXPECT_TRUE(conf1 != conf2);
+
+ conf2.getCfgOptionDef()->
+ add(OptionDefinitionPtr(new OptionDefinition("option-foo", 123, "isc",
+ "uint16_t")));
+ EXPECT_TRUE(conf1 == conf2);
+ EXPECT_FALSE(conf1 != conf2);
+
+ // Differ by option data.
+ OptionPtr option(new Option(Option::V6, 1000, OptionBuffer(1, 0xFF)));
+ conf1.getCfgOption()->add(option, false, "isc");
+
+ EXPECT_FALSE(conf1 == conf2);
+ EXPECT_TRUE(conf1 != conf2);
+
+ conf2.getCfgOption()->add(option, false, "isc");
+
+ EXPECT_TRUE(conf1 == conf2);
+ EXPECT_FALSE(conf1 != conf2);
+
+ // Add a class dictionary to conf1
+ conf1.setClientClassDictionary(ref_dictionary_);
+ EXPECT_FALSE(conf1 == conf2);
+ EXPECT_TRUE(conf1 != conf2);
+
+ // Add same class dictionary to conf2
+ conf2.setClientClassDictionary(ref_dictionary_);
+ EXPECT_TRUE(conf1 == conf2);
+ EXPECT_FALSE(conf1 != conf2);
+}
+
+// Verifies that we can get and set configured hooks libraries
+TEST_F(SrvConfigTest, hooksLibraries) {
+ SrvConfig conf(32);
+ isc::hooks::HooksConfig& libraries = conf.getHooksConfig();
+
+ // Upon construction configured hooks libraries should be empty.
+ EXPECT_EQ(0, libraries.get().size());
+
+ // Verify we can update it.
+ ConstElementPtr elem0;
+ libraries.add("foo", elem0);
+ std::string config = "{ \"library\": \"bar\" }";
+ ConstElementPtr elem1 = Element::fromJSON(config);
+ libraries.add("bar", elem1);
+ EXPECT_EQ(2, libraries.get().size());
+ EXPECT_EQ(2, conf.getHooksConfig().get().size());
+
+ // Try to copy
+ SrvConfig copied(64);
+ ASSERT_TRUE(conf != copied);
+ ASSERT_NO_THROW(conf.copy(copied));
+ ASSERT_TRUE(conf == copied);
+ EXPECT_EQ(2, copied.getHooksConfig().get().size());
+
+ EXPECT_TRUE(copied.getHooksConfig().equal(conf.getHooksConfig()));
+}
+
+// Verifies basic functions of configured global handling.
+TEST_F(SrvConfigTest, configuredGlobals) {
+ // Create an instance.
+ SrvConfig conf(32);
+
+ // The map of configured globals should be empty.
+ ConstCfgGlobalsPtr srv_globals = conf.getConfiguredGlobals();
+ ASSERT_TRUE(srv_globals);
+ ASSERT_TRUE(srv_globals->valuesMap().empty());
+
+ // Attempting to extract globals from a non-map should throw.
+ ASSERT_THROW(conf.extractConfiguredGlobals(Element::create(777)), isc::BadValue);
+
+ // Now let's create a configuration from which to extract global scalars.
+ // Extraction (currently) has no business logic, so the elements we use
+ // can be arbitrary when not scalar.
+ ConstElementPtr global_cfg;
+ std::string global_cfg_str =
+ "{\n"
+ " \"comment\": \"okay\",\n" // a string
+ " \"amap\": { \"not-this\":777, \"not-that\": \"poo\" },\n"
+ " \"valid-lifetime\": 444,\n" // an int
+ " \"alist\": [ 1, 2, 3 ],\n"
+ " \"store-extended-info\": true,\n" // a bool
+ " \"t1-percent\": 1.234\n" // a real
+ "}\n";
+ ASSERT_NO_THROW(global_cfg = Element::fromJSON(global_cfg_str));
+
+ // Extract globals from the config.
+ ASSERT_NO_THROW(conf.extractConfiguredGlobals(global_cfg));
+
+ // Now see if the extract was correct.
+ srv_globals = conf.getConfiguredGlobals();
+ ASSERT_FALSE(srv_globals->valuesMap().empty());
+
+ // Maps and lists should be excluded.
+ auto globals = srv_globals->valuesMap();
+ for (auto global = globals.begin(); global != globals.end(); ++global) {
+ if (global->first == "comment") {
+ ASSERT_EQ(Element::string, global->second->getType());
+ EXPECT_EQ("okay", global->second->stringValue());
+ } else if (global->first == "valid-lifetime") {
+ ASSERT_EQ(Element::integer, global->second->getType());
+ EXPECT_EQ(444, global->second->intValue());
+ } else if (global->first == "store-extended-info") {
+ ASSERT_EQ(Element::boolean, global->second->getType());
+ EXPECT_TRUE(global->second->boolValue());
+ } else if (global->first == "t1-percent") {
+ ASSERT_EQ(Element::real, global->second->getType());
+ EXPECT_EQ(1.234, global->second->doubleValue());
+ } else {
+ ADD_FAILURE() << "unexpected element found:" << global->first;
+ }
+ }
+
+ // Verify that using getConfiguredGlobal() to fetch an individual
+ // parameters works.
+ ConstElementPtr global;
+ // We should find global "comment".
+ ASSERT_NO_THROW(global = conf.getConfiguredGlobal("comment"));
+ ASSERT_TRUE(global);
+ ASSERT_EQ(Element::string, global->getType());
+ EXPECT_EQ("okay", global->stringValue());
+
+ // Not finding global "not-there" should throw.
+ // without throwing.
+ ASSERT_THROW(conf.getConfiguredGlobal("not-there"), isc::NotFound);
+}
+
+// Verifies that the toElement method works well (tests limited to
+// direct parameters)
+TEST_F(SrvConfigTest, unparse) {
+ SrvConfig conf(32);
+ std::string header4 = "{\n\"Dhcp4\": {\n";
+ std::string header6 = "{\n\"Dhcp6\": {\n";
+
+ std::string defaults = "\"decline-probation-period\": 0,\n";
+ defaults += "\"interfaces-config\": { \"interfaces\": [ ],\n";
+ defaults += " \"re-detect\": false },\n";
+ defaults += "\"option-def\": [ ],\n";
+ defaults += "\"option-data\": [ ],\n";
+ defaults += "\"expired-leases-processing\": ";
+ defaults += conf.getCfgExpiration()->toElement()->str() + ",\n";
+ defaults += "\"lease-database\": { \"type\": \"memfile\" },\n";
+ defaults += "\"hooks-libraries\": [ ],\n";
+ defaults += "\"sanity-checks\": {\n";
+ defaults += " \"lease-checks\": \"none\"\n";
+ defaults += " },\n";
+ defaults += "\"dhcp-ddns\": \n";
+
+ defaults += conf.getD2ClientConfig()->toElement()->str() + ",\n";
+
+ std::string defaults4 = "\"shared-networks\": [ ],\n";
+ defaults4 += "\"subnet4\": [ ],\n";
+ defaults4 += "\"host-reservation-identifiers\": ";
+ defaults4 += "[ \"hw-address\", \"duid\", \"circuit-id\", \"client-id\" ],\n";
+
+ std::string defaults6 = "\"relay-supplied-options\": [ \"65\" ],\n";
+ defaults6 += "\"shared-networks\": [ ],\n";
+ defaults6 += "\"subnet6\": [ ],\n";
+ defaults6 += "\"server-id\": ";
+ defaults6 += conf.getCfgDUID()->toElement()->str() + ",\n";
+ defaults6 += "\"host-reservation-identifiers\": ";
+ defaults6 += "[ \"hw-address\", \"duid\" ],\n";
+ defaults6 += "\"dhcp4o6-port\": 0,\n";
+ defaults6 += "\"mac-sources\": [ \"any\" ]\n";
+
+ std::string params = "\"echo-client-id\": true,\n";
+ params += "\"dhcp4o6-port\": 0\n";
+ std::string trailer = "}\n}\n";
+
+ // Verify DHCPv4
+ CfgMgr::instance().setFamily(AF_INET);
+ isc::test::runToElementTest<SrvConfig>
+ (header4 + defaults + defaults4 + params + trailer, conf);
+
+ // Verify DHCPv6
+ CfgMgr::instance().setFamily(AF_INET6);
+ isc::test::runToElementTest<SrvConfig>
+ (header6 + defaults + defaults6 + trailer, conf);
+
+ // Verify direct non-default parameters and configured globals
+ CfgMgr::instance().setFamily(AF_INET);
+ conf.setEchoClientId(false);
+ conf.setDhcp4o6Port(6767);
+ // Add "configured globals"
+ conf.addConfiguredGlobal("renew-timer", Element::create(777));
+ conf.addConfiguredGlobal("comment", Element::create("bar"));
+ params = "\"echo-client-id\": false,\n";
+ params += "\"dhcp4o6-port\": 6767,\n";
+ params += "\"renew-timer\": 777,\n";
+ params += "\"comment\": \"bar\"\n";
+ isc::test::runToElementTest<SrvConfig>
+ (header4 + defaults + defaults4 + params + trailer, conf);
+
+ // Verify direct non-default parameters and configured globals
+ CfgMgr::instance().setFamily(AF_INET6);
+ params = ",\"dhcp4o6-port\": 6767,\n";
+ params += "\"renew-timer\": 777,\n";
+ params += "\"comment\": \"bar\"\n";
+ isc::test::runToElementTest<SrvConfig>
+ (header6 + defaults + defaults6 + params + trailer, conf);
+}
+
+// Verifies that the toElement method does not miss host reservations
+TEST_F(SrvConfigTest, unparseHR) {
+ // DHCPv4 version
+ CfgMgr::instance().setFamily(AF_INET);
+ SrvConfig conf4(32);
+
+ // Add a plain subnet
+ Triplet<uint32_t> def_triplet;
+ SubnetID p_id(1);
+ Subnet4Ptr psubnet4(new Subnet4(IOAddress("192.0.1.0"), 24,
+ def_triplet, def_triplet, 4000, p_id));
+ conf4.getCfgSubnets4()->add(psubnet4);
+
+ // Add a shared network
+ SharedNetwork4Ptr network4(new SharedNetwork4("frog"));
+ conf4.getCfgSharedNetworks4()->add(network4);
+
+ // Add a shared subnet
+ SubnetID s_id(100);
+ Subnet4Ptr ssubnet4(new Subnet4(IOAddress("192.0.2.0"), 24,
+ def_triplet, def_triplet, 4000, s_id));
+ network4->add(ssubnet4);
+
+ // Add a v4 global host reservation to the plain subnet
+ HostPtr ghost4(new Host("AA:01:02:03:04:05", "hw-address",
+ SUBNET_ID_GLOBAL, SUBNET_ID_UNUSED,
+ IOAddress("192.0.3.1")));
+ conf4.getCfgHosts()->add(ghost4);
+
+ // Add a host reservation to the plain subnet
+ HostPtr phost4(new Host("00:01:02:03:04:05", "hw-address",
+ p_id, SUBNET_ID_UNUSED, IOAddress("192.0.1.1")));
+ conf4.getCfgHosts()->add(phost4);
+
+ // Add a host reservation to the shared subnet
+ HostPtr shost4(new Host("00:05:04:03:02:01", "hw-address",
+ s_id, SUBNET_ID_UNUSED, IOAddress("192.0.2.1")));
+ conf4.getCfgHosts()->add(shost4);
+
+ // Unparse the config
+ ConstElementPtr unparsed4 = conf4.toElement();
+ ASSERT_TRUE(unparsed4);
+ ASSERT_EQ(Element::map, unparsed4->getType());
+
+ // Get Dhcp4 entry
+ ConstElementPtr dhcp4;
+ ASSERT_NO_THROW(dhcp4 = unparsed4->get("Dhcp4"));
+ ASSERT_TRUE(dhcp4);
+ ASSERT_EQ(Element::map, dhcp4->getType());
+
+ // Get global host reservations
+ ConstElementPtr check;
+ ASSERT_NO_THROW(check = dhcp4->get("reservations"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::list, check->getType());
+ EXPECT_EQ(1, check->size());
+
+ // Get the global host reservation
+ ASSERT_NO_THROW(check = check->get(0));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::map, check->getType());
+
+ // Check the reserved address
+ ASSERT_NO_THROW(check = check->get("ip-address"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::string, check->getType());
+ EXPECT_EQ("192.0.3.1", check->stringValue());
+
+ // Get plain subnets
+ ASSERT_NO_THROW(check = dhcp4->get("subnet4"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::list, check->getType());
+ EXPECT_EQ(1, check->size());
+
+ // Get the plain subnet
+ ASSERT_NO_THROW(check = check->get(0));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::map, check->getType());
+
+ // Its ID is 1
+ ConstElementPtr sub;
+ ASSERT_NO_THROW(sub = check->get("id"));
+ ASSERT_TRUE(sub);
+ ASSERT_EQ(Element::integer, sub->getType());
+ EXPECT_EQ(p_id, sub->intValue());
+
+ // Get its host reservations
+ ASSERT_NO_THROW(check = check->get("reservations"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::list, check->getType());
+ EXPECT_EQ(1, check->size());
+
+ // Get the plain host reservation
+ ASSERT_NO_THROW(check = check->get(0));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::map, check->getType());
+
+ // Check the reserved address
+ ASSERT_NO_THROW(check = check->get("ip-address"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::string, check->getType());
+ EXPECT_EQ("192.0.1.1", check->stringValue());
+
+ // Get shared networks
+ ASSERT_NO_THROW(check = dhcp4->get("shared-networks"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::list, check->getType());
+ EXPECT_EQ(1, check->size());
+
+ // Get the shared network
+ ASSERT_NO_THROW(check = check->get(0));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::map, check->getType());
+
+ // Its name is "frog"
+ ASSERT_NO_THROW(sub = check->get("name"));
+ ASSERT_TRUE(sub);
+ ASSERT_EQ(Element::string, sub->getType());
+ EXPECT_EQ("frog", sub->stringValue());
+
+ // Get shared subnets
+ ASSERT_NO_THROW(check = check->get("subnet4"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::list, check->getType());
+ EXPECT_EQ(1, check->size());
+
+ // Get the shared subnet
+ ASSERT_NO_THROW(check = check->get(0));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::map, check->getType());
+
+ // Its ID is 100
+ ASSERT_NO_THROW(sub = check->get("id"));
+ ASSERT_TRUE(sub);
+ ASSERT_EQ(Element::integer, sub->getType());
+ EXPECT_EQ(s_id, sub->intValue());
+
+ // Get its host reservations
+ ASSERT_NO_THROW(check = check->get("reservations"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::list, check->getType());
+ EXPECT_EQ(1, check->size());
+
+ // Get the shared host reservation
+ ASSERT_NO_THROW(check = check->get(0));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::map, check->getType());
+
+ // Check the reserved address
+ ASSERT_NO_THROW(check = check->get("ip-address"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::string, check->getType());
+ EXPECT_EQ("192.0.2.1", check->stringValue());
+
+ // DHCPv6 version
+ CfgMgr::instance().setFamily(AF_INET6);
+ SrvConfig conf6(32);
+
+ // Add a plain subnet
+ Subnet6Ptr psubnet6(new Subnet6(IOAddress("2001:db8:1::"), 64,
+ 1000, 2000, 3000, 4000, p_id));
+ conf6.getCfgSubnets6()->add(psubnet6);
+
+ // Add a shared network
+ SharedNetwork6Ptr network6(new SharedNetwork6("frog"));
+ conf6.getCfgSharedNetworks6()->add(network6);
+
+ // Add a shared subnet
+ Subnet6Ptr ssubnet6(new Subnet6(IOAddress("2001:db8:2::"), 64,
+ 1000, 2000, 3000, 4000, s_id));
+ network6->add(ssubnet6);
+
+ // Add a v6 global host reservation
+ HostPtr ghost6(new Host("ff:b2:c3:d4:e5:f6", "duid", SUBNET_ID_UNUSED,
+ SUBNET_ID_GLOBAL, IOAddress::IPV4_ZERO_ADDRESS(),
+ "global.example.org"));
+ conf6.getCfgHosts()->add(ghost6);
+
+ // Add a host reservation to the plain subnet
+ HostPtr phost6(new Host("a1:b2:c3:d4:e5:f6", "duid", SUBNET_ID_UNUSED,
+ p_id, IOAddress::IPV4_ZERO_ADDRESS(),
+ "foo.example.org"));
+
+ conf6.getCfgHosts()->add(phost6);
+
+ // Add a host reservation to the shared subnet
+ HostPtr shost6(new Host("f6:e5:d4:c3:b2:a1", "duid", SUBNET_ID_UNUSED,
+ s_id, IOAddress::IPV4_ZERO_ADDRESS(),
+ "bar.example.org"));
+ conf6.getCfgHosts()->add(shost6);
+
+ // Unparse the config
+ ConstElementPtr unparsed6 = conf6.toElement();
+ ASSERT_TRUE(unparsed6);
+ ASSERT_EQ(Element::map, unparsed6->getType());
+
+ // Get Dhcp6 entry
+ ConstElementPtr dhcp6;
+ ASSERT_NO_THROW(dhcp6 = unparsed6->get("Dhcp6"));
+ ASSERT_TRUE(dhcp6);
+ ASSERT_EQ(Element::map, dhcp6->getType());
+
+ // Get global host reservations
+ ASSERT_NO_THROW(check = dhcp6->get("reservations"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::list, check->getType());
+ EXPECT_EQ(1, check->size());
+
+ // Get the global host reservation
+ ASSERT_NO_THROW(check = check->get(0));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::map, check->getType());
+
+ // Check the host name
+ ASSERT_NO_THROW(check = check->get("hostname"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::string, check->getType());
+ EXPECT_EQ("global.example.org", check->stringValue());
+
+ // Get plain subnets
+ ASSERT_NO_THROW(check = dhcp6->get("subnet6"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::list, check->getType());
+ EXPECT_EQ(1, check->size());
+
+ // Get the plain subnet
+ ASSERT_NO_THROW(check = check->get(0));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::map, check->getType());
+
+ // Its ID is 1
+ ASSERT_NO_THROW(sub = check->get("id"));
+ ASSERT_TRUE(sub);
+ ASSERT_EQ(Element::integer, sub->getType());
+ EXPECT_EQ(p_id, sub->intValue());
+
+ // Get its host reservations
+ ASSERT_NO_THROW(check = check->get("reservations"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::list, check->getType());
+ EXPECT_EQ(1, check->size());
+
+ // Get the plain host reservation
+ ASSERT_NO_THROW(check = check->get(0));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::map, check->getType());
+
+ // Check the host name
+ ASSERT_NO_THROW(check = check->get("hostname"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::string, check->getType());
+ EXPECT_EQ("foo.example.org", check->stringValue());
+
+ // Get shared networks
+ ASSERT_NO_THROW(check = dhcp6->get("shared-networks"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::list, check->getType());
+ EXPECT_EQ(1, check->size());
+
+ // Get the shared network
+ ASSERT_NO_THROW(check = check->get(0));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::map, check->getType());
+
+ // Its name is "frog"
+ ASSERT_NO_THROW(sub = check->get("name"));
+ ASSERT_TRUE(sub);
+ ASSERT_EQ(Element::string, sub->getType());
+ EXPECT_EQ("frog", sub->stringValue());
+
+ // Get shared subnets
+ ASSERT_NO_THROW(check = check->get("subnet6"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::list, check->getType());
+ EXPECT_EQ(1, check->size());
+
+ // Get the shared subnet
+ ASSERT_NO_THROW(check = check->get(0));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::map, check->getType());
+
+ // Its ID is 100
+ ASSERT_NO_THROW(sub = check->get("id"));
+ ASSERT_TRUE(sub);
+ ASSERT_EQ(Element::integer, sub->getType());
+ EXPECT_EQ(s_id, sub->intValue());
+
+ // Get its host reservations
+ ASSERT_NO_THROW(check = check->get("reservations"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::list, check->getType());
+ EXPECT_EQ(1, check->size());
+
+ // Get the shared host reservation
+ ASSERT_NO_THROW(check = check->get(0));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::map, check->getType());
+
+ // Check the host name
+ ASSERT_NO_THROW(check = check->get("hostname"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::string, check->getType());
+ EXPECT_EQ("bar.example.org", check->stringValue());
+}
+
+// Verifies that the toElement method does not miss config control info
+TEST_F(SrvConfigTest, unparseConfigControlInfo4) {
+ // DHCPv4 version
+ CfgMgr::instance().setFamily(AF_INET);
+ SrvConfig conf4(32);
+
+ // Unparse the config
+ ConstElementPtr unparsed4 = conf4.toElement();
+ ASSERT_TRUE(unparsed4);
+ ASSERT_EQ(Element::map, unparsed4->getType());
+
+ // Get Dhcp4 entry
+ ConstElementPtr dhcp4;
+ ASSERT_NO_THROW(dhcp4 = unparsed4->get("Dhcp4"));
+ ASSERT_TRUE(dhcp4);
+ ASSERT_EQ(Element::map, dhcp4->getType());
+
+ // Config control should not be present.
+ ConstElementPtr check;
+ ASSERT_NO_THROW(check = dhcp4->get("config-control"));
+ EXPECT_FALSE(check);
+
+ // Now let's create the info and add it to the configuration
+ ConfigControlInfoPtr info(new ConfigControlInfo());
+ ASSERT_NO_THROW(info->addConfigDatabase("type=mysql"));
+ ASSERT_NO_THROW(conf4.setConfigControlInfo(info));
+
+ // Unparse the config again
+ unparsed4 = conf4.toElement();
+ ASSERT_NO_THROW(dhcp4 = unparsed4->get("Dhcp4"));
+ ASSERT_TRUE(dhcp4);
+ ASSERT_EQ(Element::map, dhcp4->getType());
+
+ // Config control should be present.
+ ASSERT_NO_THROW(check = dhcp4->get("config-control"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::map, check->getType());
+
+ // Unparse the info object and compare its elements to
+ // that in unparsed config. They should be equal.
+ ElementPtr info_elem = info->toElement();
+ EXPECT_TRUE(info_elem->equals(*check));
+}
+
+// Verifies that the toElement method does not miss config control info
+TEST_F(SrvConfigTest, unparseConfigControlInfo6) {
+ // DHCPv6 version
+ CfgMgr::instance().setFamily(AF_INET6);
+ SrvConfig conf6(32);
+
+ // Unparse the config
+ ConstElementPtr unparsed6 = conf6.toElement();
+ ASSERT_TRUE(unparsed6);
+ ASSERT_EQ(Element::map, unparsed6->getType());
+
+ // Get Dhcp4 entry
+ ConstElementPtr dhcp6;
+ ASSERT_NO_THROW(dhcp6 = unparsed6->get("Dhcp6"));
+ ASSERT_TRUE(dhcp6);
+ ASSERT_EQ(Element::map, dhcp6->getType());
+
+ // Config control should not be present.
+ ConstElementPtr check;
+ ASSERT_NO_THROW(check = dhcp6->get("config-control"));
+ EXPECT_FALSE(check);
+
+ // Now let's create the info and add it to the configuration
+ ConfigControlInfoPtr info(new ConfigControlInfo());
+ ASSERT_NO_THROW(info->addConfigDatabase("type=mysql"));
+ ASSERT_NO_THROW(conf6.setConfigControlInfo(info));
+
+ // Unparse the config again
+ unparsed6 = conf6.toElement();
+ ASSERT_NO_THROW(dhcp6 = unparsed6->get("Dhcp6"));
+ ASSERT_TRUE(dhcp6);
+ ASSERT_EQ(Element::map, dhcp6->getType());
+
+ // Config control should be present.
+ ASSERT_NO_THROW(check = dhcp6->get("config-control"));
+ ASSERT_TRUE(check);
+ ASSERT_EQ(Element::map, check->getType());
+
+ // Unparse the info object and compare its elements to
+ // that in unparsed config. They should be equal.
+ ElementPtr info_elem = info->toElement();
+ EXPECT_TRUE(info_elem->equals(*check));
+}
+
+// Verifies that exception is thrown when instead of SrvConfig
+// another derivation of ConfigBase is used in the call to
+// merge.
+TEST_F(SrvConfigTest, mergeBadCast) {
+ SrvConfig srv_config;
+ NonSrvConfig non_srv_config;
+ ASSERT_THROW(srv_config.merge(non_srv_config), isc::InvalidOperation);
+}
+
+// This test verifies that globals from one SrvConfig
+// can be merged into another. It verifies that values
+// in the from-config override those in to-config which
+// override those in GLOBAL4_DEFAULTS.
+TEST_F(SrvConfigTest, mergeGlobals4) {
+ // Set the family we're working with.
+ CfgMgr::instance().setFamily(AF_INET);
+
+ // Let's create the "existing" config we will merge into.
+ SrvConfig cfg_to;
+
+ // Set some explicit values.
+ cfg_to.setDeclinePeriod(100);
+ cfg_to.setEchoClientId(false);
+ cfg_to.setDhcp4o6Port(777);
+ cfg_to.setServerTag("not_this_server");
+
+ // Add some configured globals
+ cfg_to.addConfiguredGlobal("decline-probation-period", Element::create(300));
+ cfg_to.addConfiguredGlobal("dhcp4o6-port", Element::create(888));
+
+ // Now we'll create the config we'll merge from.
+ SrvConfig cfg_from;
+
+ // Set some explicit values. None of these should be preserved.
+ cfg_from.setDeclinePeriod(200);
+ cfg_from.setEchoClientId(true);
+ cfg_from.setDhcp4o6Port(888);
+ cfg_from.setServerTag("nor_this_server");
+ cfg_from.setReservationsLookupFirst(true);
+
+ // Add a configured global ip-reservations-unique. It should be populated
+ // to the CfgDbAccess and CfgHosts.
+ cfg_from.addConfiguredGlobal("ip-reservations-unique", Element::create(false));
+
+ // Add some configured globals:
+ cfg_to.addConfiguredGlobal("dhcp4o6-port", Element::create(999));
+ cfg_to.addConfiguredGlobal("server-tag", Element::create("use_this_server"));
+ cfg_to.addConfiguredGlobal("reservations-lookup-first", Element::create(true));
+
+ // Now let's merge.
+ ASSERT_NO_THROW(cfg_to.merge(cfg_from));
+
+ // Make sure the explicit values are set correctly.
+
+ // decline-probation-period should be the "to" configured value.
+ EXPECT_EQ(300, cfg_to.getDeclinePeriod());
+
+ // echo-client-id should be the preserved "to" member value.
+ EXPECT_FALSE(cfg_to.getEchoClientId());
+
+ // dhcp4o6-port should be the "from" configured value.
+ EXPECT_EQ(999, cfg_to.getDhcp4o6Port());
+
+ // server-tag port should be the "from" configured value.
+ EXPECT_EQ("use_this_server", cfg_to.getServerTag().get());
+
+ // reservations-lookup-first should be the "from" configured value.
+ EXPECT_TRUE(cfg_to.getReservationsLookupFirst());
+
+ // ip-reservations-unique
+ EXPECT_FALSE(cfg_to.getCfgDbAccess()->getIPReservationsUnique());
+
+ // Next we check the explicitly "configured" globals.
+ // The list should be all of the "to" + "from", with the
+ // latter overwriting the former.
+ std::string exp_globals =
+ "{ \n"
+ " \"decline-probation-period\": 300, \n"
+ " \"dhcp4o6-port\": 999, \n"
+ " \"ip-reservations-unique\": false, \n"
+ " \"server-tag\": \"use_this_server\", \n"
+ " \"reservations-lookup-first\": true"
+ "} \n";
+
+ ConstElementPtr expected_globals;
+ ASSERT_NO_THROW(expected_globals = Element::fromJSON(exp_globals))
+ << "exp_globals didn't parse, test is broken";
+
+ EXPECT_TRUE(isEquivalent(expected_globals,
+ cfg_to.getConfiguredGlobals()->toElement()));
+}
+
+// This test verifies that globals from one SrvConfig
+// can be merged into another. It verifies that values
+// in the from-config override those in to-config which
+// override those in GLOBAL6_DEFAULTS.
+TEST_F(SrvConfigTest, mergeGlobals6) {
+ // Set the family we're working with.
+ CfgMgr::instance().setFamily(AF_INET6);
+
+ // Let's create the "existing" config we will merge into.
+ SrvConfig cfg_to;
+
+ // Set some explicit values.
+ cfg_to.setDeclinePeriod(100);
+ cfg_to.setDhcp4o6Port(777);
+ cfg_to.setServerTag("not_this_server");
+
+ // Add some configured globals
+ cfg_to.addConfiguredGlobal("decline-probation-period", Element::create(300));
+ cfg_to.addConfiguredGlobal("dhcp4o6-port", Element::create(888));
+
+ // Now we'll create the config we'll merge from.
+ SrvConfig cfg_from;
+
+ // Set some explicit values. None of these should be preserved.
+ cfg_from.setDeclinePeriod(200);
+ cfg_from.setEchoClientId(true);
+ cfg_from.setDhcp4o6Port(888);
+ cfg_from.setServerTag("nor_this_server");
+ cfg_from.setReservationsLookupFirst(true);
+
+ // Add a configured global ip-reservations-unique. It should be populated
+ // to the CfgDbAccess and CfgHosts.
+ cfg_from.addConfiguredGlobal("ip-reservations-unique", Element::create(false));
+
+ // Add some configured globals:
+ cfg_to.addConfiguredGlobal("dhcp4o6-port", Element::create(999));
+ cfg_to.addConfiguredGlobal("server-tag", Element::create("use_this_server"));
+ cfg_to.addConfiguredGlobal("reservations-lookup-first", Element::create(true));
+
+ // Now let's merge.
+ ASSERT_NO_THROW(cfg_to.merge(cfg_from));
+
+ // Make sure the explicit values are set correctly.
+
+ // decline-probation-period should be the "to" configured value.
+ EXPECT_EQ(300, cfg_to.getDeclinePeriod());
+
+ // dhcp4o6-port should be the "from" configured value.
+ EXPECT_EQ(999, cfg_to.getDhcp4o6Port());
+
+ // server-tag port should be the "from" configured value.
+ EXPECT_EQ("use_this_server", cfg_to.getServerTag().get());
+
+ // reservations-lookup-first should be the "from" configured value.
+ EXPECT_TRUE(cfg_to.getReservationsLookupFirst());
+
+ // ip-reservations-unique
+ EXPECT_FALSE(cfg_to.getCfgDbAccess()->getIPReservationsUnique());
+
+ // Next we check the explicitly "configured" globals.
+ // The list should be all of the "to" + "from", with the
+ // latter overwriting the former.
+ std::string exp_globals =
+ "{ \n"
+ " \"decline-probation-period\": 300, \n"
+ " \"dhcp4o6-port\": 999, \n"
+ " \"ip-reservations-unique\": false, \n"
+ " \"server-tag\": \"use_this_server\", \n"
+ " \"reservations-lookup-first\": true"
+ "} \n";
+
+ ConstElementPtr expected_globals;
+ ASSERT_NO_THROW(expected_globals = Element::fromJSON(exp_globals))
+ << "exp_globals didn't parse, test is broken";
+
+ EXPECT_TRUE(isEquivalent(expected_globals,
+ cfg_to.getConfiguredGlobals()->toElement()));
+}
+
+// This test verifies that new list of client classes replaces and old list
+// when server configuration is merged.
+TEST_F(SrvConfigTest, mergeClientClasses) {
+ // Let's create the "existing" config we will merge into.
+ SrvConfig cfg_to;
+
+ auto expression = boost::make_shared<Expression>();
+ auto client_class = boost::make_shared<ClientClassDef>("foo", expression);
+ cfg_to.getClientClassDictionary()->addClass(client_class);
+
+ client_class = boost::make_shared<ClientClassDef>("bar", expression);
+ cfg_to.getClientClassDictionary()->addClass(client_class);
+
+ // Now we'll create the config we'll merge from.
+ SrvConfig cfg_from;
+ client_class = boost::make_shared<ClientClassDef>("baz", expression);
+ cfg_from.getClientClassDictionary()->addClass(client_class);
+
+ client_class = boost::make_shared<ClientClassDef>("abc", expression);
+ cfg_from.getClientClassDictionary()->addClass(client_class);
+
+ ASSERT_NO_THROW(cfg_to.merge(cfg_from));
+
+ // The old classes should be replaced with new classes.
+ EXPECT_FALSE(cfg_to.getClientClassDictionary()->findClass("foo"));
+ EXPECT_FALSE(cfg_to.getClientClassDictionary()->findClass("bar"));
+ EXPECT_TRUE(cfg_to.getClientClassDictionary()->findClass("baz"));
+ EXPECT_TRUE(cfg_to.getClientClassDictionary()->findClass("abc"));
+}
+
+// This test verifies that client classes are not modified if the merged
+// list of classes is empty.
+TEST_F(SrvConfigTest, mergeEmptyClientClasses) {
+ // Let's create the "existing" config we will merge into.
+ SrvConfig cfg_to;
+
+ auto expression = boost::make_shared<Expression>();
+ auto client_class = boost::make_shared<ClientClassDef>("foo", expression);
+ cfg_to.getClientClassDictionary()->addClass(client_class);
+
+ client_class = boost::make_shared<ClientClassDef>("bar", expression);
+ cfg_to.getClientClassDictionary()->addClass(client_class);
+
+ // Now we'll create the config we'll merge from.
+ SrvConfig cfg_from;
+
+ ASSERT_NO_THROW(cfg_to.merge(cfg_from));
+
+ // Empty list of classes should not replace an existing list.
+ EXPECT_TRUE(cfg_to.getClientClassDictionary()->findClass("foo"));
+ EXPECT_TRUE(cfg_to.getClientClassDictionary()->findClass("bar"));
+}
+
+// Validates SrvConfig::moveDdnsParams by ensuring that deprecated dhcp-ddns
+// parameters are:
+// 1. Translated to their global counterparts if they do not exist globally
+// 2. Removed from the dhcp-ddns element
+TEST_F(SrvConfigTest, moveDdnsParamsTest) {
+ DdnsParamsPtr params;
+
+ CfgMgr::instance().setFamily(AF_INET);
+
+ struct Scenario {
+ std::string description;
+ std::string input_cfg;
+ std::string exp_cfg;
+ };
+
+ std::vector<Scenario> scenarios {
+ {
+ "scenario 1, move with no global conflicts",
+ // input_cfg
+ "{\n"
+ " \"dhcp-ddns\": {\n"
+ " \"enable-updates\": true, \n"
+ " \"server-ip\" : \"192.0.2.0\",\n"
+ " \"server-port\" : 3432,\n"
+ " \"sender-ip\" : \"192.0.2.1\",\n"
+ " \"sender-port\" : 3433,\n"
+ " \"max-queue-size\" : 2048,\n"
+ " \"ncr-protocol\" : \"UDP\",\n"
+ " \"ncr-format\" : \"JSON\",\n"
+ " \"user-context\": { \"foo\": \"bar\" },\n"
+ " \"override-no-update\": true,\n"
+ " \"override-client-update\": false,\n"
+ " \"replace-client-name\": \"always\",\n"
+ " \"generated-prefix\": \"prefix\",\n"
+ " \"qualifying-suffix\": \"suffix.com.\",\n"
+ " \"hostname-char-set\": \"[^A-Z]\",\n"
+ " \"hostname-char-replacement\": \"x\"\n"
+ " }\n"
+ "}\n",
+ // exp_cfg
+ "{\n"
+ " \"dhcp-ddns\": {\n"
+ " \"enable-updates\": true, \n"
+ " \"server-ip\" : \"192.0.2.0\",\n"
+ " \"server-port\" : 3432,\n"
+ " \"sender-ip\" : \"192.0.2.1\",\n"
+ " \"sender-port\" : 3433,\n"
+ " \"max-queue-size\" : 2048,\n"
+ " \"ncr-protocol\" : \"UDP\",\n"
+ " \"ncr-format\" : \"JSON\",\n"
+ " \"user-context\": { \"foo\": \"bar\" }\n"
+ " },\n"
+ " \"ddns-override-no-update\": true,\n"
+ " \"ddns-override-client-update\": false,\n"
+ " \"ddns-replace-client-name\": \"always\",\n"
+ " \"ddns-generated-prefix\": \"prefix\",\n"
+ " \"ddns-qualifying-suffix\": \"suffix.com.\",\n"
+ " \"hostname-char-set\": \"[^A-Z]\",\n"
+ " \"hostname-char-replacement\": \"x\"\n"
+ "}\n"
+ },
+ {
+ "scenario 2, globals already exist for all movable params",
+ // input_cfg
+ "{\n"
+ " \"dhcp-ddns\" : {\n"
+ " \"enable-updates\": true, \n"
+ " \"override-no-update\": true,\n"
+ " \"override-client-update\": true,\n"
+ " \"replace-client-name\": \"always\",\n"
+ " \"generated-prefix\": \"prefix\",\n"
+ " \"qualifying-suffix\": \"suffix.com.\",\n"
+ " \"hostname-char-set\": \"[^A-Z]\",\n"
+ " \"hostname-char-replacement\": \"x\"\n"
+ " },\n"
+ " \"ddns-override-no-update\": false,\n"
+ " \"ddns-override-client-update\": false,\n"
+ " \"ddns-replace-client-name\": \"when-present\",\n"
+ " \"ddns-generated-prefix\": \"org_prefix\",\n"
+ " \"ddns-qualifying-suffix\": \"org_suffix.com.\",\n"
+ " \"hostname-char-set\": \"[^a-z]\",\n"
+ " \"hostname-char-replacement\": \"y\"\n"
+ "}\n",
+ // exp_cfg
+ "{\n"
+ " \"dhcp-ddns\" : {\n"
+ " \"enable-updates\": true\n"
+ " },\n"
+ " \"ddns-override-no-update\": false,\n"
+ " \"ddns-override-client-update\": false,\n"
+ " \"ddns-replace-client-name\": \"when-present\",\n"
+ " \"ddns-generated-prefix\": \"org_prefix\",\n"
+ " \"ddns-qualifying-suffix\": \"org_suffix.com.\",\n"
+ " \"hostname-char-set\": \"[^a-z]\",\n"
+ " \"hostname-char-replacement\": \"y\"\n"
+ "}\n"
+ },
+ {
+ "scenario 3, nothing to move",
+ // input_cfg
+ "{\n"
+ " \"dhcp-ddns\" : {\n"
+ " \"enable-updates\": true, \n"
+ " \"server-ip\" : \"192.0.2.0\",\n"
+ " \"server-port\" : 3432,\n"
+ " \"sender-ip\" : \"192.0.2.1\"\n"
+ " }\n"
+ "}\n",
+ // exp_output
+ "{\n"
+ " \"dhcp-ddns\" : {\n"
+ " \"enable-updates\": true, \n"
+ " \"server-ip\" : \"192.0.2.0\",\n"
+ " \"server-port\" : 3432,\n"
+ " \"sender-ip\" : \"192.0.2.1\"\n"
+ " }\n"
+ "}\n"
+ }
+ };
+
+ for (auto scenario : scenarios) {
+ SrvConfig conf(32);
+ ElementPtr input_cfg;
+ ConstElementPtr exp_cfg;
+ {
+ SCOPED_TRACE(scenario.description);
+ // Parse the input cfg into a mutable Element map.
+ ASSERT_NO_THROW(input_cfg = boost::const_pointer_cast<Element>
+ (Element::fromJSON(scenario.input_cfg)))
+ << "input_cfg didn't parse, test is broken";
+
+ // Parse the expected cfg into an Element map.
+ ASSERT_NO_THROW(exp_cfg = Element::fromJSON(scenario.exp_cfg))
+ << "exp_cfg didn't parse, test is broken";
+
+ // Now call moveDdnsParams.
+ ASSERT_NO_THROW(SrvConfig::moveDdnsParams(input_cfg));
+
+ // Make sure the resultant configuration is as expected.
+ EXPECT_TRUE(input_cfg->equals(*exp_cfg));
+ }
+ }
+}
+
+// Verifies that the scoped values for DDNS parameters can be fetched
+// for a given Subnet4.
+TEST_F(SrvConfigTest, getDdnsParamsTest4) {
+ DdnsParamsPtr params;
+
+ CfgMgr::instance().setFamily(AF_INET);
+ SrvConfig conf(32);
+
+ // This disables D2 connectivity. When it is false, updates
+ // are off at all scopes, regardless of ddns-send-updates values.
+ enableD2Client(false);
+
+ // Disable sending updates globally.
+ conf.addConfiguredGlobal("ddns-send-updates", Element::create(false));
+ // Configure global host sanitizing.
+ conf.addConfiguredGlobal("hostname-char-set", Element::create("[^A-Z]"));
+ conf.addConfiguredGlobal("hostname-char-replacement", Element::create("x"));
+ // Enable conflict resolution globally.
+ conf.addConfiguredGlobal("ddns-use-conflict-resolution", Element::create(true));
+
+ // Add a plain subnet
+ Triplet<uint32_t> def_triplet;
+ Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.1.0"), 24,
+ def_triplet, def_triplet, 4000, SubnetID(1)));
+ // In order to take advantage of the dynamic inheritance of global
+ // parameters to a subnet we need to set a callback function for each
+ // subnet to allow for fetching global parameters.
+ subnet1->setFetchGlobalsFn([conf]() -> ConstCfgGlobalsPtr {
+ return (conf.getConfiguredGlobals());
+ });
+
+ conf.getCfgSubnets4()->add(subnet1);
+
+ // Add a shared network
+ SharedNetwork4Ptr frognet(new SharedNetwork4("frog"));
+ conf.getCfgSharedNetworks4()->add(frognet);
+
+ // Add a shared subnet
+ Subnet4Ptr subnet2(new Subnet4(IOAddress("192.0.2.0"), 24,
+ def_triplet, def_triplet, 4000, SubnetID(2)));
+
+ // In order to take advantage of the dynamic inheritance of global
+ // parameters to a subnet we need to set a callback function for each
+ // subnet to allow for fetching global parameters.
+ subnet2->setFetchGlobalsFn([conf]() -> ConstCfgGlobalsPtr {
+ return (conf.getConfiguredGlobals());
+ });
+
+ frognet->add(subnet2);
+ subnet2->setDdnsSendUpdates(true);
+ subnet2->setDdnsOverrideNoUpdate(true);
+ subnet2->setDdnsOverrideClientUpdate(true);
+ subnet2->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_ALWAYS);
+ subnet2->setDdnsGeneratedPrefix("prefix");
+ subnet2->setDdnsQualifyingSuffix("example.com.");
+ subnet2->setHostnameCharSet("");
+ subnet2->setDdnsUpdateOnRenew(true);
+ subnet2->setDdnsUseConflictResolution(false);
+
+ // Get DDNS params for subnet1.
+ ASSERT_NO_THROW(params = conf_.getDdnsParams(subnet1));
+
+ // Verify subnet1 values are right. Note, updates should be disabled.
+ EXPECT_FALSE(params->getEnableUpdates());
+ EXPECT_FALSE(params->getOverrideNoUpdate());
+ EXPECT_FALSE(params->getOverrideClientUpdate());
+ EXPECT_EQ(D2ClientConfig::RCM_NEVER, params->getReplaceClientNameMode());
+ EXPECT_TRUE(params->getGeneratedPrefix().empty());
+ EXPECT_TRUE(params->getQualifyingSuffix().empty());
+ EXPECT_EQ("[^A-Z]", params->getHostnameCharSet());
+ EXPECT_EQ("x", params->getHostnameCharReplacement());
+ EXPECT_FALSE(params->getUpdateOnRenew());
+ EXPECT_TRUE(params->getUseConflictResolution());
+
+ // We inherited a non-blank hostname_char_set so we
+ // should get a sanitizer instance.
+ isc::util::str::StringSanitizerPtr sanitizer;
+ ASSERT_NO_THROW(sanitizer = params->getHostnameSanitizer());
+ EXPECT_TRUE(sanitizer);
+
+ // Get DDNS params for subnet2.
+ ASSERT_NO_THROW(params = conf_.getDdnsParams(subnet2));
+
+ // Verify subnet2 values are right. Note, updates should be disabled,
+ // because D2Client is disabled.
+ EXPECT_FALSE(params->getEnableUpdates());
+ EXPECT_TRUE(params->getOverrideNoUpdate());
+ EXPECT_TRUE(params->getOverrideClientUpdate());
+ EXPECT_EQ(D2ClientConfig::RCM_ALWAYS, params->getReplaceClientNameMode());
+ EXPECT_EQ("prefix", params->getGeneratedPrefix());
+ EXPECT_EQ("example.com.", params->getQualifyingSuffix());
+ EXPECT_EQ("", params->getHostnameCharSet());
+ EXPECT_EQ("x", params->getHostnameCharReplacement());
+ EXPECT_TRUE(params->getUpdateOnRenew());
+ EXPECT_FALSE(params->getUseConflictResolution());
+
+ // We have a blank hostname-char-set so we should not get a sanitizer instance.
+ ASSERT_NO_THROW(sanitizer = params->getHostnameSanitizer());
+ ASSERT_FALSE(sanitizer);
+
+ // Enable D2Client.
+ enableD2Client(true);
+
+ // Make sure subnet1 updates are still disabled.
+ ASSERT_NO_THROW(params = conf_.getDdnsParams(subnet1));
+ EXPECT_FALSE(params->getEnableUpdates());
+
+ // Make sure subnet2 updates are now enabled.
+ ASSERT_NO_THROW(params = conf_.getDdnsParams(subnet2));
+ EXPECT_TRUE(params->getEnableUpdates());
+
+ // Enable sending updates globally. This should inherit down subnet1.
+ conf.addConfiguredGlobal("ddns-send-updates", Element::create(true));
+
+ // Make sure subnet1 updates are now enabled.
+ ASSERT_NO_THROW(params = conf_.getDdnsParams(subnet1));
+ EXPECT_TRUE(params->getEnableUpdates());
+}
+
+// Verifies that the fallback values for DDNS parameters when
+// no Subnet4 has been selected. In theory, we should never want
+// these values without a selected subnet.
+TEST_F(SrvConfigTest, getDdnsParamsNoSubnetTest4) {
+ DdnsParamsPtr params;
+
+ CfgMgr::instance().setFamily(AF_INET);
+ SrvConfig conf(32);
+
+ // Enable D2 connectivity.
+ enableD2Client(true);
+
+ // Give all of the parameters a global value.
+ conf.addConfiguredGlobal("ddns-send-updates", Element::create(true));
+ conf.addConfiguredGlobal("ddns-override-no-update", Element::create(true));
+ conf.addConfiguredGlobal("ddns-override-client-update", Element::create(true));
+ conf.addConfiguredGlobal("ddns-replace-client-name", Element::create("always"));
+ conf.addConfiguredGlobal("ddns-generated-prefix", Element::create("some_prefix"));
+ conf.addConfiguredGlobal("ddns-qualifying-suffix", Element::create("example.com"));
+ conf.addConfiguredGlobal("hostname-char-set", Element::create("[^A-Z]"));
+ conf.addConfiguredGlobal("hostname-char-replacement", Element::create("x"));
+ conf.addConfiguredGlobal("ddns-update-on-renew", Element::create(true));
+ conf.addConfiguredGlobal("ddns-use-conflict-resolution", Element::create(false));
+
+ // Get DDNS params for no subnet.
+ Subnet4Ptr subnet4;
+ ASSERT_NO_THROW(params = conf_.getDdnsParams(subnet4));
+
+ // Verify fallback values are right. Note, updates should be disabled.
+ EXPECT_FALSE(params->getEnableUpdates());
+ EXPECT_FALSE(params->getOverrideNoUpdate());
+ EXPECT_FALSE(params->getOverrideClientUpdate());
+ EXPECT_EQ(D2ClientConfig::RCM_NEVER, params->getReplaceClientNameMode());
+ EXPECT_TRUE(params->getGeneratedPrefix().empty());
+ EXPECT_TRUE(params->getQualifyingSuffix().empty());
+ EXPECT_TRUE(params->getHostnameCharSet().empty());
+ EXPECT_TRUE(params->getHostnameCharReplacement().empty());
+ EXPECT_FALSE(params->getUpdateOnRenew());
+ EXPECT_TRUE(params->getUseConflictResolution());
+}
+
+// Verifies that the scoped values for DDNS parameters can be fetched
+// for a given Subnet6.
+TEST_F(SrvConfigTest, getDdnsParamsTest6) {
+ DdnsParamsPtr params;
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ SrvConfig conf(32);
+
+ // This disables D2 connectivity. When it is false, updates
+ // are off at all scopes, regardless of ddns-send-updates values.
+ enableD2Client(false);
+
+ // Disable sending updates globally.
+ conf.addConfiguredGlobal("ddns-send-updates", Element::create(false));
+ // Configure global host sanitizing.
+ conf.addConfiguredGlobal("hostname-char-set", Element::create("[^A-Z]"));
+ conf.addConfiguredGlobal("hostname-char-replacement", Element::create("x"));
+ // Enable conflict resolution globally.
+ conf.addConfiguredGlobal("ddns-use-conflict-resolution", Element::create(true));
+
+ // Add a plain subnet
+ Triplet<uint32_t> def_triplet;
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 64,
+ 1000, 2000, 3000, 4000, SubnetID(1)));
+ // In order to take advantage of the dynamic inheritance of global
+ // parameters to a subnet we need to set a callback function for each
+ // subnet to allow for fetching global parameters.
+ subnet1->setFetchGlobalsFn([conf]() -> ConstCfgGlobalsPtr {
+ return (conf.getConfiguredGlobals());
+ });
+
+ conf.getCfgSubnets6()->add(subnet1);
+
+ // Add a shared network
+ SharedNetwork6Ptr frognet(new SharedNetwork6("frog"));
+ conf.getCfgSharedNetworks6()->add(frognet);
+
+ // Add a shared subnet
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 64,
+ 1000, 2000, 3000, 4000, SubnetID(2)));
+
+ // In order to take advantage of the dynamic inheritance of global
+ // parameters to a subnet we need to set a callback function for each
+ // subnet to allow for fetching global parameters.
+ subnet2->setFetchGlobalsFn([conf]() -> ConstCfgGlobalsPtr {
+ return (conf.getConfiguredGlobals());
+ });
+
+ frognet->add(subnet2);
+ subnet2->setDdnsSendUpdates(true);
+ subnet2->setDdnsOverrideNoUpdate(true);
+ subnet2->setDdnsOverrideClientUpdate(true);
+ subnet2->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_ALWAYS);
+ subnet2->setDdnsGeneratedPrefix("prefix");
+ subnet2->setDdnsQualifyingSuffix("example.com.");
+ subnet2->setHostnameCharSet("");
+ subnet2->setDdnsUpdateOnRenew(true);
+ subnet2->setDdnsUseConflictResolution(false);
+
+ // Get DDNS params for subnet1.
+ ASSERT_NO_THROW(params = conf_.getDdnsParams(subnet1));
+
+ // Verify subnet1 values are right. Note, updates should be disabled.
+ EXPECT_FALSE(params->getEnableUpdates());
+ EXPECT_FALSE(params->getOverrideNoUpdate());
+ EXPECT_FALSE(params->getOverrideClientUpdate());
+ EXPECT_EQ(D2ClientConfig::RCM_NEVER, params->getReplaceClientNameMode());
+ EXPECT_TRUE(params->getGeneratedPrefix().empty());
+ EXPECT_TRUE(params->getQualifyingSuffix().empty());
+ EXPECT_EQ("[^A-Z]", params->getHostnameCharSet());
+ EXPECT_EQ("x", params->getHostnameCharReplacement());
+ EXPECT_FALSE(params->getUpdateOnRenew());
+ EXPECT_TRUE(params->getUseConflictResolution());
+
+ // We inherited a non-blank hostname_char_set so we
+ // should get a sanitizer instance.
+ isc::util::str::StringSanitizerPtr sanitizer;
+ ASSERT_NO_THROW(sanitizer = params->getHostnameSanitizer());
+ EXPECT_TRUE(sanitizer);
+
+ // Get DDNS params for subnet2.
+ ASSERT_NO_THROW(params = conf_.getDdnsParams(subnet2));
+
+ // Verify subnet2 values are right. Note, updates should be disabled,
+ // because D2Client is disabled.
+ EXPECT_FALSE(params->getEnableUpdates());
+ EXPECT_TRUE(params->getOverrideNoUpdate());
+ EXPECT_TRUE(params->getOverrideClientUpdate());
+ EXPECT_EQ(D2ClientConfig::RCM_ALWAYS, params->getReplaceClientNameMode());
+ EXPECT_EQ("prefix", params->getGeneratedPrefix());
+ EXPECT_EQ("example.com.", params->getQualifyingSuffix());
+ EXPECT_EQ("", params->getHostnameCharSet());
+ EXPECT_EQ("x", params->getHostnameCharReplacement());
+ EXPECT_TRUE(params->getUpdateOnRenew());
+ EXPECT_FALSE(params->getUseConflictResolution());
+
+ // We have a blank hostname-char-set so we should not get a sanitizer instance.
+ ASSERT_NO_THROW(sanitizer = params->getHostnameSanitizer());
+ ASSERT_FALSE(sanitizer);
+
+ // Enable D2Client.
+ enableD2Client(true);
+
+ // Make sure subnet1 updates are still disabled.
+ ASSERT_NO_THROW(params = conf_.getDdnsParams(subnet1));
+ EXPECT_FALSE(params->getEnableUpdates());
+
+ // Make sure subnet2 updates are now enabled.
+ ASSERT_NO_THROW(params = conf_.getDdnsParams(subnet2));
+ EXPECT_TRUE(params->getEnableUpdates());
+
+ // Enable sending updates globally. This should inherit down subnet1.
+ conf.addConfiguredGlobal("ddns-send-updates", Element::create(true));
+
+ // Make sure subnet1 updates are now enabled.
+ ASSERT_NO_THROW(params = conf_.getDdnsParams(subnet1));
+ EXPECT_TRUE(params->getEnableUpdates());
+}
+
+// Verifies that the fallback values for DDNS parameters when
+// no Subnet6 has been selected. In theory, we should never want
+// these values without a selected subnet.
+TEST_F(SrvConfigTest, getDdnsParamsNoSubnetTest6) {
+ DdnsParamsPtr params;
+
+ CfgMgr::instance().setFamily(AF_INET6);
+ SrvConfig conf(32);
+
+ // Enable D2 connectivity.
+ enableD2Client(true);
+
+ // Give all of the parameters a global value.
+ conf.addConfiguredGlobal("ddns-send-updates", Element::create(true));
+ conf.addConfiguredGlobal("ddns-override-no-update", Element::create(true));
+ conf.addConfiguredGlobal("ddns-override-client-update", Element::create(true));
+ conf.addConfiguredGlobal("ddns-replace-client-name", Element::create("always"));
+ conf.addConfiguredGlobal("ddns-generated-prefix", Element::create("some_prefix"));
+ conf.addConfiguredGlobal("ddns-qualifying-suffix", Element::create("example.com"));
+ conf.addConfiguredGlobal("hostname-char-set", Element::create("[^A-Z]"));
+ conf.addConfiguredGlobal("hostname-char-replacement", Element::create("x"));
+ conf.addConfiguredGlobal("ddns-update-on-renew", Element::create(true));
+ conf.addConfiguredGlobal("ddns-use-conflict-resolution", Element::create(false));
+
+ // Get DDNS params for no subnet.
+ Subnet6Ptr subnet6;
+ ASSERT_NO_THROW(params = conf_.getDdnsParams(subnet6));
+
+ // Verify fallback values are right. Note, updates should be disabled.
+ EXPECT_FALSE(params->getEnableUpdates());
+ EXPECT_FALSE(params->getOverrideNoUpdate());
+ EXPECT_FALSE(params->getOverrideClientUpdate());
+ EXPECT_EQ(D2ClientConfig::RCM_NEVER, params->getReplaceClientNameMode());
+ EXPECT_TRUE(params->getGeneratedPrefix().empty());
+ EXPECT_TRUE(params->getQualifyingSuffix().empty());
+ EXPECT_TRUE(params->getHostnameCharSet().empty());
+ EXPECT_TRUE(params->getHostnameCharReplacement().empty());
+}
+
+// Verifies that adding multi threading settings works
+TEST_F(SrvConfigTest, multiThreadingSettings) {
+ SrvConfig conf(32);
+ ElementPtr param = Element::createMap();
+ param->set("enable-multi-threading", Element::create(true));
+ conf.setDHCPMultiThreading(param);
+ EXPECT_TRUE(isEquivalent(param, conf.getDHCPMultiThreading()));
+}
+
+// Verifies that sanityChecksLifetime works as expected.
+TEST_F(SrvConfigTest, sanityChecksLifetime) {
+ // First the overload checking the current config.
+ // Note that lifetimes have a default so some cases here should not happen.
+ {
+ SCOPED_TRACE("no lifetime");
+
+ SrvConfig conf(32);
+ EXPECT_NO_THROW(conf.sanityChecksLifetime("valid-lifetime"));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime("preferred-lifetime"));
+ }
+
+ {
+ SCOPED_TRACE("lifetime only");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(500));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime("valid-lifetime"));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime("preferred-lifetime"));
+ }
+
+ {
+ SCOPED_TRACE("min-lifetime only");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(500));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime("valid-lifetime"));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime("preferred-lifetime"));
+ }
+
+ {
+ SCOPED_TRACE("max-lifetime only");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(500));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime("valid-lifetime"));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime("preferred-lifetime"));
+ }
+
+ {
+ SCOPED_TRACE("min-lifetime and max-lifetime but no lifetime");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(500));
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(2000));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(1000));
+ std::string msg = "have min-valid-lifetime and max-valid-lifetime ";
+ msg += "but no valid-lifetime (default)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime("valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "have min-preferred-lifetime and max-preferred-lifetime ";
+ msg += "but no preferred-lifetime (default)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime("preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("all lifetime parameters");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(500));
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(2000));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(3000));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(1500));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime("valid-lifetime"));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime("preferred-lifetime"));
+ }
+
+ {
+ SCOPED_TRACE("min-lifetime > max-lifetime");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(2000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(2000));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(500));
+ std::string msg = "the value of min-valid-lifetime (2000) is ";
+ msg += "not less than max-valid-lifetime (1000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime("valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of min-preferred-lifetime (1000) is ";
+ msg += "not less than max-preferred-lifetime (500)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime("preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("min-lifetime > lifetime");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(2000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(500));
+ std::string msg = "the value of min-valid-lifetime (2000) is ";
+ msg += "not less than (default) valid-lifetime (1000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime("valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of min-preferred-lifetime (1000) is ";
+ msg += "not less than (default) preferred-lifetime (500)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime("preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("lifetime > max-lifetime");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(2000));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(500));
+ std::string msg = "the value of (default) valid-lifetime (2000) is ";
+ msg += "not less than max-valid-lifetime (1000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime("valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of (default) preferred-lifetime (1000) is ";
+ msg += "not less than max-preferred-lifetime (500)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime("preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("lifetime not between min-lifetime and max-lifetime (too small)");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(500));
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(500));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(250));
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(2000));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(1000));
+ std::string msg = "the value of (default) valid-lifetime (500) is ";
+ msg += "not between min-valid-lifetime (1000) and ";
+ msg += "max-valid-lifetime (2000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime("valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of (default) preferred-lifetime (250) is ";
+ msg += "not between min-preferred-lifetime (500) and ";
+ msg += "max-preferred-lifetime (1000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime("preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("lifetime not between min-lifetime and max-lifetime (too large)");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(500));
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(3000));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(1500));
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(2000));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(1000));
+ std::string msg = "the value of (default) valid-lifetime (3000) is ";
+ msg += "not between min-valid-lifetime (1000) and ";
+ msg += "max-valid-lifetime (2000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime("valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of (default) preferred-lifetime (1500) is ";
+ msg += "not between min-preferred-lifetime (500) and ";
+ msg += "max-preferred-lifetime (1000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime("preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ // Second the overload checking an external config before merging.
+ // We assume that the target config is correct as this was the case
+ // when this overload is used, and this lowers the number of cases...
+
+ SrvConfig target(10);
+ target.addConfiguredGlobal("min-valid-lifetime", Element::create(1000));
+ target.addConfiguredGlobal("min-preferred-lifetime", Element::create(500));
+ target.addConfiguredGlobal("valid-lifetime", Element::create(2000));
+ target.addConfiguredGlobal("preferred-lifetime", Element::create(1000));
+ target.addConfiguredGlobal("max-valid-lifetime", Element::create(3000));
+ target.addConfiguredGlobal("max-preferred-lifetime", Element::create(1500));
+
+ {
+ SCOPED_TRACE("no lifetime");
+
+ SrvConfig conf(32);
+ EXPECT_NO_THROW(conf.sanityChecksLifetime(target, "valid-lifetime"));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime(target,
+ "preferred-lifetime"));
+ }
+
+ {
+ SCOPED_TRACE("lifetime only");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(500));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime(target, "valid-lifetime"));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime(target,
+ "preferred-lifetime"));
+ }
+
+ {
+ SCOPED_TRACE("min-lifetime only");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(500));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime(target, "valid-lifetime"));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime(target,
+ "preferred-lifetime"));
+ }
+
+ {
+ SCOPED_TRACE("max-lifetime only");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(3000));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(1500));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime(target, "valid-lifetime"));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime(target,
+ "preferred-lifetime"));
+ }
+
+ {
+ SCOPED_TRACE("min-lifetime and max-lifetime but no lifetime");
+
+ SrvConfig empty(10);
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(500));
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(3000));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(1500));
+ std::string msg = "have min-valid-lifetime and ";
+ msg += "max-valid-lifetime but no valid-lifetime (default)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(empty, "valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "have min-preferred-lifetime and ";
+ msg += "max-preferred-lifetime but no preferred-lifetime (default)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(empty,
+ "preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("all lifetime parameters");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(500));
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(2000));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(3000));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(1500));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime(target, "valid-lifetime"));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime(target,
+ "preferred-lifetime"));
+ }
+
+ {
+ SCOPED_TRACE("overwrite all lifetime parameters");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(100));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(50));
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(200));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(100));
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(300));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(150));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime(target, "valid-lifetime"));
+ EXPECT_NO_THROW(conf.sanityChecksLifetime(target,
+ "preferred-lifetime"));
+ }
+
+ {
+ SCOPED_TRACE("min-lifetime > max-lifetime");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(2000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(500));
+ std::string msg = "the value of new min-valid-lifetime (2000) is ";
+ msg += "not less than new max-valid-lifetime (1000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target, "valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of new min-preferred-lifetime (1000) is ";
+ msg += "not less than new max-preferred-lifetime (500)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target,
+ "preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("target min-lifetime > max-lifetime");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(500));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(250));
+ std::string msg = "the value of previous min-valid-lifetime (1000) is ";
+ msg += "not less than new max-valid-lifetime (500)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target, "valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of previous min-preferred-lifetime (500) is ";
+ msg += "not less than new max-preferred-lifetime (250)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target,
+ "preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("min-lifetime > target max-lifetime");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(4000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(2000));
+ std::string msg = "the value of new min-valid-lifetime (4000) is ";
+ msg += "not less than previous max-valid-lifetime (3000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target, "valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of new min-preferred-lifetime (2000) is ";
+ msg += "not less than previous max-preferred-lifetime (1500)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target,
+ "preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("min-lifetime > lifetime");
+
+ SrvConfig empty(10);
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(2000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(500));
+ std::string msg = "the value of new min-valid-lifetime (2000) is ";
+ msg += "not less than new (default) valid-lifetime (1000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(empty, "valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of new min-preferred-lifetime (1000) is ";
+ msg += "not less than new (default) preferred-lifetime (500)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(empty,
+ "preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("target min-lifetime > lifetime");
+
+ SrvConfig conf(32);
+ SrvConfig target2(20);
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(500));
+ target2.addConfiguredGlobal("min-valid-lifetime", Element::create(2000));
+ target2.addConfiguredGlobal("min-preferred-lifetime", Element::create(1000));
+ std::string msg = "the value of previous min-valid-lifetime (2000) ";
+ msg += "is not less than new (default) valid-lifetime (1000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target2, "valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of previous min-preferred-lifetime (1000) ";
+ msg += "is not less than new (default) preferred-lifetime (500)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target2,
+ "preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("min-lifetime > target lifetime");
+
+ SrvConfig conf(32);
+ SrvConfig target2(20);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(2000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(1000));
+ target2.addConfiguredGlobal("valid-lifetime", Element::create(1000));
+ target2.addConfiguredGlobal("preferred-lifetime", Element::create(500));
+ std::string msg = "the value of new min-valid-lifetime (2000) is ";
+ msg += "not less than previous (default) valid-lifetime (1000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target2, "valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of new min-preferred-lifetime (1000) is ";
+ msg += "not less than previous (default) preferred-lifetime (500)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target2,
+ "preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("lifetime > max-lifetime");
+
+ SrvConfig empty(10);
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(2000));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(500));
+ std::string msg = "the value of new (default) valid-lifetime (2000) ";
+ msg += "is not less than new max-valid-lifetime (1000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(empty, "valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of new (default) preferred-lifetime (1000) ";
+ msg += "is not less than new max-preferred-lifetime (500)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(empty,
+ "preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("target lifetime > max-lifetime");
+
+ SrvConfig conf(32);
+ SrvConfig target2(20);
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(1000));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(500));
+ target2.addConfiguredGlobal("valid-lifetime", Element::create(2000));
+ target2.addConfiguredGlobal("preferred-lifetime", Element::create(1000));
+ std::string msg = "the value of previous (default) valid-lifetime ";
+ msg += "(2000) is not less than new max-valid-lifetime (1000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target2, "valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of previous (default) preferred-lifetime ";
+ msg += "(1000) is not less than new max-preferred-lifetime (500)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target2,
+ "preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("lifetime > target max-lifetime");
+
+ SrvConfig conf(32);
+ SrvConfig target2(20);
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(2000));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(1000));
+ target2.addConfiguredGlobal("max-valid-lifetime", Element::create(1000));
+ target2.addConfiguredGlobal("max-preferred-lifetime", Element::create(500));
+ std::string msg = "the value of new (default) valid-lifetime (2000) ";
+ msg += "is not less than previous max-valid-lifetime (1000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target2, "valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of new (default) preferred-lifetime (1000) ";
+ msg += "is not less than previous max-preferred-lifetime (500)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target2,
+ "preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("lifetime not between min-lifetime and max-lifetime (too small)");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(500));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(250));
+ std::string msg = "the value of new (default) valid-lifetime (500) ";
+ msg += "is not between previous min-valid-lifetime (1000) and ";
+ msg += "previous max-valid-lifetime (3000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target, "valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of new (default) preferred-lifetime (250) ";
+ msg += "is not between previous min-preferred-lifetime (500) and ";
+ msg += "previous max-preferred-lifetime (1500)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target,
+ "preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("lifetime not between min-lifetime and max-lifetime (too large)");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("valid-lifetime", Element::create(4000));
+ conf.addConfiguredGlobal("preferred-lifetime", Element::create(2000));
+ std::string msg = "the value of new (default) valid-lifetime (4000) ";
+ msg += "is not between previous min-valid-lifetime (1000) and ";
+ msg += "previous max-valid-lifetime (3000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target, "valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of new (default) preferred-lifetime (2000) ";
+ msg += "is not between previous min-preferred-lifetime (500) and ";
+ msg += "previous max-preferred-lifetime (1500)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target,
+ "preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("lifetime not between min-lifetime and max-lifetime (too low)");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(100));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(50));
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(300));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(150));
+ std::string msg = "the value of previous (default) valid-lifetime ";
+ msg += "(2000) is not between new min-valid-lifetime (100) and ";
+ msg += "new max-valid-lifetime (300)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target, "valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of previous (default) preferred-lifetime ";
+ msg += "(1000) is not between new min-preferred-lifetime (50) and ";
+ msg += "new max-preferred-lifetime (150)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target,
+ "preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+
+ {
+ SCOPED_TRACE("lifetime not between min-lifetime and max-lifetime (too high)");
+
+ SrvConfig conf(32);
+ conf.addConfiguredGlobal("min-valid-lifetime", Element::create(10000));
+ conf.addConfiguredGlobal("min-preferred-lifetime", Element::create(5000));
+ conf.addConfiguredGlobal("max-valid-lifetime", Element::create(30000));
+ conf.addConfiguredGlobal("max-preferred-lifetime", Element::create(15000));
+ std::string msg = "the value of previous (default) valid-lifetime ";
+ msg += "(2000) is not between new min-valid-lifetime (10000) and ";
+ msg += "new max-valid-lifetime (30000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target, "valid-lifetime"),
+ isc::BadValue, msg);
+ msg = "the value of previous (default) preferred-lifetime ";
+ msg += "(1000) is not between new min-preferred-lifetime (5000) and ";
+ msg += "new max-preferred-lifetime (15000)";
+ EXPECT_THROW_MSG(conf.sanityChecksLifetime(target,
+ "preferred-lifetime"),
+ isc::BadValue, msg);
+ }
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/subnet_unittest.cc b/src/lib/dhcpsrv/tests/subnet_unittest.cc
new file mode 100644
index 0000000..f19b06e
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/subnet_unittest.cc
@@ -0,0 +1,1862 @@
+// Copyright (C) 2012-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/libdhcp++.h>
+#include <dhcp/option.h>
+#include <dhcp/option_custom.h>
+#include <dhcp/option_definition.h>
+#include <dhcp/option_space.h>
+#include <dhcpsrv/shared_network.h>
+#include <dhcpsrv/subnet.h>
+#include <exceptions/exceptions.h>
+#include <testutils/multi_threading_utils.h>
+
+#include <boost/pointer_cast.hpp>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+#include <limits>
+
+// don't import the entire boost namespace. It will unexpectedly hide uint8_t
+// for some systems.
+using boost::scoped_ptr;
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::asiolink;
+using namespace isc::test;
+using namespace isc::util;
+
+namespace {
+
+TEST(Subnet4Test, constructor) {
+ EXPECT_NO_THROW(Subnet4 subnet1(IOAddress("192.0.2.2"), 16,
+ 1, 2, 3, 10));
+
+ EXPECT_THROW(Subnet4 subnet2(IOAddress("192.0.2.0"), 33, 1, 2, 3),
+ BadValue); // invalid prefix length
+ EXPECT_THROW(Subnet4 subnet3(IOAddress("2001:db8::1"), 24, 1, 2, 3),
+ BadValue); // IPv6 addresses are not allowed in Subnet4
+}
+
+// This test verifies that the Subnet4 factory function creates a
+// valid subnet instance.
+TEST(Subnet4Test, create) {
+ auto subnet = Subnet4::create(IOAddress("192.0.2.2"), 16,
+ 1, 2, 3, 10);
+ ASSERT_TRUE(subnet);
+
+ EXPECT_EQ("192.0.2.2/16", subnet->toText());
+ EXPECT_EQ(1, subnet->getT1().get());
+ EXPECT_EQ(2, subnet->getT2().get());
+ EXPECT_EQ(3, subnet->getValid().get());
+ EXPECT_EQ(10, subnet->getID());
+}
+
+// This test verifies the default values set for the subnets and verifies
+// that the optional values are unspecified.
+TEST(Subnet4Test, defaults) {
+ Triplet<uint32_t> t1;
+ Triplet<uint32_t> t2;
+ Triplet<uint32_t> valid_lft;
+ Subnet4 subnet(IOAddress("192.0.2.0"), 24, t1, t2, valid_lft);
+
+ EXPECT_TRUE(subnet.getIface().unspecified());
+ EXPECT_TRUE(subnet.getIface().empty());
+
+ EXPECT_TRUE(subnet.getClientClass().unspecified());
+ EXPECT_TRUE(subnet.getClientClass().empty());
+
+ EXPECT_TRUE(subnet.getValid().unspecified());
+ EXPECT_EQ(0, subnet.getValid().get());
+
+ EXPECT_TRUE(subnet.getT1().unspecified());
+ EXPECT_EQ(0, subnet.getT1().get());
+
+ EXPECT_TRUE(subnet.getT2().unspecified());
+ EXPECT_EQ(0, subnet.getT2().get());
+
+ EXPECT_TRUE(subnet.getReservationsGlobal().unspecified());
+ EXPECT_FALSE(subnet.getReservationsGlobal().get());
+
+ EXPECT_TRUE(subnet.getReservationsInSubnet().unspecified());
+ EXPECT_TRUE(subnet.getReservationsInSubnet().get());
+
+ EXPECT_TRUE(subnet.getReservationsOutOfPool().unspecified());
+ EXPECT_FALSE(subnet.getReservationsOutOfPool().get());
+
+ EXPECT_TRUE(subnet.getCalculateTeeTimes().unspecified());
+ EXPECT_FALSE(subnet.getCalculateTeeTimes().get());
+
+ EXPECT_TRUE(subnet.getT1Percent().unspecified());
+ EXPECT_EQ(0.0, subnet.getT1Percent().get());
+
+ EXPECT_TRUE(subnet.getT2Percent().unspecified());
+ EXPECT_EQ(0.0, subnet.getT2Percent().get());
+
+ EXPECT_TRUE(subnet.getMatchClientId().unspecified());
+ EXPECT_TRUE(subnet.getMatchClientId().get());
+
+ EXPECT_TRUE(subnet.getAuthoritative().unspecified());
+ EXPECT_FALSE(subnet.getAuthoritative().get());
+
+ EXPECT_TRUE(subnet.getSiaddr().unspecified());
+ EXPECT_TRUE(subnet.getSiaddr().get().isV4Zero());
+
+ EXPECT_TRUE(subnet.getSname().unspecified());
+ EXPECT_TRUE(subnet.getSname().empty());
+
+ EXPECT_TRUE(subnet.getFilename().unspecified());
+ EXPECT_TRUE(subnet.getFilename().empty());
+
+ EXPECT_FALSE(subnet.get4o6().enabled());
+
+ EXPECT_TRUE(subnet.get4o6().getIface4o6().unspecified());
+ EXPECT_TRUE(subnet.get4o6().getIface4o6().empty());
+
+ EXPECT_TRUE(subnet.get4o6().getSubnet4o6().unspecified());
+ EXPECT_TRUE(subnet.get4o6().getSubnet4o6().get().first.isV6Zero());
+ EXPECT_EQ(128, subnet.get4o6().getSubnet4o6().get().second);
+
+ EXPECT_TRUE(subnet.getDdnsSendUpdates().unspecified());
+ EXPECT_FALSE(subnet.getDdnsSendUpdates().get());
+
+ EXPECT_TRUE(subnet.getDdnsOverrideNoUpdate().unspecified());
+ EXPECT_FALSE(subnet.getDdnsOverrideNoUpdate().get());
+
+ EXPECT_TRUE(subnet.getDdnsOverrideClientUpdate().unspecified());
+ EXPECT_FALSE(subnet.getDdnsOverrideClientUpdate().get());
+
+ EXPECT_TRUE(subnet.getDdnsReplaceClientNameMode().unspecified());
+ EXPECT_EQ(D2ClientConfig::RCM_NEVER, subnet.getDdnsReplaceClientNameMode().get());
+
+ EXPECT_TRUE(subnet.getDdnsGeneratedPrefix().unspecified());
+ EXPECT_TRUE(subnet.getDdnsGeneratedPrefix().empty());
+
+ EXPECT_TRUE(subnet.getDdnsQualifyingSuffix().unspecified());
+ EXPECT_TRUE(subnet.getDdnsQualifyingSuffix().empty());
+
+ EXPECT_TRUE(subnet.getHostnameCharSet().unspecified());
+ EXPECT_TRUE(subnet.getHostnameCharSet().empty());
+
+ EXPECT_TRUE(subnet.getHostnameCharReplacement().unspecified());
+ EXPECT_TRUE(subnet.getHostnameCharReplacement().empty());
+
+ EXPECT_TRUE(subnet.getDdnsUpdateOnRenew().unspecified());
+ EXPECT_FALSE(subnet.getDdnsUpdateOnRenew().get());
+}
+
+// Checks that the subnet id can be either autogenerated or set to an
+// arbitrary value through the constructor.
+TEST(Subnet4Test, subnetID) {
+ // Create subnet and don't specify id, so as it is autogenerated.
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 24, 1000, 2000,
+ 3000));
+ SubnetID id0 = subnet->getID();
+
+ // Create another subnet and let id be autogenerated.
+ subnet.reset(new Subnet4(IOAddress("192.0.3.0"), 24, 1000, 2000,
+ 3000));
+ SubnetID id1 = subnet->getID();
+
+ // The autogenerated ids must not be equal.
+ EXPECT_NE(id0, id1);
+
+ // Create third subnet but this time select an arbitrary id. The id
+ // we use the one of the second subnet. That way we ensure that the
+ // subnet id we provide via constructor is used and it is not
+ // autogenerated - if it was autogenerated we would get id other
+ // than id1 because id1 has already been used.
+ subnet.reset(new Subnet4(IOAddress("192.0.4.0"), 24, 1000, 2000,
+ 3000, id1));
+ EXPECT_EQ(id1, subnet->getID());
+}
+
+TEST(Subnet4Test, inRange) {
+ Subnet4 subnet(IOAddress("192.0.2.1"), 24, 1000, 2000, 3000);
+
+ EXPECT_EQ(1000, subnet.getT1());
+ EXPECT_EQ(2000, subnet.getT2());
+ EXPECT_EQ(3000, subnet.getValid());
+
+ EXPECT_FALSE(subnet.hasRelays());
+
+ EXPECT_FALSE(subnet.inRange(IOAddress("192.0.0.0")));
+ EXPECT_TRUE(subnet.inRange(IOAddress("192.0.2.0")));
+ EXPECT_TRUE(subnet.inRange(IOAddress("192.0.2.1")));
+ EXPECT_TRUE(subnet.inRange(IOAddress("192.0.2.255")));
+ EXPECT_FALSE(subnet.inRange(IOAddress("192.0.3.0")));
+ EXPECT_FALSE(subnet.inRange(IOAddress("0.0.0.0")));
+ EXPECT_FALSE(subnet.inRange(IOAddress("255.255.255.255")));
+}
+
+// Checks whether the relay list is empty by default
+// and basic operations function
+TEST(Subnet4Test, relay) {
+ Subnet4 subnet(IOAddress("192.0.2.1"), 24, 1000, 2000, 3000);
+
+ // Should be empty.
+ EXPECT_FALSE(subnet.hasRelays());
+ EXPECT_EQ(0, subnet.getRelayAddresses().size());
+
+ // Matching should fail.
+ EXPECT_FALSE(subnet.hasRelayAddress(IOAddress("192.0.123.45")));
+
+ // Should be able to add them.
+ subnet.addRelayAddress(IOAddress("192.0.123.45"));
+ subnet.addRelayAddress(IOAddress("192.0.123.46"));
+
+ // Should not be empty.
+ EXPECT_TRUE(subnet.hasRelays());
+
+ // Should be two in the list.
+ EXPECT_EQ(2, subnet.getRelayAddresses().size());
+
+ // Should be able to match them if they are there.
+ EXPECT_TRUE(subnet.hasRelayAddress(IOAddress("192.0.123.45")));
+ EXPECT_TRUE(subnet.hasRelayAddress(IOAddress("192.0.123.46")));
+
+ // Should not match those that are not.
+ EXPECT_FALSE(subnet.hasRelayAddress(IOAddress("192.0.123.47")));
+}
+
+// Checks whether siaddr field can be set and retrieved correctly.
+TEST(Subnet4Test, siaddr) {
+ Subnet4 subnet(IOAddress("192.0.2.1"), 24, 1000, 2000, 3000);
+
+ // Check if the default is 0.0.0.0
+ EXPECT_EQ("0.0.0.0", subnet.getSiaddr().get().toText());
+
+ // Check that we can set it up
+ EXPECT_NO_THROW(subnet.setSiaddr(IOAddress("1.2.3.4")));
+
+ // Check that we can get it back
+ EXPECT_EQ("1.2.3.4", subnet.getSiaddr().get().toText());
+
+ // Check that only v4 addresses are supported
+ EXPECT_THROW(subnet.setSiaddr(IOAddress("2001:db8::1")),
+ BadValue);
+}
+
+// Checks whether server-hostname field can be set and retrieved correctly.
+TEST(Subnet4Test, serverHostname) {
+ Subnet4 subnet(IOAddress("192.0.2.1"), 24, 1000, 2000, 3000);
+
+ // Check if the default is empty
+ EXPECT_TRUE(subnet.getSname().empty());
+
+ // Check that we can set it up
+ EXPECT_NO_THROW(subnet.setSname("foobar"));
+
+ // Check that we can get it back
+ EXPECT_EQ("foobar", subnet.getSname().get());
+}
+
+// Checks whether boot-file-name field can be set and retrieved correctly.
+TEST(Subnet4Test, bootFileName) {
+ Subnet4 subnet(IOAddress("192.0.2.1"), 24, 1000, 2000, 3000);
+
+ // Check if the default is empty
+ EXPECT_TRUE(subnet.getFilename().empty());
+
+ // Check that we can set it up
+ EXPECT_NO_THROW(subnet.setFilename("foobar"));
+
+ // Check that we can get it back
+ EXPECT_EQ("foobar", subnet.getFilename().get());
+}
+
+// Checks if the match-client-id flag can be set and retrieved.
+TEST(Subnet4Test, matchClientId) {
+ Subnet4 subnet(IOAddress("192.0.2.1"), 24, 1000, 2000, 3000);
+
+ // By default the flag should be set to true.
+ EXPECT_TRUE(subnet.getMatchClientId());
+
+ // Modify it and retrieve.
+ subnet.setMatchClientId(false);
+ EXPECT_FALSE(subnet.getMatchClientId());
+
+ // Modify again.
+ subnet.setMatchClientId(true);
+ EXPECT_TRUE(subnet.getMatchClientId());
+}
+
+// Checks that it is possible to add and retrieve multiple pools.
+TEST(Subnet4Test, pool4InSubnet4) {
+
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.1.2.0"), 24, 1, 2, 3));
+
+ PoolPtr pool1(new Pool4(IOAddress("192.1.2.0"), 25));
+ PoolPtr pool2(new Pool4(IOAddress("192.1.2.128"), 26));
+ PoolPtr pool3(new Pool4(IOAddress("192.1.2.192"), 30));
+ pool3->allowClientClass("bar");
+ PoolPtr pool4(new Pool4(IOAddress("192.1.2.200"), 30));
+
+ // Add pools in reverse order to make sure that they get ordered by
+ // first address.
+ EXPECT_NO_THROW(subnet->addPool(pool4));
+
+ // If there's only one pool, get that pool
+ PoolPtr mypool = subnet->getAnyPool(Lease::TYPE_V4);
+ EXPECT_EQ(mypool, pool4);
+
+ EXPECT_NO_THROW(subnet->addPool(pool3));
+ EXPECT_NO_THROW(subnet->addPool(pool2));
+ EXPECT_NO_THROW(subnet->addPool(pool1));
+
+ // If there are more than one pool and we didn't provide hint, we
+ // should get the first pool
+ EXPECT_NO_THROW(mypool = subnet->getAnyPool(Lease::TYPE_V4));
+
+ EXPECT_EQ(mypool, pool1);
+
+ // If we provide a hint, we should get a pool that this hint belongs to
+ ASSERT_NO_THROW(mypool = subnet->getPool(Lease::TYPE_V4,
+ IOAddress("192.1.2.201")));
+ EXPECT_EQ(mypool, pool4);
+
+ ASSERT_NO_THROW(mypool = subnet->getPool(Lease::TYPE_V4,
+ IOAddress("192.1.2.129")));
+ EXPECT_EQ(mypool, pool2);
+
+ ASSERT_NO_THROW(mypool = subnet->getPool(Lease::TYPE_V4,
+ IOAddress("192.1.2.64")));
+ EXPECT_EQ(mypool, pool1);
+
+ // Specify addresses which don't belong to any existing pools. The
+ // third parameter prevents it from returning "any" available
+ // pool if a good match is not found.
+ ASSERT_NO_THROW(mypool = subnet->getPool(Lease::TYPE_V4,
+ IOAddress("192.1.2.210"),
+ false));
+ EXPECT_FALSE(mypool);
+
+ ASSERT_NO_THROW(mypool = subnet->getPool(Lease::TYPE_V4,
+ IOAddress("192.1.1.254"),
+ false));
+ EXPECT_FALSE(mypool);
+
+ // Now play with classes
+
+ // 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");
+
+ // This client belongs to foo, bar and baz classes.
+ isc::dhcp::ClientClasses three_classes;
+ three_classes.insert("foo");
+ three_classes.insert("bar");
+ three_classes.insert("baz");
+
+ // If we provide a hint, we should get a pool that this hint belongs to
+ ASSERT_NO_THROW(mypool = subnet->getPool(Lease::TYPE_V4, no_class,
+ IOAddress("192.1.2.201")));
+ EXPECT_EQ(mypool, pool4);
+
+ ASSERT_NO_THROW(mypool = subnet->getPool(Lease::TYPE_V4, no_class,
+ IOAddress("192.1.2.129")));
+ EXPECT_EQ(mypool, pool2);
+
+ ASSERT_NO_THROW(mypool = subnet->getPool(Lease::TYPE_V4, no_class,
+ IOAddress("192.1.2.64")));
+ EXPECT_EQ(mypool, pool1);
+
+ // Specify addresses which don't belong to any existing pools.
+ ASSERT_NO_THROW(mypool = subnet->getPool(Lease::TYPE_V4, three_classes,
+ IOAddress("192.1.2.210")));
+ EXPECT_FALSE(mypool);
+
+ // Pool3 requires a member of bar
+ ASSERT_NO_THROW(mypool = subnet->getPool(Lease::TYPE_V4, no_class,
+ IOAddress("192.1.2.195")));
+ EXPECT_FALSE(mypool);
+ ASSERT_NO_THROW(mypool = subnet->getPool(Lease::TYPE_V4, foo_class,
+ IOAddress("192.1.2.195")));
+ EXPECT_FALSE(mypool);
+ ASSERT_NO_THROW(mypool = subnet->getPool(Lease::TYPE_V4, bar_class,
+ IOAddress("192.1.2.195")));
+ EXPECT_EQ(mypool, pool3);
+ ASSERT_NO_THROW(mypool = subnet->getPool(Lease::TYPE_V4, three_classes,
+ IOAddress("192.1.2.195")));
+ EXPECT_EQ(mypool, pool3);
+}
+
+// Check if it's possible to get specified number of possible leases for
+// an IPv4 subnet.
+TEST(Subnet4Test, getCapacity) {
+
+ // There's one /24 pool.
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.1.2.0"), 24, 1, 2, 3));
+
+ // There are no pools defined, so the total number of available addrs is 0.
+ EXPECT_EQ(0, subnet->getPoolCapacity(Lease::TYPE_V4));
+
+ // Let's add a /25 pool. That's 128 addresses.
+ PoolPtr pool1(new Pool4(IOAddress("192.1.2.0"), 25));
+ subnet->addPool(pool1);
+ EXPECT_EQ(128, subnet->getPoolCapacity(Lease::TYPE_V4));
+
+ // Let's add another /26 pool. That's extra 64 addresses.
+ PoolPtr pool2(new Pool4(IOAddress("192.1.2.128"), 26));
+ subnet->addPool(pool2);
+ EXPECT_EQ(192, subnet->getPoolCapacity(Lease::TYPE_V4));
+
+ // Let's add a third pool /30. This one has 4 addresses.
+ PoolPtr pool3(new Pool4(IOAddress("192.1.2.192"), 30));
+ subnet->addPool(pool3);
+ EXPECT_EQ(196, subnet->getPoolCapacity(Lease::TYPE_V4));
+
+ // Let's add a forth pool /30. This one has 4 addresses.
+ PoolPtr pool4(new Pool4(IOAddress("192.1.2.200"), 30));
+ subnet->addPool(pool4);
+ EXPECT_EQ(200, subnet->getPoolCapacity(Lease::TYPE_V4));
+
+ // Now play with classes
+
+ // 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");
+
+ // This client belongs to foo, bar and baz classes.
+ isc::dhcp::ClientClasses three_classes;
+ three_classes.insert("foo");
+ three_classes.insert("bar");
+ three_classes.insert("baz");
+
+ pool3->allowClientClass("bar");
+
+ // Pool3 requires a member of bar
+ EXPECT_EQ(196, subnet->getPoolCapacity(Lease::TYPE_V4, no_class));
+ EXPECT_EQ(196, subnet->getPoolCapacity(Lease::TYPE_V4, foo_class));
+ EXPECT_EQ(200, subnet->getPoolCapacity(Lease::TYPE_V4, bar_class));
+ EXPECT_EQ(200, subnet->getPoolCapacity(Lease::TYPE_V4, three_classes));
+}
+
+// Checks that it is not allowed to add invalid pools.
+TEST(Subnet4Test, pool4Checks) {
+
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 8, 1, 2, 3));
+
+ // this one is in subnet
+ Pool4Ptr pool1(new Pool4(IOAddress("192.254.0.0"), 16));
+ subnet->addPool(pool1);
+
+ // this one is larger than the subnet!
+ Pool4Ptr pool2(new Pool4(IOAddress("193.0.0.0"), 24));
+
+ ASSERT_THROW(subnet->addPool(pool2), BadValue);
+
+ // this one is totally out of blue
+ Pool4Ptr pool3(new Pool4(IOAddress("1.2.3.4"), 16));
+ ASSERT_THROW(subnet->addPool(pool3), BadValue);
+
+ // This pool should be added just fine.
+ Pool4Ptr pool4(new Pool4(IOAddress("192.0.2.10"),
+ IOAddress("192.0.2.20")));
+ ASSERT_NO_THROW(subnet->addPool(pool4));
+
+ // This one overlaps with the previous pool.
+ Pool4Ptr pool5(new Pool4(IOAddress("192.0.2.1"),
+ IOAddress("192.0.2.15")));
+ ASSERT_THROW(subnet->addPool(pool5), BadValue);
+
+ // This one also overlaps.
+ Pool4Ptr pool6(new Pool4(IOAddress("192.0.2.20"),
+ IOAddress("192.0.2.30")));
+ ASSERT_THROW(subnet->addPool(pool6), BadValue);
+
+ // This one "surrounds" the other pool.
+ Pool4Ptr pool7(new Pool4(IOAddress("192.0.2.8"),
+ IOAddress("192.0.2.23")));
+ ASSERT_THROW(subnet->addPool(pool7), BadValue);
+
+ // This one does not overlap.
+ Pool4Ptr pool8(new Pool4(IOAddress("192.0.2.30"),
+ IOAddress("192.0.2.40")));
+ ASSERT_NO_THROW(subnet->addPool(pool8));
+
+ // This one has a lower bound in the pool of 192.0.2.10-20.
+ Pool4Ptr pool9(new Pool4(IOAddress("192.0.2.18"),
+ IOAddress("192.0.2.30")));
+ ASSERT_THROW(subnet->addPool(pool9), BadValue);
+
+ // This one has an upper bound in the pool of 192.0.2.30-40.
+ Pool4Ptr pool10(new Pool4(IOAddress("192.0.2.25"),
+ IOAddress("192.0.2.32")));
+ ASSERT_THROW(subnet->addPool(pool10), BadValue);
+
+ // Add a pool with a single address.
+ Pool4Ptr pool11(new Pool4(IOAddress("192.255.0.50"),
+ IOAddress("192.255.0.50")));
+ ASSERT_NO_THROW(subnet->addPool(pool11));
+
+ // Now we're going to add the same pool again. This is an interesting
+ // case because we're checking if the code is properly using upper_bound
+ // function, which returns a pool that has an address greater than the
+ // specified one.
+ ASSERT_THROW(subnet->addPool(pool11), BadValue);
+}
+
+// Tests whether Subnet4 object is able to store and process properly
+// information about allowed client class (a single class).
+TEST(Subnet4Test, clientClass) {
+ // Create the V4 subnet.
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 8, 1, 2, 3));
+
+ // 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");
+
+ // This client belongs to foo, bar and baz classes.
+ isc::dhcp::ClientClasses three_classes;
+ three_classes.insert("foo");
+ three_classes.insert("bar");
+ three_classes.insert("baz");
+
+ // This client belongs to foo, bar, baz and network classes.
+ isc::dhcp::ClientClasses four_classes;
+ four_classes.insert("foo");
+ four_classes.insert("bar");
+ four_classes.insert("baz");
+ four_classes.insert("network");
+
+ // No class restrictions defined, any client should be supported
+ EXPECT_TRUE(subnet->getClientClass().empty());
+ EXPECT_TRUE(subnet->clientSupported(no_class));
+ EXPECT_TRUE(subnet->clientSupported(foo_class));
+ EXPECT_TRUE(subnet->clientSupported(bar_class));
+ EXPECT_TRUE(subnet->clientSupported(three_classes));
+
+ // Let's allow only clients belonging to "bar" class.
+ subnet->allowClientClass("bar");
+ EXPECT_EQ("bar", subnet->getClientClass().get());
+
+ EXPECT_FALSE(subnet->clientSupported(no_class));
+ EXPECT_FALSE(subnet->clientSupported(foo_class));
+ EXPECT_TRUE(subnet->clientSupported(bar_class));
+ EXPECT_TRUE(subnet->clientSupported(three_classes));
+
+ // Add shared network which can only be selected when the client
+ // class is "network".
+ SharedNetwork4Ptr network(new SharedNetwork4("network"));
+ network->allowClientClass("network");
+ ASSERT_NO_THROW(network->add(subnet));
+
+ // This time, if the client doesn't support network class,
+ // the subnets from the shared network can't be selected.
+ EXPECT_FALSE(subnet->clientSupported(bar_class));
+ EXPECT_FALSE(subnet->clientSupported(three_classes));
+
+ // If the classes include "network", the subnet is selected.
+ EXPECT_TRUE(subnet->clientSupported(four_classes));
+}
+
+TEST(Subnet4Test, addInvalidOption) {
+ // Create the V4 subnet.
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 8, 1, 2, 3));
+
+ // Create NULL pointer option. Attempt to add NULL option
+ // should result in exception.
+ OptionPtr option2;
+ ASSERT_FALSE(option2);
+ EXPECT_THROW(subnet->getCfgOption()->add(option2, false, DHCP4_OPTION_SPACE),
+ isc::BadValue);
+}
+
+// This test verifies that inRange() and inPool() methods work properly.
+TEST(Subnet4Test, inRangeinPool) {
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.0.0"), 8, 1, 2, 3));
+
+ // this one is in subnet
+ Pool4Ptr pool1(new Pool4(IOAddress("192.2.0.0"), 16));
+ subnet->addPool(pool1);
+
+ // 192.1.1.1 belongs to the subnet...
+ EXPECT_TRUE(subnet->inRange(IOAddress("192.1.1.1")));
+
+ // ... but it does not belong to any pool within
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_V4, IOAddress("192.1.1.1")));
+
+ // the last address that is in range, but out of pool
+ EXPECT_TRUE(subnet->inRange(IOAddress("192.1.255.255")));
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_V4, IOAddress("192.1.255.255")));
+
+ // the first address that is in range, in pool
+ EXPECT_TRUE(subnet->inRange(IOAddress("192.2.0.0")));
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_V4, IOAddress("192.2.0.0")));
+
+ // let's try something in the middle as well
+ EXPECT_TRUE(subnet->inRange(IOAddress("192.2.3.4")));
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_V4, IOAddress("192.2.3.4")));
+
+ // the last address that is in range, in pool
+ EXPECT_TRUE(subnet->inRange(IOAddress("192.2.255.255")));
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_V4, IOAddress("192.2.255.255")));
+
+ // the first address that is in range, but out of pool
+ EXPECT_TRUE(subnet->inRange(IOAddress("192.3.0.0")));
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_V4, IOAddress("192.3.0.0")));
+
+ // Add with classes
+ pool1->allowClientClass("bar");
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_V4, IOAddress("192.2.3.4")));
+
+ // This client does not belong to any class.
+ isc::dhcp::ClientClasses no_class;
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_V4, IOAddress("192.2.3.4"), no_class));
+
+ // This client belongs to foo only
+ isc::dhcp::ClientClasses foo_class;
+ foo_class.insert("foo");
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_V4, IOAddress("192.2.3.4"), foo_class));
+
+ // This client belongs to bar only. I like that client.
+ isc::dhcp::ClientClasses bar_class;
+ bar_class.insert("bar");
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_V4, IOAddress("192.2.3.4"), bar_class));
+
+ // This client belongs to foo, bar and baz classes.
+ isc::dhcp::ClientClasses three_classes;
+ three_classes.insert("foo");
+ three_classes.insert("bar");
+ three_classes.insert("baz");
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_V4, IOAddress("192.2.3.4"), three_classes));
+}
+
+// This test checks if the toText() method returns text representation
+TEST(Subnet4Test, toText) {
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3));
+ EXPECT_EQ("192.0.2.0/24", subnet->toText());
+}
+
+// This test verifies that the IPv4 prefix can be parsed into prefix/length pair.
+TEST(Subnet4Test, parsePrefix) {
+ std::pair<IOAddress, uint8_t> parsed =
+ std::make_pair(IOAddress::IPV4_ZERO_ADDRESS(), 0);
+
+ // Valid prefix.
+ EXPECT_NO_THROW(parsed = Subnet4::parsePrefix("192.0.5.0/24"));
+ EXPECT_EQ("192.0.5.0", parsed.first.toText());
+ EXPECT_EQ(24, static_cast<int>(parsed.second));
+
+ // Invalid IPv4 address.
+ EXPECT_THROW(Subnet4::parsePrefix("192.0.2.322/24"), BadValue);
+
+ // Invalid prefix length.
+ EXPECT_THROW(Subnet4::parsePrefix("192.0.2.0/64"), BadValue);
+ EXPECT_THROW(Subnet4::parsePrefix("192.0.2.0/0"), BadValue);
+
+ // No IP address.
+ EXPECT_THROW(Subnet4::parsePrefix(" /24"), BadValue);
+
+ // No prefix length but slash present.
+ EXPECT_THROW(Subnet4::parsePrefix("10.0.0.0/ "), BadValue);
+
+ // No slash sign.
+ EXPECT_THROW(Subnet4::parsePrefix("10.0.0.1"), BadValue);
+
+ // IPv6 is not allowed here.
+ EXPECT_THROW(Subnet4::parsePrefix("3000::/24"), BadValue);
+}
+
+// This test checks if the get() method returns proper parameters
+TEST(Subnet4Test, get) {
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 28, 1, 2, 3));
+ EXPECT_EQ("192.0.2.0", subnet->get().first.toText());
+ EXPECT_EQ(28, subnet->get().second);
+}
+
+// Checks if last allocated address/prefix is stored/retrieved properly
+TEST(Subnet4Test, lastAllocated) {
+ IOAddress addr("192.0.2.17");
+
+ IOAddress last("192.0.2.255");
+
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3));
+
+ // Check initial conditions (all should be set to the last address in range)
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_V4).toText());
+
+ // Now set last allocated for IA
+ EXPECT_NO_THROW(subnet->setLastAllocated(Lease::TYPE_V4, addr));
+ EXPECT_EQ(addr.toText(), subnet->getLastAllocated(Lease::TYPE_V4).toText());
+
+ // No, you can't set the last allocated IPv6 address in IPv4 subnet
+ EXPECT_THROW(subnet->setLastAllocated(Lease::TYPE_TA, addr), BadValue);
+ EXPECT_THROW(subnet->setLastAllocated(Lease::TYPE_TA, addr), BadValue);
+ EXPECT_THROW(subnet->setLastAllocated(Lease::TYPE_PD, addr), BadValue);
+}
+
+// Checks if last allocated address/prefix is stored/retrieved properly
+TEST(Subnet4Test, lastAllocatedMultiThreading) {
+ MultiThreadingTest mt(true);
+ IOAddress addr("192.0.2.17");
+
+ IOAddress last("192.0.2.255");
+
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3));
+
+ // Check initial conditions (all should be set to the last address in range)
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_V4).toText());
+
+ // Now set last allocated for IA
+ EXPECT_NO_THROW(subnet->setLastAllocated(Lease::TYPE_V4, addr));
+ EXPECT_EQ(addr.toText(), subnet->getLastAllocated(Lease::TYPE_V4).toText());
+
+ // No, you can't set the last allocated IPv6 address in IPv4 subnet
+ EXPECT_THROW(subnet->setLastAllocated(Lease::TYPE_TA, addr), BadValue);
+ EXPECT_THROW(subnet->setLastAllocated(Lease::TYPE_TA, addr), BadValue);
+ EXPECT_THROW(subnet->setLastAllocated(Lease::TYPE_PD, addr), BadValue);
+}
+
+// Checks if the V4 is the only allowed type for Pool4 and if getPool()
+// is working properly.
+TEST(Subnet4Test, PoolType) {
+
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.2.0.0"), 16, 1, 2, 3));
+
+ PoolPtr pool1(new Pool4(IOAddress("192.2.1.0"), 24));
+ PoolPtr pool2(new Pool4(IOAddress("192.2.2.0"), 24));
+ PoolPtr pool3(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:3::"), 64));
+ PoolPtr pool4(new Pool6(Lease::TYPE_TA, IOAddress("2001:db8:1:4::"), 64));
+ PoolPtr pool5(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:1:1::"), 64));
+
+ // There should be no pools of any type by default
+ EXPECT_EQ(PoolPtr(), subnet->getAnyPool(Lease::TYPE_V4));
+
+ // It should not be possible to ask for V6 pools in Subnet4
+ EXPECT_THROW(subnet->getAnyPool(Lease::TYPE_NA), BadValue);
+ EXPECT_THROW(subnet->getAnyPool(Lease::TYPE_TA), BadValue);
+ EXPECT_THROW(subnet->getAnyPool(Lease::TYPE_PD), BadValue);
+
+ // Let's add a single V4 pool and check that it can be retrieved
+ EXPECT_NO_THROW(subnet->addPool(pool1));
+
+ // If there's only one IA pool, get that pool (without and with hint)
+ EXPECT_EQ(pool1, subnet->getAnyPool(Lease::TYPE_V4));
+ EXPECT_EQ(pool1, subnet->getPool(Lease::TYPE_V4, IOAddress("192.0.1.167")));
+
+ // Let's add additional V4 pool
+ EXPECT_NO_THROW(subnet->addPool(pool2));
+
+ // Try without hints
+ EXPECT_EQ(pool1, subnet->getAnyPool(Lease::TYPE_V4));
+
+ // Try with valid hints
+ EXPECT_EQ(pool1, subnet->getPool(Lease::TYPE_V4, IOAddress("192.2.1.5")));
+ EXPECT_EQ(pool2, subnet->getPool(Lease::TYPE_V4, IOAddress("192.2.2.254")));
+
+ // Try with bogus hints (hints should be ignored)
+ EXPECT_EQ(pool1, subnet->getPool(Lease::TYPE_V4, IOAddress("10.1.1.1")));
+
+ // Trying to add Pool6 to Subnet4 is a big no,no!
+ EXPECT_THROW(subnet->addPool(pool3), BadValue);
+ EXPECT_THROW(subnet->addPool(pool4), BadValue);
+ EXPECT_THROW(subnet->addPool(pool5), BadValue);
+}
+
+// Tests if correct value of server identifier is returned when getServerId is
+// called.
+TEST(Subnet4Test, getServerId) {
+ // Initially, the subnet has no server identifier.
+ Subnet4 subnet(IOAddress("192.2.0.0"), 16, 1, 2, 3);
+ EXPECT_TRUE(subnet.getServerId().isV4Zero());
+
+ // Add server identifier.
+ OptionDefinitionPtr option_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
+ DHO_DHCP_SERVER_IDENTIFIER);
+ OptionCustomPtr option_server_id(new OptionCustom(*option_def, Option::V4));
+ option_server_id->writeAddress(IOAddress("1.2.3.4"));
+
+ CfgOptionPtr cfg_option = subnet.getCfgOption();
+ cfg_option->add(option_server_id, false, DHCP4_OPTION_SPACE);
+
+ // Verify that the server identifier returned by the Subnet4 object is
+ // correct.
+ OptionBuffer server_id_buf = { 1, 2, 3, 4 };
+ EXPECT_EQ("1.2.3.4", subnet.getServerId().toText());
+}
+
+// Tests for Subnet6
+
+TEST(Subnet6Test, constructor) {
+
+ EXPECT_NO_THROW(Subnet6 subnet1(IOAddress("2001:db8:1::"), 64,
+ 1, 2, 3, 4));
+
+ EXPECT_THROW(Subnet6 subnet2(IOAddress("2001:db8:1::"), 129, 1, 2, 3, 4),
+ BadValue); // invalid prefix length
+ EXPECT_THROW(Subnet6 subnet3(IOAddress("192.168.0.0"), 32, 1, 2, 3, 4),
+ BadValue); // IPv4 addresses are not allowed in Subnet6
+}
+
+// This test verifies that the Subnet6 factory function creates a
+// valid subnet instance.
+TEST(Subnet6Test, create) {
+ auto subnet = Subnet6::create(IOAddress("2001:db8:1::"), 64,
+ 1, 2, 3, 4, 10);
+ ASSERT_TRUE(subnet);
+
+ EXPECT_EQ("2001:db8:1::/64", subnet->toText());
+ EXPECT_EQ(1, subnet->getT1().get());
+ EXPECT_EQ(2, subnet->getT2().get());
+ EXPECT_EQ(3, subnet->getPreferred().get());
+ EXPECT_EQ(4, subnet->getValid().get());
+ EXPECT_EQ(10, subnet->getID());
+}
+
+// This test verifies the default values set for the shared
+// networks and verifies that the optional values are unspecified.
+TEST(SharedNetwork6Test, defaults) {
+ Triplet<uint32_t> t1;
+ Triplet<uint32_t> t2;
+ Triplet<uint32_t> preferred_lft;
+ Triplet<uint32_t> valid_lft;
+ Subnet6 subnet(IOAddress("2001:db8:1::"), 64, t1, t2, preferred_lft,
+ valid_lft);
+
+ EXPECT_TRUE(subnet.getIface().unspecified());
+ EXPECT_TRUE(subnet.getIface().empty());
+
+ EXPECT_TRUE(subnet.getClientClass().unspecified());
+ EXPECT_TRUE(subnet.getClientClass().empty());
+
+ EXPECT_TRUE(subnet.getValid().unspecified());
+ EXPECT_EQ(0, subnet.getValid().get());
+
+ EXPECT_TRUE(subnet.getT1().unspecified());
+ EXPECT_EQ(0, subnet.getT1().get());
+
+ EXPECT_TRUE(subnet.getT2().unspecified());
+ EXPECT_EQ(0, subnet.getT2().get());
+
+ EXPECT_TRUE(subnet.getReservationsGlobal().unspecified());
+ EXPECT_FALSE(subnet.getReservationsGlobal().get());
+
+ EXPECT_TRUE(subnet.getReservationsInSubnet().unspecified());
+ EXPECT_TRUE(subnet.getReservationsInSubnet().get());
+
+ EXPECT_TRUE(subnet.getReservationsOutOfPool().unspecified());
+ EXPECT_FALSE(subnet.getReservationsOutOfPool().get());
+
+ EXPECT_TRUE(subnet.getCalculateTeeTimes().unspecified());
+ EXPECT_FALSE(subnet.getCalculateTeeTimes().get());
+
+ EXPECT_TRUE(subnet.getT1Percent().unspecified());
+ EXPECT_EQ(0.0, subnet.getT1Percent().get());
+
+ EXPECT_TRUE(subnet.getT2Percent().unspecified());
+ EXPECT_EQ(0.0, subnet.getT2Percent().get());
+
+ EXPECT_TRUE(subnet.getPreferred().unspecified());
+ EXPECT_EQ(0, subnet.getPreferred().get());
+
+ EXPECT_TRUE(subnet.getRapidCommit().unspecified());
+ EXPECT_FALSE(subnet.getRapidCommit().get());
+
+ EXPECT_TRUE(subnet.getDdnsSendUpdates().unspecified());
+ EXPECT_FALSE(subnet.getDdnsSendUpdates().get());
+
+ EXPECT_TRUE(subnet.getDdnsOverrideNoUpdate().unspecified());
+ EXPECT_FALSE(subnet.getDdnsOverrideNoUpdate().get());
+
+ EXPECT_TRUE(subnet.getDdnsOverrideClientUpdate().unspecified());
+ EXPECT_FALSE(subnet.getDdnsOverrideClientUpdate().get());
+
+ EXPECT_TRUE(subnet.getDdnsReplaceClientNameMode().unspecified());
+ EXPECT_EQ(D2ClientConfig::RCM_NEVER, subnet.getDdnsReplaceClientNameMode().get());
+
+ EXPECT_TRUE(subnet.getDdnsGeneratedPrefix().unspecified());
+ EXPECT_TRUE(subnet.getDdnsGeneratedPrefix().empty());
+
+ EXPECT_TRUE(subnet.getDdnsQualifyingSuffix().unspecified());
+ EXPECT_TRUE(subnet.getDdnsQualifyingSuffix().empty());
+
+ EXPECT_TRUE(subnet.getHostnameCharSet().unspecified());
+ EXPECT_TRUE(subnet.getHostnameCharSet().empty());
+
+ EXPECT_TRUE(subnet.getHostnameCharReplacement().unspecified());
+ EXPECT_TRUE(subnet.getHostnameCharReplacement().empty());
+
+ EXPECT_TRUE(subnet.getDdnsUpdateOnRenew().unspecified());
+ EXPECT_FALSE(subnet.getDdnsUpdateOnRenew().get());
+}
+
+// Checks that the subnet id can be either autogenerated or set to an
+// arbitrary value through the constructor.
+TEST(Subnet6Test, subnetID) {
+ // Create subnet and don't specify id, so as it is autogenerated.
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 64, 1000, 2000,
+ 3000, 4000));
+ SubnetID id0 = subnet->getID();
+
+ // Create another subnet and let id be autogenerated.
+ subnet.reset(new Subnet6(IOAddress("2001:db8:2::"), 64, 1000, 2000,
+ 3000, 4000));
+ SubnetID id1 = subnet->getID();
+
+ // The autogenerated ids must not be equal.
+ EXPECT_NE(id0, id1);
+
+ // Create third subnet but this time select an arbitrary id. The id
+ // we use us the one of second subnet. That way we ensure that the
+ // subnet id we provide via constructor is used and it is not
+ // autogenerated - if it was autogenerated we would get id other
+ // than id1 because id1 has already been used.
+ subnet.reset(new Subnet6(IOAddress("2001:db8:3::"), 64, 1000, 2000,
+ 3000, 4000, id1));
+ EXPECT_EQ(id1, subnet->getID());
+}
+
+TEST(Subnet6Test, inRange) {
+ Subnet6 subnet(IOAddress("2001:db8:1::"), 64, 1000, 2000, 3000, 4000);
+
+ EXPECT_EQ(1000, subnet.getT1());
+ EXPECT_EQ(2000, subnet.getT2());
+ EXPECT_EQ(3000, subnet.getPreferred());
+ EXPECT_EQ(4000, subnet.getValid());
+
+ EXPECT_FALSE(subnet.inRange(IOAddress("2001:db8:0:ffff:ffff:ffff:ffff:ffff")));
+ EXPECT_TRUE(subnet.inRange(IOAddress("2001:db8:1::0")));
+ EXPECT_TRUE(subnet.inRange(IOAddress("2001:db8:1::1")));
+ EXPECT_TRUE(subnet.inRange(IOAddress("2001:db8:1::ffff:ffff:ffff:ffff")));
+ EXPECT_FALSE(subnet.inRange(IOAddress("2001:db8:1:1::")));
+ EXPECT_FALSE(subnet.inRange(IOAddress("::")));
+}
+
+// Checks whether the relay list is empty by default
+// and basic operations function
+TEST(Subnet6Test, relay) {
+ Subnet6 subnet(IOAddress("2001:db8:1::"), 64, 1000, 2000, 3000, 4000);
+
+ // Should be empty.
+ EXPECT_FALSE(subnet.hasRelays());
+ EXPECT_EQ(0, subnet.getRelayAddresses().size());
+
+ // Matching should fail.
+ EXPECT_FALSE(subnet.hasRelayAddress(IOAddress("2001:ffff::45")));
+
+ // Should be able to add them.
+ subnet.addRelayAddress(IOAddress("2001:ffff::45"));
+ subnet.addRelayAddress(IOAddress("2001:ffff::46"));
+
+ // Should not be empty.
+ EXPECT_TRUE(subnet.hasRelays());
+
+ // Should be two in the list.
+ EXPECT_EQ(2, subnet.getRelayAddresses().size());
+
+ // Should be able to match them if they are there.
+ EXPECT_TRUE(subnet.hasRelayAddress(IOAddress("2001:ffff::45")));
+ EXPECT_TRUE(subnet.hasRelayAddress(IOAddress("2001:ffff::46")));
+
+ // Should not match those that are not.
+ EXPECT_FALSE(subnet.hasRelayAddress(IOAddress("2001:ffff::47")));
+}
+
+// Test checks whether the number of addresses available in the pools are
+// calculated properly.
+TEST(Subnet6Test, Pool6getCapacity) {
+
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+
+ // There's 2^16 = 65536 addresses in this one.
+ PoolPtr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:1::"), 112));
+
+ // There's 2^32 = 4294967296 addresses in each of those.
+ PoolPtr pool2(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:2::"), 96));
+ PoolPtr pool3(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:3::"), 96));
+
+ EXPECT_EQ(0, subnet->getPoolCapacity(Lease::TYPE_NA));
+ EXPECT_EQ(0, subnet->getPoolCapacity(Lease::TYPE_TA));
+ EXPECT_EQ(0, subnet->getPoolCapacity(Lease::TYPE_PD));
+
+ subnet->addPool(pool1);
+ EXPECT_EQ(65536, subnet->getPoolCapacity(Lease::TYPE_NA));
+
+ subnet->addPool(pool2);
+ EXPECT_EQ(uint64_t(4294967296ull + 65536), subnet->getPoolCapacity(Lease::TYPE_NA));
+
+ subnet->addPool(pool3);
+ EXPECT_EQ(uint64_t(4294967296ull + 4294967296ull + 65536),
+ subnet->getPoolCapacity(Lease::TYPE_NA));
+
+ // Now play with classes
+
+ // 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");
+
+ // This client belongs to foo, bar and baz classes.
+ isc::dhcp::ClientClasses three_classes;
+ three_classes.insert("foo");
+ three_classes.insert("bar");
+ three_classes.insert("baz");
+
+ pool3->allowClientClass("bar");
+
+ // Pool3 requires a member of bar
+ EXPECT_EQ(uint64_t(4294967296ull + 65536),
+ subnet->getPoolCapacity(Lease::TYPE_NA, no_class));
+ EXPECT_EQ(uint64_t(4294967296ull + 65536),
+ subnet->getPoolCapacity(Lease::TYPE_NA, foo_class));
+ EXPECT_EQ(uint64_t(4294967296ull + 4294967296ull + 65536),
+ subnet->getPoolCapacity(Lease::TYPE_NA, bar_class));
+ EXPECT_EQ(uint64_t(4294967296ull + 4294967296ull + 65536),
+ subnet->getPoolCapacity(Lease::TYPE_NA, three_classes));
+}
+
+// Test checks whether the number of prefixes available in the pools are
+// calculated properly.
+TEST(Subnet6Test, Pool6PdgetPoolCapacity) {
+
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8::"), 32, 1, 2, 3, 4));
+
+ // There's 2^16 = 65536 addresses in this one.
+ PoolPtr pool1(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:1::"), 48, 64));
+
+ // There's 2^32 = 4294967296 addresses in each of those.
+ PoolPtr pool2(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:2::"), 48, 80));
+ PoolPtr pool3(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:3::"), 48, 80));
+
+ EXPECT_EQ(0, subnet->getPoolCapacity(Lease::TYPE_NA));
+ EXPECT_EQ(0, subnet->getPoolCapacity(Lease::TYPE_TA));
+ EXPECT_EQ(0, subnet->getPoolCapacity(Lease::TYPE_PD));
+
+ subnet->addPool(pool1);
+ EXPECT_EQ(65536, subnet->getPoolCapacity(Lease::TYPE_PD));
+
+ subnet->addPool(pool2);
+ EXPECT_EQ(uint64_t(4294967296ull + 65536), subnet->getPoolCapacity(Lease::TYPE_PD));
+
+ subnet->addPool(pool3);
+ EXPECT_EQ(uint64_t(4294967296ull + 4294967296ull + 65536),
+ subnet->getPoolCapacity(Lease::TYPE_PD));
+
+ // This is 2^64.
+ PoolPtr pool4(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:4::"), 48, 112));
+ subnet->addPool(pool4);
+ EXPECT_EQ(std::numeric_limits<uint64_t>::max(),
+ subnet->getPoolCapacity(Lease::TYPE_PD));
+
+ PoolPtr pool5(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:5::"), 48, 112));
+ subnet->addPool(pool5);
+ EXPECT_EQ(std::numeric_limits<uint64_t>::max(),
+ subnet->getPoolCapacity(Lease::TYPE_PD));
+}
+
+TEST(Subnet6Test, Pool6InSubnet6) {
+
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+
+ PoolPtr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:1::"), 64));
+ PoolPtr pool2(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:2::"), 64));
+ PoolPtr pool3(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:3::"), 64));
+
+ subnet->addPool(pool1);
+
+ // If there's only one pool, get that pool
+ PoolPtr mypool = subnet->getAnyPool(Lease::TYPE_NA);
+ EXPECT_EQ(mypool, pool1);
+
+ subnet->addPool(pool2);
+ subnet->addPool(pool3);
+
+ // If there are more than one pool and we didn't provide hint, we
+ // should get the first pool
+ mypool = subnet->getAnyPool(Lease::TYPE_NA);
+
+ EXPECT_EQ(mypool, pool1);
+
+ // If we provide a hint, we should get a pool that this hint belongs to
+ mypool = subnet->getPool(Lease::TYPE_NA, IOAddress("2001:db8:1:3::dead:beef"));
+
+ EXPECT_EQ(mypool, pool3);
+
+ // Now play with classes
+
+ // 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");
+
+ // This client belongs to foo, bar and baz classes.
+ isc::dhcp::ClientClasses three_classes;
+ three_classes.insert("foo");
+ three_classes.insert("bar");
+ three_classes.insert("baz");
+
+ pool3->allowClientClass("bar");
+
+ // Pool3 requires a member of bar
+ mypool = subnet->getPool(Lease::TYPE_NA, no_class,
+ IOAddress("2001:db8:1:3::dead:beef"));
+ EXPECT_FALSE(mypool);
+ mypool = subnet->getPool(Lease::TYPE_NA, foo_class,
+ IOAddress("2001:db8:1:3::dead:beef"));
+ EXPECT_FALSE(mypool);
+ mypool = subnet->getPool(Lease::TYPE_NA, bar_class,
+ IOAddress("2001:db8:1:3::dead:beef"));
+ EXPECT_EQ(mypool, pool3);
+ mypool = subnet->getPool(Lease::TYPE_NA, three_classes,
+ IOAddress("2001:db8:1:3::dead:beef"));
+ EXPECT_EQ(mypool, pool3);
+}
+
+// Check if Subnet6 supports different types of pools properly.
+TEST(Subnet6Test, poolTypes) {
+
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+
+ PoolPtr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:1::"), 64));
+ PoolPtr pool2(new Pool6(Lease::TYPE_TA, IOAddress("2001:db8:1:2::"), 64));
+ PoolPtr pool3(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:1:3::"), 64));
+ PoolPtr pool4(new Pool6(Lease::TYPE_PD, IOAddress("3000:1::"), 64));
+
+ PoolPtr pool5(new Pool4(IOAddress("192.0.2.0"), 24));
+
+ // There should be no pools of any type by default
+ EXPECT_EQ(PoolPtr(), subnet->getAnyPool(Lease::TYPE_NA));
+ EXPECT_EQ(PoolPtr(), subnet->getAnyPool(Lease::TYPE_TA));
+ EXPECT_EQ(PoolPtr(), subnet->getAnyPool(Lease::TYPE_PD));
+
+ // Trying to get IPv4 pool from Subnet6 is not allowed
+ EXPECT_THROW(subnet->getAnyPool(Lease::TYPE_V4), BadValue);
+
+ // Let's add a single IA pool and check that it can be retrieved
+ EXPECT_NO_THROW(subnet->addPool(pool1));
+
+ // If there's only one IA pool, get that pool
+ EXPECT_EQ(pool1, subnet->getAnyPool(Lease::TYPE_NA));
+ EXPECT_EQ(pool1, subnet->getPool(Lease::TYPE_NA, IOAddress("2001:db8:1:1::1")));
+
+ // Check if pools of different type are not returned
+ EXPECT_EQ(PoolPtr(), subnet->getAnyPool(Lease::TYPE_TA));
+ EXPECT_EQ(PoolPtr(), subnet->getAnyPool(Lease::TYPE_PD));
+
+ // We ask with good hints, but wrong types, should return nothing
+ EXPECT_EQ(PoolPtr(), subnet->getPool(Lease::TYPE_PD, IOAddress("2001:db8:1:2::1")));
+ EXPECT_EQ(PoolPtr(), subnet->getPool(Lease::TYPE_TA, IOAddress("2001:db8:1:3::1")));
+
+ // Let's add TA and PD pools
+ EXPECT_NO_THROW(subnet->addPool(pool2));
+ EXPECT_NO_THROW(subnet->addPool(pool3));
+
+ // Try without hints
+ EXPECT_EQ(pool1, subnet->getAnyPool(Lease::TYPE_NA));
+ EXPECT_EQ(pool2, subnet->getAnyPool(Lease::TYPE_TA));
+ EXPECT_EQ(pool3, subnet->getAnyPool(Lease::TYPE_PD));
+
+ // Try with valid hints
+ EXPECT_EQ(pool1, subnet->getPool(Lease::TYPE_NA, IOAddress("2001:db8:1:1::1")));
+ EXPECT_EQ(pool2, subnet->getPool(Lease::TYPE_TA, IOAddress("2001:db8:1:2::1")));
+ EXPECT_EQ(pool3, subnet->getPool(Lease::TYPE_PD, IOAddress("2001:db8:1:3::1")));
+
+ // Try with bogus hints (hints should be ignored)
+ EXPECT_EQ(pool1, subnet->getPool(Lease::TYPE_NA, IOAddress("2001:db8:1:7::1")));
+ EXPECT_EQ(pool2, subnet->getPool(Lease::TYPE_TA, IOAddress("2001:db8:1:7::1")));
+ EXPECT_EQ(pool3, subnet->getPool(Lease::TYPE_PD, IOAddress("2001:db8:1:7::1")));
+
+ // Let's add a second PD pool
+ EXPECT_NO_THROW(subnet->addPool(pool4));
+
+ // Without hints, it should return the first pool
+ EXPECT_EQ(pool3, subnet->getAnyPool(Lease::TYPE_PD));
+
+ // With valid hint, it should return that hint
+ EXPECT_EQ(pool3, subnet->getPool(Lease::TYPE_PD, IOAddress("2001:db8:1:3::1")));
+ EXPECT_EQ(pool4, subnet->getPool(Lease::TYPE_PD, IOAddress("3000:1::")));
+
+ // With invalid hint, it should return the first pool
+ EXPECT_EQ(pool3, subnet->getPool(Lease::TYPE_PD, IOAddress("2001:db8::123")));
+
+ // Adding Pool4 to Subnet6 is a big no, no!
+ EXPECT_THROW(subnet->addPool(pool5), BadValue);
+}
+
+// Tests whether Subnet6 object is able to store and process properly
+// information about allowed client class (a single class).
+TEST(Subnet6Test, clientClass) {
+ // Create the V6 subnet.
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+
+ // 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");
+
+ // This client belongs to foo, bar and baz classes.
+ isc::dhcp::ClientClasses three_classes;
+ three_classes.insert("foo");
+ three_classes.insert("bar");
+ three_classes.insert("baz");
+
+ // This client belongs to foo, bar, baz and network classes.
+ isc::dhcp::ClientClasses four_classes;
+ four_classes.insert("foo");
+ four_classes.insert("bar");
+ four_classes.insert("baz");
+ four_classes.insert("network");
+
+ // No class restrictions defined, any client should be supported
+ EXPECT_TRUE(subnet->getClientClass().empty());
+ EXPECT_TRUE(subnet->clientSupported(no_class));
+ EXPECT_TRUE(subnet->clientSupported(foo_class));
+ EXPECT_TRUE(subnet->clientSupported(bar_class));
+ EXPECT_TRUE(subnet->clientSupported(three_classes));
+
+ // Let's allow only clients belonging to "bar" class.
+ subnet->allowClientClass("bar");
+ EXPECT_EQ("bar", subnet->getClientClass().get());
+
+ EXPECT_FALSE(subnet->clientSupported(no_class));
+ EXPECT_FALSE(subnet->clientSupported(foo_class));
+ EXPECT_TRUE(subnet->clientSupported(bar_class));
+ EXPECT_TRUE(subnet->clientSupported(three_classes));
+
+ // Add shared network which can only be selected when the client
+ // class is "network".
+ SharedNetwork6Ptr network(new SharedNetwork6("network"));
+ network->allowClientClass("network");
+ ASSERT_NO_THROW(network->add(subnet));
+
+ // This time, if the client doesn't support network class,
+ // the subnets from the shared network can't be selected.
+ EXPECT_FALSE(subnet->clientSupported(bar_class));
+ EXPECT_FALSE(subnet->clientSupported(three_classes));
+
+ // If the classes include "network", the subnet is selected.
+ EXPECT_TRUE(subnet->clientSupported(four_classes));
+}
+
+// Checks that it is not allowed to add invalid pools.
+TEST(Subnet6Test, pool6Checks) {
+
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+
+ // this one is in subnet
+ Pool6Ptr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:1::"), 64));
+ ASSERT_NO_THROW(subnet->addPool(pool1));
+
+ // this one is larger than the subnet!
+ Pool6Ptr pool2(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8::"), 48));
+
+ ASSERT_THROW(subnet->addPool(pool2), BadValue);
+
+ // this one is totally out of blue
+ Pool6Ptr pool3(new Pool6(Lease::TYPE_NA, IOAddress("3000::"), 16));
+ ASSERT_THROW(subnet->addPool(pool3), BadValue);
+
+ Pool6Ptr pool4(new Pool6(Lease::TYPE_NA, IOAddress("4001:db8:1::"), 80));
+ ASSERT_THROW(subnet->addPool(pool4), BadValue);
+
+ // This pool should be added just fine.
+ Pool6Ptr pool5(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:2::100"),
+ IOAddress("2001:db8:1:2::200")));
+ ASSERT_NO_THROW(subnet->addPool(pool5));
+
+ // This pool overlaps with a previously added pool.
+ Pool6Ptr pool6(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:2::1"),
+ IOAddress("2001:db8:1:2::150")));
+ ASSERT_THROW(subnet->addPool(pool6), BadValue);
+
+ // This pool also overlaps
+ Pool6Ptr pool7(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:2::150"),
+ IOAddress("2001:db8:1:2::300")));
+ ASSERT_THROW(subnet->addPool(pool7), BadValue);
+
+ // This one "surrounds" the other pool.
+ Pool6Ptr pool8(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:2::50"),
+ IOAddress("2001:db8:1:2::250")));
+ ASSERT_THROW(subnet->addPool(pool8), BadValue);
+
+ // This one does not overlap.
+ Pool6Ptr pool9(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:2::300"),
+ IOAddress("2001:db8:1:2::400")));
+ ASSERT_NO_THROW(subnet->addPool(pool9));
+
+ // This one has a lower bound in the pool of 2001:db8:1::100-200.
+ Pool6Ptr pool10(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:2::200"),
+ IOAddress("2001:db8:1:2::225")));
+ ASSERT_THROW(subnet->addPool(pool10), BadValue);
+
+ // This one has an upper bound in the pool of 2001:db8:1::300-400.
+ Pool6Ptr pool11(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:2::250"),
+ IOAddress("2001:db8:1:2::300")));
+ ASSERT_THROW(subnet->addPool(pool11), BadValue);
+
+ // Add a pool with a single address.
+ Pool6Ptr pool12(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:3::250"),
+ IOAddress("2001:db8:1:3::250")));
+ ASSERT_NO_THROW(subnet->addPool(pool12));
+
+ // Now we're going to add the same pool again. This is an interesting
+ // case because we're checking if the code is properly using upper_bound
+ // function, which returns a pool that has an address greater than the
+ // specified one.
+ ASSERT_THROW(subnet->addPool(pool12), BadValue);
+
+ // Prefix pool overlaps with the pool1. We can't hand out addresses and
+ // prefixes from the same range.
+ Pool6Ptr pool13(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:1:1:2::"),
+ 80, 96));
+ ASSERT_THROW(subnet->addPool(pool13), BadValue);
+}
+
+TEST(Subnet6Test, addOptions) {
+ // Create as subnet to add options to it.
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+
+ // Differentiate options by their codes (100-109)
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(subnet->getCfgOption()->add(option, false, DHCP6_OPTION_SPACE));
+ }
+
+ // Add 7 options to another option space. The option codes partially overlap
+ // with option codes that we have added to dhcp6 option space.
+ for (uint16_t code = 105; code < 112; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(subnet->getCfgOption()->add(option, false, "isc"));
+ }
+
+ // Get options from the Subnet and check if all 10 are there.
+ OptionContainerPtr options = subnet->getCfgOption()->getAll(DHCP6_OPTION_SPACE);
+ ASSERT_TRUE(options);
+ ASSERT_EQ(10, options->size());
+
+ // Validate codes of options added to dhcp6 option space.
+ uint16_t expected_code = 100;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ options = subnet->getCfgOption()->getAll("isc");
+ ASSERT_TRUE(options);
+ ASSERT_EQ(7, options->size());
+
+ // Validate codes of options added to isc option space.
+ expected_code = 105;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ // Try to get options from a non-existing option space.
+ options = subnet->getCfgOption()->getAll("abcd");
+ ASSERT_TRUE(options);
+ EXPECT_TRUE(options->empty());
+}
+
+TEST(Subnet6Test, addNonUniqueOptions) {
+ // Create as subnet to add options to it.
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+
+ // Create a set of options with non-unique codes.
+ for (int i = 0; i < 2; ++i) {
+ // In the inner loop we create options with unique codes (100-109).
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(subnet->getCfgOption()->add(option, false, DHCP6_OPTION_SPACE));
+ }
+ }
+
+ // Sanity check that all options are there.
+ OptionContainerPtr options = subnet->getCfgOption()->getAll(DHCP6_OPTION_SPACE);
+ ASSERT_EQ(20, options->size());
+
+ // Use container index #1 to get the options by their codes.
+ OptionContainerTypeIndex& idx = options->get<1>();
+ // Look for the codes 100-109.
+ for (uint16_t code = 100; code < 110; ++ code) {
+ // For each code we should get two instances of options->
+ OptionContainerTypeRange range = idx.equal_range(code);
+ // Distance between iterators indicates how many options
+ // have been returned for the particular code.
+ ASSERT_EQ(2, distance(range.first, range.second));
+ // Check that returned options actually have the expected option code.
+ for (OptionContainerTypeIndex::const_iterator option_desc = range.first;
+ option_desc != range.second; ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(code, option_desc->option_->getType());
+ }
+ }
+
+ // Let's try to find some non-exiting option.
+ const uint16_t non_existing_code = 150;
+ OptionContainerTypeRange range = idx.equal_range(non_existing_code);
+ // Empty set is expected.
+ EXPECT_EQ(0, distance(range.first, range.second));
+}
+
+TEST(Subnet6Test, addPersistentOption) {
+ // Create as subnet to add options to it.
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+
+ // Add 10 options to the subnet with option codes 100 - 109.
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ // We create 10 options and want some of them to be flagged
+ // persistent and some non-persistent. Persistent options are
+ // those that server sends to clients regardless if they ask
+ // for them or not. We pick 3 out of 10 options and mark them
+ // non-persistent and 7 other options persistent.
+ // Code values: 102, 105 and 108 are divisible by 3
+ // and options with these codes will be flagged non-persistent.
+ // Options with other codes will be flagged persistent.
+ bool persistent = (code % 3) ? true : false;
+ ASSERT_NO_THROW(subnet->getCfgOption()->add(option, persistent, DHCP6_OPTION_SPACE));
+ }
+
+ // Get added options from the subnet.
+ OptionContainerPtr options = subnet->getCfgOption()->getAll(DHCP6_OPTION_SPACE);
+
+ // options->get<2> returns reference to container index #2. This
+ // index is used to access options by the 'persistent' flag.
+ OptionContainerPersistIndex& idx = options->get<2>();
+
+ // Get all persistent options->
+ OptionContainerPersistRange range_persistent = idx.equal_range(true);
+ // 7 out of 10 options have been flagged persistent.
+ ASSERT_EQ(7, distance(range_persistent.first, range_persistent.second));
+
+ // Get all non-persistent options->
+ OptionContainerPersistRange range_non_persistent = idx.equal_range(false);
+ // 3 out of 10 options have been flagged not persistent.
+ ASSERT_EQ(3, distance(range_non_persistent.first, range_non_persistent.second));
+}
+
+TEST(Subnet6Test, getOptions) {
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8::"), 56, 1, 2, 3, 4));
+
+ // Add 10 options to a "dhcp6" option space in the subnet.
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(subnet->getCfgOption()->add(option, false, DHCP6_OPTION_SPACE));
+ }
+
+ // Check that we can get each added option descriptor using
+ // individually.
+ for (uint16_t code = 100; code < 110; ++code) {
+ std::ostringstream stream;
+ // First, try the invalid option space name.
+ OptionDescriptor desc = subnet->getCfgOption()->get("isc", code);
+ // Returned descriptor should contain NULL option ptr.
+ EXPECT_FALSE(desc.option_);
+ // Now, try the valid option space.
+ desc = subnet->getCfgOption()->get(DHCP6_OPTION_SPACE, code);
+ // Test that the option code matches the expected code.
+ ASSERT_TRUE(desc.option_);
+ EXPECT_EQ(code, desc.option_->getType());
+ }
+}
+
+TEST(Subnet6Test, addVendorOption) {
+
+ // Create as subnet to add options to it.
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+
+ // Differentiate options by their codes (100-109)
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(subnet->getCfgOption()->add(option, false, "vendor-12345678"));
+ }
+
+ // Add 7 options to another option space. The option codes partially overlap
+ // with option codes that we have added to dhcp6 option space.
+ for (uint16_t code = 105; code < 112; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(subnet->getCfgOption()->add(option, false, "vendor-87654321"));
+ }
+
+ // Get options from the Subnet and check if all 10 are there.
+ OptionContainerPtr options = subnet->getCfgOption()->getAll(12345678);
+ ASSERT_TRUE(options);
+ ASSERT_EQ(10, options->size());
+
+ // Validate codes of options added to dhcp6 option space.
+ uint16_t expected_code = 100;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ options = subnet->getCfgOption()->getAll(87654321);
+ ASSERT_TRUE(options);
+ ASSERT_EQ(7, options->size());
+
+ // Validate codes of options added to isc option space.
+ expected_code = 105;
+ for (OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option_);
+ EXPECT_EQ(expected_code, option_desc->option_->getType());
+ ++expected_code;
+ }
+
+ // Try to get options from a non-existing option space.
+ options = subnet->getCfgOption()->getAll(1111111);
+ ASSERT_TRUE(options);
+ EXPECT_TRUE(options->empty());
+}
+
+// This test verifies that inRange() and inPool() methods work properly.
+TEST(Subnet6Test, inRangeinPool) {
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8::"), 32, 1, 2, 3, 4));
+
+ // this one is in subnet
+ Pool6Ptr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8::10"),
+ IOAddress("2001:db8::20")));
+ subnet->addPool(pool1);
+
+ // 2001:db8::1 belongs to the subnet...
+ EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::1")));
+ // ... but it does not belong to any pool within
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::1")));
+
+ // the last address that is in range, but out of pool
+ EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::f")));
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::f")));
+
+ // the first address that is in range, in pool
+ EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::10")));
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::10")));
+
+ // let's try something in the middle as well
+ EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::18")));
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::18")));
+
+ // the last address that is in range, in pool
+ EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::20")));
+ EXPECT_TRUE (subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::20")));
+
+ // the first address that is in range, but out of pool
+ EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::21")));
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::21")));
+
+ // Add with classes
+ pool1->allowClientClass("bar");
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::18")));
+
+ // This client does not belong to any class.
+ isc::dhcp::ClientClasses no_class;
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::18"), no_class));
+
+ // This client belongs to foo only
+ isc::dhcp::ClientClasses foo_class;
+ foo_class.insert("foo");
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::18"), foo_class));
+
+ // This client belongs to bar only. I like that client.
+ isc::dhcp::ClientClasses bar_class;
+ bar_class.insert("bar");
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::18"), bar_class));
+
+ // This client belongs to foo, bar and baz classes.
+ isc::dhcp::ClientClasses three_classes;
+ three_classes.insert("foo");
+ three_classes.insert("bar");
+ three_classes.insert("baz");
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::18"), three_classes));
+}
+
+// This test verifies that inRange() and inPool() methods work properly
+// for prefixes too.
+TEST(Subnet6Test, PdinRangeinPool) {
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8::"), 64, 1, 2, 3, 4));
+
+ // this one is in subnet
+ Pool6Ptr pool1(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8::"),
+ 96, 112));
+ subnet->addPool(pool1);
+
+ // this one is not in subnet
+ Pool6Ptr pool2(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:1::"),
+ 96, 112));
+ subnet->addPool(pool2);
+
+ // 2001:db8::1:0:0 belongs to the subnet...
+ EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::1:0:0")));
+ // ... but it does not belong to any pool within
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_PD, IOAddress("2001:db8::1:0:0")));
+
+ // 2001:db8:1::1 does not belong to the subnet...
+ EXPECT_FALSE(subnet->inRange(IOAddress("2001:db8:1::1")));
+ // ... but it belongs to the second pool
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_PD, IOAddress("2001:db8:1::1")));
+
+ // 2001:db8::1 belongs to the subnet and to the first pool
+ EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::1")));
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_PD, IOAddress("2001:db8::1")));
+
+ // 2001:db8:0:1:0:1:: does not belong to the subnet and any pool
+ EXPECT_FALSE(subnet->inRange(IOAddress("2001:db8:0:1:0:1::")));
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_PD, IOAddress("2001:db8:0:1:0:1::")));
+}
+
+// This test checks if the toText() method returns text representation
+TEST(Subnet6Test, toText) {
+ Subnet6 subnet(IOAddress("2001:db8::"), 32, 1, 2, 3, 4);
+ EXPECT_EQ("2001:db8::/32", subnet.toText());
+}
+
+// This test verifies that the IPv6 prefix can be parsed into prefix/length pair.
+TEST(Subnet6Test, parsePrefix) {
+ std::pair<IOAddress, uint8_t> parsed =
+ std::make_pair(IOAddress::IPV6_ZERO_ADDRESS(), 0);
+
+ // Valid prefix.
+ EXPECT_NO_THROW(parsed = Subnet6::parsePrefix("2001:db8:1::/64"));
+ EXPECT_EQ("2001:db8:1::", parsed.first.toText());
+ EXPECT_EQ(64, static_cast<int>(parsed.second));
+
+ // Invalid IPv6 address.
+ EXPECT_THROW(Subnet6::parsePrefix("2001:db8::1::/64"), BadValue);
+
+ // Invalid prefix length.
+ EXPECT_THROW(Subnet6::parsePrefix("2001:db8:1::/164"), BadValue);
+ EXPECT_THROW(Subnet6::parsePrefix("2001:db8:1::/0"), BadValue);
+
+ // No IP address.
+ EXPECT_THROW(Subnet6::parsePrefix(" /64"), BadValue);
+
+ // No prefix length but slash present.
+ EXPECT_THROW(Subnet6::parsePrefix("3000::/ "), BadValue);
+
+ // No slash sign.
+ EXPECT_THROW(Subnet6::parsePrefix("3000::"), BadValue);
+
+ // IPv4 is not allowed here.
+ EXPECT_THROW(Subnet6::parsePrefix("192.0.2.0/24"), BadValue);
+}
+
+// This test checks if the get() method returns proper parameters
+TEST(Subnet6Test, get) {
+ Subnet6 subnet(IOAddress("2001:db8::"), 32, 1, 2, 3, 4);
+ EXPECT_EQ("2001:db8::", subnet.get().first.toText());
+ EXPECT_EQ(32, subnet.get().second);
+}
+
+// This trivial test checks if interface name is stored properly
+// in Subnet6 objects.
+TEST(Subnet6Test, iface) {
+ Subnet6 subnet(IOAddress("2001:db8::"), 32, 1, 2, 3, 4);
+
+ EXPECT_TRUE(subnet.getIface().empty());
+
+ subnet.setIface("en1");
+ EXPECT_EQ("en1", subnet.getIface().get());
+}
+
+// This trivial test checks if the interface-id option can be set and
+// later retrieved for a subnet6 object.
+TEST(Subnet6Test, interfaceId) {
+ // Create as subnet to add options to it.
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+
+ EXPECT_FALSE(subnet->getInterfaceId());
+
+ OptionPtr option(new Option(Option::V6, D6O_INTERFACE_ID, OptionBuffer(10, 0xFF)));
+ subnet->setInterfaceId(option);
+
+ EXPECT_EQ(option, subnet->getInterfaceId());
+
+}
+
+// This test checks that the Rapid Commit support can be enabled or
+// disabled for a subnet. It also checks that the Rapid Commit
+// support is disabled by default.
+TEST(Subnet6Test, rapidCommit) {
+ Subnet6 subnet(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4);
+
+ // By default, the RC should be disabled.
+ EXPECT_FALSE(subnet.getRapidCommit());
+
+ // Enable Rapid Commit.
+ subnet.setRapidCommit(true);
+ EXPECT_TRUE(subnet.getRapidCommit());
+
+ // Disable again.
+ subnet.setRapidCommit(false);
+ EXPECT_FALSE(subnet.getRapidCommit());
+}
+
+// Checks if last allocated address/prefix is stored/retrieved properly
+TEST(Subnet6Test, lastAllocated) {
+ IOAddress ia("2001:db8:1::1");
+ IOAddress ta("2001:db8:1::abcd");
+ IOAddress pd("2001:db8:1::1234:5678");
+
+ IOAddress last("2001:db8:1::ffff:ffff:ffff:ffff");
+
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 64, 1, 2, 3, 4));
+
+ // Check initial conditions (all should be set to the last address in range)
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_NA).toText());
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_TA).toText());
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_PD).toText());
+
+ // Now set last allocated for IA
+ EXPECT_NO_THROW(subnet->setLastAllocated(Lease::TYPE_NA, ia));
+ EXPECT_EQ(ia.toText(), subnet->getLastAllocated(Lease::TYPE_NA).toText());
+
+ // TA and PD should be unchanged
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_TA).toText());
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_PD).toText());
+
+ // Now set TA and PD
+ EXPECT_NO_THROW(subnet->setLastAllocated(Lease::TYPE_TA, ta));
+ EXPECT_NO_THROW(subnet->setLastAllocated(Lease::TYPE_PD, pd));
+
+ EXPECT_EQ(ia.toText(), subnet->getLastAllocated(Lease::TYPE_NA).toText());
+ EXPECT_EQ(ta.toText(), subnet->getLastAllocated(Lease::TYPE_TA).toText());
+ EXPECT_EQ(pd.toText(), subnet->getLastAllocated(Lease::TYPE_PD).toText());
+
+ // No, you can't set the last allocated IPv4 address in IPv6 subnet
+ EXPECT_THROW(subnet->setLastAllocated(Lease::TYPE_V4, ia), BadValue);
+}
+
+// Checks if last allocated address/prefix is stored/retrieved properly
+TEST(Subnet6Test, lastAllocatedMultiThreading) {
+ MultiThreadingTest mt(true);
+ IOAddress ia("2001:db8:1::1");
+ IOAddress ta("2001:db8:1::abcd");
+ IOAddress pd("2001:db8:1::1234:5678");
+
+ IOAddress last("2001:db8:1::ffff:ffff:ffff:ffff");
+
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 64, 1, 2, 3, 4));
+
+ // Check initial conditions (all should be set to the last address in range)
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_NA).toText());
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_TA).toText());
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_PD).toText());
+
+ // Now set last allocated for IA
+ EXPECT_NO_THROW(subnet->setLastAllocated(Lease::TYPE_NA, ia));
+ EXPECT_EQ(ia.toText(), subnet->getLastAllocated(Lease::TYPE_NA).toText());
+
+ // TA and PD should be unchanged
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_TA).toText());
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_PD).toText());
+
+ // Now set TA and PD
+ EXPECT_NO_THROW(subnet->setLastAllocated(Lease::TYPE_TA, ta));
+ EXPECT_NO_THROW(subnet->setLastAllocated(Lease::TYPE_PD, pd));
+
+ EXPECT_EQ(ia.toText(), subnet->getLastAllocated(Lease::TYPE_NA).toText());
+ EXPECT_EQ(ta.toText(), subnet->getLastAllocated(Lease::TYPE_TA).toText());
+ EXPECT_EQ(pd.toText(), subnet->getLastAllocated(Lease::TYPE_PD).toText());
+
+ // No, you can't set the last allocated IPv4 address in IPv6 subnet
+ EXPECT_THROW(subnet->setLastAllocated(Lease::TYPE_V4, ia), BadValue);
+}
+
+// This test verifies that the IPv4 subnet can be fetched by id.
+TEST(SubnetFetcherTest, getSubnet4ById) {
+ Subnet4Collection collection;
+
+ // Subnet hasn't been added to the collection. A null pointer should
+ // be returned.
+ auto subnet = SubnetFetcher4::get(collection, SubnetID(1024));
+ EXPECT_FALSE(subnet);
+
+ subnet.reset(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3, 1024));
+ EXPECT_NO_THROW(collection.insert(subnet));
+
+ subnet.reset(new Subnet4(IOAddress("192.0.3.0"), 24, 1, 2, 3, 2048));
+ EXPECT_NO_THROW(collection.insert(subnet));
+
+ subnet = SubnetFetcher4::get(collection, SubnetID(1024));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ(1024, subnet->getID());
+ EXPECT_EQ("192.0.2.0/24", subnet->toText());
+
+ subnet = SubnetFetcher4::get(collection, SubnetID(2048));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ(2048, subnet->getID());
+ EXPECT_EQ("192.0.3.0/24", subnet->toText());
+}
+
+// This test verifies that the IPv6 subnet can be fetched by id.
+TEST(SubnetFetcherTest, getSubnet6ById) {
+ Subnet6Collection collection;
+
+ // Subnet hasn't been added to the collection. A null pointer should
+ // be returned.
+ auto subnet = SubnetFetcher6::get(collection, SubnetID(1026));
+ EXPECT_FALSE(subnet);
+
+ subnet.reset(new Subnet6(IOAddress("2001:db8:1::"), 64, 1, 2, 3, 4, 1024));
+ EXPECT_NO_THROW(collection.insert(subnet));
+
+ subnet.reset(new Subnet6(IOAddress("2001:db8:2::"), 64, 1, 2, 3, 4, 2048));
+ EXPECT_NO_THROW(collection.insert(subnet));
+
+ subnet = SubnetFetcher6::get(collection, SubnetID(1024));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ(1024, subnet->getID());
+ EXPECT_EQ("2001:db8:1::/64", subnet->toText());
+
+ subnet = SubnetFetcher6::get(collection, SubnetID(2048));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ(2048, subnet->getID());
+ EXPECT_EQ("2001:db8:2::/64", subnet->toText());
+}
+
+};
diff --git a/src/lib/dhcpsrv/tests/test_get_callout_handle.cc b/src/lib/dhcpsrv/tests/test_get_callout_handle.cc
new file mode 100644
index 0000000..a5eac6b
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/test_get_callout_handle.cc
@@ -0,0 +1,24 @@
+// Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcpsrv/callout_handle_store.h>
+#include "test_get_callout_handle.h"
+
+// Just instantiate the getCalloutHandle function and call it.
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+isc::hooks::CalloutHandlePtr
+testGetCalloutHandle(const Pkt6Ptr& pktptr) {
+ return (isc::dhcp::getCalloutHandle(pktptr));
+}
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
diff --git a/src/lib/dhcpsrv/tests/test_get_callout_handle.h b/src/lib/dhcpsrv/tests/test_get_callout_handle.h
new file mode 100644
index 0000000..c6f4a40
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/test_get_callout_handle.h
@@ -0,0 +1,38 @@
+// Copyright (C) 2013-2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef TEST_GET_CALLOUT_HANDLE_H
+#define TEST_GET_CALLOUT_HANDLE_H
+
+#include <dhcp/pkt6.h>
+#include <hooks/callout_handle.h>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @file
+/// @brief Get Callout Handle
+///
+/// This function is a shall around getCalloutHandle. It's purpose is to
+/// ensure that the getCalloutHandle() template function is referred to by
+/// two separate compilation units, and so test that data stored in one unit
+/// can be accessed by another. (This should be the case, but some compilers
+/// maybe be odd when it comes to template instantiation.)
+///
+/// @param pktptr Pointer to a Pkt6 object.
+///
+/// @return CalloutHandlePtr pointing to CalloutHandle associated with the
+/// Pkt6 object.
+isc::hooks::CalloutHandlePtr
+testGetCalloutHandle(const Pkt6Ptr& pktptr);
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
+
+
+#endif // TEST_GET_CALLOUT_HANDLE_H
diff --git a/src/lib/dhcpsrv/tests/test_libraries.h.in b/src/lib/dhcpsrv/tests/test_libraries.h.in
new file mode 100644
index 0000000..5a5545e
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/test_libraries.h.in
@@ -0,0 +1,35 @@
+// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef TEST_LIBRARIES_H
+#define TEST_LIBRARIES_H
+
+#include <config.h>
+
+namespace {
+
+// Names of the libraries used in these tests. These libraries are built using
+// libtool, so we need to look in the hidden ".libs" directory to locate the
+// shared library.
+
+// Library with load/unload functions creating marker files to check their
+// operation.
+static const char* CALLOUT_LIBRARY_1 = "@abs_builddir@/.libs/libco1.so";
+static const char* CALLOUT_LIBRARY_2 = "@abs_builddir@/.libs/libco2.so";
+
+// This library will try to get the following parameters:
+// - svalue (and will expect its value to be "string value")
+// - ivalue (and will expect its value to be 42)
+// - bvalue (and will expect its value to be true)
+static const char* CALLOUT_PARAMS_LIBRARY = "@abs_builddir@/.libs/libco3.so";
+
+// Name of a library which is not present.
+static const char* NOT_PRESENT_LIBRARY = "@abs_builddir@/.libs/libnothere.so";
+
+} // anonymous namespace
+
+
+#endif // TEST_LIBRARIES_H
diff --git a/src/lib/dhcpsrv/tests/timer_mgr_unittest.cc b/src/lib/dhcpsrv/tests/timer_mgr_unittest.cc
new file mode 100644
index 0000000..4f7b58d
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/timer_mgr_unittest.cc
@@ -0,0 +1,484 @@
+// Copyright (C) 2016-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/interval_timer.h>
+#include <asiolink/io_service.h>
+#include <dhcpsrv/timer_mgr.h>
+#include <exceptions/exceptions.h>
+#include <testutils/multi_threading_utils.h>
+
+#include <gtest/gtest.h>
+
+#include <functional>
+#include <sstream>
+#include <unistd.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::asiolink;
+using namespace isc::test;
+
+namespace {
+
+/// @brief Test fixture class for @c TimerMgr.
+class TimerMgrTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor
+ TimerMgrTest() = default;
+
+ /// @brief Destructor
+ virtual ~TimerMgrTest() = default;
+
+private:
+
+ /// @brief Prepares the class for a test.
+ virtual void SetUp();
+
+ /// @brief Cleans up after the test.
+ virtual void TearDown();
+
+public:
+
+ /// @brief Wrapper method for registering a new timer.
+ ///
+ /// This method registers a new timer in the @c TimerMgr. It associates a
+ /// @c timerCallback method with a timer. This method registers a number of
+ /// calls to the particular timer in the @c calls_count_ map.
+ ///
+ /// @param timer_name Unique timer name.
+ /// @param timer_interval Timer interval.
+ /// @param mode Interval timer mode, which defaults to
+ /// @c IntervalTimer::ONE_SHOT.
+ void registerTimer(const std::string& timer_name, const long timer_interval,
+ const IntervalTimer::Mode& timer_mode = IntervalTimer::ONE_SHOT);
+
+ /// @brief Wait for one or many ready handlers.
+ ///
+ /// @param timeout Wait timeout in milliseconds.
+ /// @param call_receive Indicates if the @c IfaceMgr::receive6
+ /// should be called to run pending callbacks and clear
+ /// watch sockets.
+ void doWait(const long timeout, const bool call_receive = true);
+
+ /// @brief Generic callback for timers under test.
+ ///
+ /// This callback increases the calls count for specified timer name.
+ ///
+ /// @param timer_name Name of the timer for which callback counter should
+ /// be increased.
+ void timerCallback(const std::string& timer_name);
+
+ /// @brief Callback which generates exception.
+ ///
+ /// This callback is used to test that the @c TimerMgr can handle
+ /// the case when the callback generates exceptions.
+ void timerCallbackWithException();
+
+ /// @brief Create a generic callback function for the timer.
+ ///
+ /// This is just a wrapped to make it a bit more convenient
+ /// in the test.
+ std::function<void ()> makeCallback(const std::string& timer_name);
+
+ /// @brief Create a callback which generates exception.
+ std::function<void ()> makeCallbackWithException();
+
+ /// @brief Callback for timeout.
+ ///
+ /// This callback indicates the test timeout by setting the
+ /// @c timeout_ member.
+ void timeoutCallback();
+
+ // @brief This test checks that certain errors are returned when invalid
+ // parameters are specified when registering a timer, or when
+ // the registration can't be made.
+ void testRegisterTimer();
+
+ /// @brief This test verifies that it is possible to unregister a timer from
+ /// the TimerMgr.
+ void testUnregisterTimer();
+
+ /// @brief This test verifies that it is possible to unregister all timers.
+ void testUnregisterTimers();
+
+ /// @brief This test verifies that the timer execution can be cancelled.
+ void testCancel();
+
+ /// @brief This test verifies that the callbacks for the scheduled timers
+ /// are actually called.
+ void testScheduleTimers();
+
+ /// @brief This test verifies that exceptions emitted from the callback
+ /// would be handled by the TimerMgr.
+ void testCallbackWithException();
+
+ /// @brief Type definition for a map holding calls counters for
+ /// timers.
+ typedef std::map<std::string, unsigned int> CallsCount;
+
+ /// @brief Pointer to IO service used by the tests.
+ IOServicePtr io_service_;
+
+ /// @brief Holds the calls count for test timers.
+ ///
+ /// The key of this map holds the timer names. The value holds the number
+ /// of calls to the timer handlers.
+ CallsCount calls_count_;
+
+ /// @brief Instance of @c TimerMgr used by the tests.
+ TimerMgrPtr timer_mgr_;
+};
+
+void
+TimerMgrTest::SetUp() {
+ io_service_.reset(new IOService());
+ timer_mgr_ = TimerMgr::instance();
+ timer_mgr_->setIOService(io_service_);
+ calls_count_.clear();
+}
+
+void
+TimerMgrTest::TearDown() {
+ // Remove all timers.
+ timer_mgr_->unregisterTimers();
+}
+
+void
+TimerMgrTest::registerTimer(const std::string& timer_name, const long timer_interval,
+ const IntervalTimer::Mode& timer_mode) {
+ // Register the timer with the generic callback that counts the
+ // number of callback invocations.
+ ASSERT_NO_THROW(
+ timer_mgr_->registerTimer(timer_name, makeCallback(timer_name), timer_interval,
+ timer_mode)
+ );
+
+ calls_count_[timer_name] = 0;
+
+}
+
+void
+TimerMgrTest::doWait(const long timeout, const bool /*call_receive*/) {
+ IntervalTimer timer(*io_service_);
+ timer.setup([this]() {
+ io_service_->stop();
+ }, timeout, IntervalTimer::ONE_SHOT);
+ io_service_->run();
+ io_service_->get_io_service().reset();
+}
+
+void
+TimerMgrTest::timerCallback(const std::string& timer_name) {
+ // Accumulate the number of calls to the timer handler.
+ ++calls_count_[timer_name];
+
+ // The timer installed is the ONE_SHOT timer, so we have
+ // to reschedule the timer.
+ timer_mgr_->setup(timer_name);
+}
+
+void
+TimerMgrTest::timerCallbackWithException() {
+ isc_throw(Exception, "timerCallbackWithException");
+}
+
+std::function<void ()>
+TimerMgrTest::makeCallback(const std::string& timer_name) {
+ return (std::bind(&TimerMgrTest::timerCallback, this, timer_name));
+}
+
+std::function<void ()>
+TimerMgrTest::makeCallbackWithException() {
+ return (std::bind(&TimerMgrTest::timerCallbackWithException, this));
+}
+
+// This test checks that certain errors are returned when invalid
+// parameters are specified when registering a timer, or when
+// the registration can't be made.
+void
+TimerMgrTest::testRegisterTimer() {
+ // Empty timer name is not allowed.
+ ASSERT_THROW(timer_mgr_->registerTimer("", makeCallback("timer1"), 1,
+ IntervalTimer::ONE_SHOT),
+ BadValue);
+
+ // Add a timer with a correct name.
+ ASSERT_NO_THROW(timer_mgr_->registerTimer("timer2", makeCallback("timer2"), 1,
+ IntervalTimer::ONE_SHOT));
+ EXPECT_TRUE(timer_mgr_->isTimerRegistered("timer2"));
+
+ // Adding the timer with the same name as the existing timer is not
+ // allowed.
+ ASSERT_THROW(timer_mgr_->registerTimer("timer2", makeCallback("timer2"), 1,
+ IntervalTimer::ONE_SHOT),
+ BadValue);
+}
+
+TEST_F(TimerMgrTest, registerTimer) {
+ // Disable Multi-Threading.
+ MultiThreadingTest mt(false);
+ testRegisterTimer();
+}
+
+TEST_F(TimerMgrTest, registerTimerMultiThreading) {
+ // Enable Multi-Threading.
+ MultiThreadingTest mt(true);
+ testRegisterTimer();
+}
+
+void
+TimerMgrTest::testUnregisterTimer() {
+ // Register a timer and start it.
+ ASSERT_NO_FATAL_FAILURE(registerTimer("timer1", 1));
+ ASSERT_EQ(1, timer_mgr_->timersCount());
+ ASSERT_NO_THROW(timer_mgr_->setup("timer1"));
+
+ // Wait for the timer to execute several times.
+ doWait(100);
+
+ // Remember how many times the timer's callback was executed.
+ const unsigned int calls_count = calls_count_["timer1"];
+ ASSERT_GT(calls_count, 0);
+
+ // Check that an attempt to unregister a non-existing timer would
+ // result in exception.
+ ASSERT_THROW(timer_mgr_->unregisterTimer("timer2"), BadValue);
+ // Number of timers shouldn't have changed.
+ ASSERT_EQ(1, timer_mgr_->timersCount());
+
+ // Now unregister the correct one.
+ ASSERT_NO_THROW(timer_mgr_->unregisterTimer("timer1"));
+ ASSERT_EQ(0, timer_mgr_->timersCount());
+ EXPECT_FALSE(timer_mgr_->isTimerRegistered("timer1"));
+
+ doWait(100);
+
+ // The number of calls for the timer1 shouldn't change as the
+ // timer had been unregistered.
+ EXPECT_EQ(calls_count_["timer1"], calls_count);
+}
+
+TEST_F(TimerMgrTest, unregisterTimer) {
+ // Disable Multi-Threading.
+ MultiThreadingTest mt(false);
+ testUnregisterTimer();
+}
+
+TEST_F(TimerMgrTest, unregisterTimerMultiThreading) {
+ // Enable Multi-Threading.
+ MultiThreadingTest mt(true);
+ testUnregisterTimer();
+}
+
+void
+TimerMgrTest::testUnregisterTimers() {
+ // Register 10 timers.
+ for (int i = 1; i <= 20; ++i) {
+ std::ostringstream s;
+ s << "timer" << i;
+ ASSERT_NO_FATAL_FAILURE(registerTimer(s.str(), 1))
+ << "fatal failure occurred while registering "
+ << s.str();
+ ASSERT_EQ(i, timer_mgr_->timersCount())
+ << "invalid number of registered timers returned";
+ ASSERT_NO_THROW(timer_mgr_->setup(s.str()))
+ << "exception thrown while calling setup() for the "
+ << s.str();
+ }
+
+ doWait(500);
+
+ // Make sure that all timers have been executed at least once.
+ for (CallsCount::iterator it = calls_count_.begin();
+ it != calls_count_.end(); ++it) {
+ unsigned int calls_count = it->second;
+ ASSERT_GT(calls_count, 0)
+ << "expected calls counter for timer"
+ << (std::distance(calls_count_.begin(), it) + 1)
+ << " greater than 0";
+ }
+
+ // Copy counters for all timers.
+ CallsCount calls_count(calls_count_);
+
+ // Let's unregister all timers.
+ ASSERT_NO_THROW(timer_mgr_->unregisterTimers());
+
+ // Make sure there are no timers registered.
+ ASSERT_EQ(0, timer_mgr_->timersCount());
+
+ doWait(500);
+
+ // The calls counter shouldn't change because there are
+ // no timers registered.
+ EXPECT_TRUE(calls_count == calls_count_);
+}
+
+TEST_F(TimerMgrTest, unregisterTimers) {
+ // Disable Multi-Threading.
+ MultiThreadingTest mt(false);
+ testUnregisterTimers();
+}
+
+TEST_F(TimerMgrTest, unregisterTimersMultiThreading) {
+ // Enable Multi-Threading.
+ MultiThreadingTest mt(true);
+ testUnregisterTimers();
+}
+
+void
+TimerMgrTest::testCancel() {
+ // Register timer.
+ ASSERT_NO_FATAL_FAILURE(registerTimer("timer1", 1));
+
+ // Kick in the timer and wait for 500ms.
+ ASSERT_NO_THROW(timer_mgr_->setup("timer1"));
+
+ doWait(500);
+
+ // Canceling non-existing timer should fail.
+ EXPECT_THROW(timer_mgr_->cancel("timer2"), BadValue);
+
+ // Canceling the good one should pass, even when the worker
+ // thread is running.
+ ASSERT_NO_THROW(timer_mgr_->cancel("timer1"));
+
+ // Remember how many calls have been invoked and wait for
+ // another 500ms.
+ unsigned int calls_count = calls_count_["timer1"];
+
+ doWait(500);
+
+ // The number of calls shouldn't change because the timer had been
+ // cancelled.
+ ASSERT_EQ(calls_count, calls_count_["timer1"]);
+
+ // Setup the timer again.
+ ASSERT_NO_THROW(timer_mgr_->setup("timer1"));
+
+ // Restart the thread.
+ doWait(500);
+
+ // New calls should be recorded.
+ EXPECT_GT(calls_count_["timer1"], calls_count);
+}
+
+TEST_F(TimerMgrTest, cancel) {
+ // Disable Multi-Threading.
+ MultiThreadingTest mt(false);
+ testCancel();
+}
+
+TEST_F(TimerMgrTest, cancelMultiThreading) {
+ // Enable Multi-Threading.
+ MultiThreadingTest mt(true);
+ testCancel();
+}
+
+void
+TimerMgrTest::testScheduleTimers() {
+ // Register two timers: 'timer1' and 'timer2'. The first timer will
+ // be executed at the 50ms interval. The second one at the 100ms
+ // interval.
+ ASSERT_NO_FATAL_FAILURE(registerTimer("timer1", 50));
+ ASSERT_NO_FATAL_FAILURE(registerTimer("timer2", 100));
+
+ // Kick in the timers.
+ ASSERT_NO_THROW(timer_mgr_->setup("timer1"));
+ ASSERT_NO_THROW(timer_mgr_->setup("timer2"));
+
+ // Run IfaceMgr::receive6() in the loop for 1000ms. This function
+ // will read data from the watch sockets created when the timers
+ // were registered. The data is delivered to the watch sockets
+ // at the interval of the timers, which should break the blocking
+ // call to receive6(). As a result, the callbacks associated
+ // with the watch sockets should be called.
+ doWait(1000);
+
+ // We have been running the timer for 1000ms at the interval of
+ // 50ms. The maximum number of callbacks is 20. However, the
+ // callback itself takes time. Stopping the thread takes time.
+ // So, the real number differs significantly. We don't know
+ // exactly how many have been executed. It should be more
+ // than 10 for sure. But we really made up the numbers here.
+ EXPECT_GT(calls_count_["timer1"], 10);
+ // For the second timer it should be more than 5.
+ EXPECT_GT(calls_count_["timer2"], 5);
+
+ // Because the interval of the 'timer1' is lower than the
+ // interval of the 'timer2' the number of calls should
+ // be higher for the 'timer1'.
+ EXPECT_GT(calls_count_["timer1"], calls_count_["timer2"]);
+
+ // Remember the number of calls from 'timer1' and 'timer2'.
+ unsigned int calls_count_timer1 = calls_count_["timer1"];
+ unsigned int calls_count_timer2 = calls_count_["timer2"];
+
+ // Unregister the 'timer1'.
+ ASSERT_NO_THROW(timer_mgr_->unregisterTimer("timer1"));
+
+ // Wait another 500ms. The 'timer1' was unregistered so it
+ // should not make any more calls. The 'timer2' should still
+ // work as previously.
+ doWait(500);
+
+ // The number of calls shouldn't have changed.
+ EXPECT_EQ(calls_count_timer1, calls_count_["timer1"]);
+ // There should be some new calls registered for the 'timer2'.
+ EXPECT_GT(calls_count_["timer2"], calls_count_timer2);
+}
+
+TEST_F(TimerMgrTest, scheduleTimers) {
+ // Disable Multi-Threading.
+ MultiThreadingTest mt(false);
+ testScheduleTimers();
+}
+
+TEST_F(TimerMgrTest, scheduleTimersMultiThreading) {
+ // Enable Multi-Threading.
+ MultiThreadingTest mt(true);
+ testScheduleTimers();
+}
+
+void
+TimerMgrTest::testCallbackWithException() {
+ // Create timer which will trigger callback generating exception.
+ ASSERT_NO_THROW(
+ timer_mgr_->registerTimer("timer1", makeCallbackWithException(), 1,
+ IntervalTimer::ONE_SHOT)
+ );
+
+ // Setup the timer.
+ ASSERT_NO_THROW(timer_mgr_->setup("timer1"));
+
+ // Start thread. We hope that exception will be caught by the @c TimerMgr
+ // and will not kill the process.
+ doWait(500);
+}
+
+TEST_F(TimerMgrTest, callbackWithException) {
+ // Disable Multi-Threading.
+ MultiThreadingTest mt(false);
+ testCallbackWithException();
+}
+
+TEST_F(TimerMgrTest, callbackWithExceptionMultiThreading) {
+ // Enable Multi-Threading.
+ MultiThreadingTest mt(true);
+ testCallbackWithException();
+}
+
+// This test verifies that IO service specified for the TimerMgr
+// must not be null.
+TEST_F(TimerMgrTest, setIOService) {
+ EXPECT_THROW(timer_mgr_->setIOService(IOServicePtr()),
+ BadValue);
+}
+
+} // end of anonymous namespace