diff options
Diffstat (limited to 'src/lib/dhcpsrv/tests')
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='[0;31m'; \ + grn='[0;32m'; \ + lgn='[1;32m'; \ + blu='[1;34m'; \ + mgn='[0;35m'; \ + brg='[1m'; \ + std='[m'; \ + 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("hostxampleˌom"); + std::string context_str("{ \"ºr\": true, \"foo\": false, \"x\": \"fac,tor\" }"); + + // 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("hostxampleˌom"); + std::string context_str("{ \"ºr\": true, \"foo\": false, \"x\": \"fac,tor\" }"); + + // 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\"," + " \"comment2\": \"don't release it\" }\n" + + "192.0.0.8,08:08:08:08:08:08,,800,6400,1,1,1,,0," + "{ \"a\": \"b\", \"c\": { \"d\": 1,\"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\"," + " \"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\", \"c\": { \"d\": 1,\"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 |