]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[5645] Moved HA hook library from premium repo to main repo.
authorMarcin Siodelski <marcin@isc.org>
Fri, 8 Jun 2018 10:37:16 +0000 (12:37 +0200)
committerMarcin Siodelski <marcin@isc.org>
Fri, 8 Jun 2018 10:37:16 +0000 (12:37 +0200)
40 files changed:
AUTHORS
configure.ac
src/hooks/dhcp/Makefile.am
src/hooks/dhcp/high_availability/.gitignore [new file with mode: 0644]
src/hooks/dhcp/high_availability/Doxyfile [new file with mode: 0644]
src/hooks/dhcp/high_availability/Doxyfile-xml [new file with mode: 0644]
src/hooks/dhcp/high_availability/Makefile.am [new file with mode: 0644]
src/hooks/dhcp/high_availability/command_creator.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/command_creator.h [new file with mode: 0644]
src/hooks/dhcp/high_availability/communication_state.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/communication_state.h [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha.dox [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_callouts.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_config.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_config.h [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_config_parser.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_config_parser.h [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_impl.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_impl.h [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_log.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_log.h [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_messages.mes [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_server_type.h [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_service.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_service.h [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_service_states.h [new file with mode: 0644]
src/hooks/dhcp/high_availability/query_filter.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/query_filter.h [new file with mode: 0644]
src/hooks/dhcp/high_availability/tests/.gitignore [new file with mode: 0644]
src/hooks/dhcp/high_availability/tests/Makefile.am [new file with mode: 0644]
src/hooks/dhcp/high_availability/tests/command_creator_unittest.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/tests/communication_state_unittest.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/tests/ha_impl_unittest.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/tests/ha_test.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/tests/ha_test.h [new file with mode: 0644]
src/hooks/dhcp/high_availability/tests/query_filter_unittest.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/tests/run_unittests.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/version.cc [new file with mode: 0644]

diff --git a/AUTHORS b/AUTHORS
index cfdcb815a22f010c93be0484d2f7dbd44bbe2aa5..60050cff6e4a9694e79f67a7d8c4412c974e5926 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -5,12 +5,12 @@
 Primary developers:
  - Tomek Mrugalski (lead developer: DHCPv4, DHCPv6 components, prefix
                     delegation, memfile, database interface, core libdhcp++,
-                   host reservation, MAC extraction in DHCPv6, statistics manager,
-                    kea-shell)
+                    host reservation, MAC extraction in DHCPv6,
+                    statistics manager, kea-shell)
  - Stephen Morris (Hooks, MySQL)
  - Marcin Siodelski (DHCPv4, DHCPv6 components, options handling, perfdhcp,
-                    host reservation, lease file cleanup, lease expiration,
-                     control agent, shared networks)
+                     host reservation, lease file cleanup, lease expiration,
+                     control agent, shared networks, high availability)
  - Thomas Markwalder (DDNS, user_chk)
  - Jeremy C. Reed (documentation, build system, testing, release engineering)
  - Wlodek Wencel (testing, release engineering)
index 4e9c1cfb956bd43b197d0e299726cab2a087359e..777ea8b02875aaf3c338a4f2fabd2aef4e732142 100644 (file)
@@ -1409,6 +1409,8 @@ AC_CONFIG_FILES([Makefile
                  src/bin/shell/tests/shell_unittest.py
                  src/hooks/Makefile
                  src/hooks/dhcp/Makefile
+                 src/hooks/dhcp/high_availability/Makefile
+                 src/hooks/dhcp/high_availability/tests/Makefile
                  src/hooks/dhcp/lease_cmds/Makefile
                  src/hooks/dhcp/lease_cmds/tests/Makefile
                  src/hooks/dhcp/user_chk/Makefile
index 26daf09e7fbb2480ed12a0d887a326c9166ef181..d8e33e26c86bb32a1251508ef5579190c6bfa2f1 100644 (file)
@@ -1 +1 @@
-SUBDIRS = user_chk lease_cmds stat_cmds
+SUBDIRS = high_availability lease_cmds stat_cmds user_chk
diff --git a/src/hooks/dhcp/high_availability/.gitignore b/src/hooks/dhcp/high_availability/.gitignore
new file mode 100644 (file)
index 0000000..0be2599
--- /dev/null
@@ -0,0 +1,4 @@
+/ha_messages.cc
+/ha_messages.h
+/s-messages
+/html
diff --git a/src/hooks/dhcp/high_availability/Doxyfile b/src/hooks/dhcp/high_availability/Doxyfile
new file mode 100644 (file)
index 0000000..8c90100
--- /dev/null
@@ -0,0 +1,2430 @@
+# Doxyfile 1.8.11
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single hash (#) is considered a comment and will be ignored.
+# The format is:
+# TAG = value [value, ...]
+# For lists, items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (\" \").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the config file
+# that follow. The default is UTF-8 which is also the encoding used for all text
+# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv
+# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv
+# for the list of possible encodings.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
+
+PROJECT_NAME           = "Kea High Availability Hook"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
+# could be handy for archiving the generated documentation or if some version
+# control system is used.
+
+PROJECT_NUMBER         =
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF          =
+
+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
+# in the documentation. The maximum height of the logo should not exceed 55
+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
+# the logo to the output directory.
+
+PROJECT_LOGO           = ../../../../../doc/guide/kea-logo-100x70.png
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. If a relative path is
+# entered, it will be relative to the location where doxygen was started. If
+# left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       = html
+
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
+# directories (in 2 levels) under the output directory of each output format and
+# will distribute the generated files over these directories. Enabling this
+# option can be useful when feeding doxygen a huge amount of source files, where
+# putting all generated files in the same directory would otherwise causes
+# performance problems for the file system.
+# The default value is: NO.
+
+CREATE_SUBDIRS         = YES
+
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES    = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
+
+OUTPUT_LANGUAGE        = English
+
+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
+# descriptions after the members that are listed in the file and class
+# documentation (similar to Javadoc). Set to NO to disable this.
+# The default value is: YES.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
+# description of a member or function before the detailed description
+#
+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+# The default value is: YES.
+
+REPEAT_BRIEF           = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator that is
+# used to form the text in various listings. Each string in this list, if found
+# as the leading text of the brief description, will be stripped from the text
+# and the result, after processing the whole list, is used as the annotated
+# text. Otherwise, the brief description is used as-is. If left blank, the
+# following values are used ($name is automatically replaced with the name of
+# the entity):The $name class, The $name widget, The $name file, is, provides,
+# specifies, contains, represents, a, an and the.
+
+ABBREVIATE_BRIEF       =
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# doxygen will generate a detailed section even if there is only a brief
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
+# before files name in the file list and in the header files. If set to NO the
+# shortest path that makes the file name unique will be used
+# The default value is: YES.
+
+FULL_PATH_NAMES        = NO
+
+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
+# Stripping is only done if one of the specified strings matches the left-hand
+# part of the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the path to
+# strip.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
+
+STRIP_FROM_PATH        =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
+# path mentioned in the documentation of a class, which tells the reader which
+# header file to include in order to use a class. If left blank only the name of
+# the header file containing the class definition is used. Otherwise one should
+# specify the list of include paths that are normally passed to the compiler
+# using the -I flag.
+
+STRIP_FROM_INC_PATH    =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
+# first line (until the first dot) of a Javadoc-style comment as the brief
+# description. If set to NO, the Javadoc-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
+
+JAVADOC_AUTOBRIEF      = YES
+
+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
+# line (until the first dot) of a Qt-style comment as the brief description. If
+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
+
+QT_AUTOBRIEF           = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
+# a brief description. This used to be the default behavior. The new default is
+# to treat a multi-line C++ comment block as a detailed description. Set this
+# tag to YES if you prefer the old behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
+# page for each member. If set to NO, the documentation of a member will be part
+# of the file/class/namespace that contains it.
+# The default value is: NO.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
+# uses this value to replace tabs by spaces in code fragments.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE               = 4
+
+# This tag can be used to specify a number of aliases that act as commands in
+# the documentation. An alias has the form:
+# name=value
+# For example adding
+# "sideeffect=@par Side Effects:\n"
+# will allow you to put the command \sideeffect (or @sideeffect) in the
+# documentation, which will result in a user-defined paragraph with heading
+# "Side Effects:". You can put \n's in the value part of an alias to insert
+# newlines.
+
+ALIASES                =
+
+# This tag can be used to specify a number of word-keyword mappings (TCL only).
+# A mapping has the form "name=value". For example adding "class=itcl::class"
+# will allow you to use the command class in the itcl::class meaning.
+
+TCL_SUBST              =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C. For
+# instance, some of the names that are used will be different. The list of all
+# members will be omitted, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C  = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
+
+OPTIMIZE_FOR_FORTRAN   = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_VHDL   = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension, and
+# language is one of the parsers supported by doxygen: IDL, Java, Javascript,
+# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran:
+# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran:
+# Fortran. In the later case the parser tries to guess whether the code is fixed
+# or free formatted code, this is the default for Fortran type files), VHDL. For
+# instance to make doxygen treat .inc files as Fortran files (default is PHP),
+# and .f files as C (default is Fortran), use: inc=Fortran f=C.
+#
+# Note: For files without extension you can use no_extension as a placeholder.
+#
+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
+# the files are not read by doxygen.
+
+EXTENSION_MAPPING      =
+
+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
+# according to the Markdown format, which allows for more readable
+# documentation. See http://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you can
+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
+# case of backward compatibilities issues.
+# The default value is: YES.
+
+MARKDOWN_SUPPORT       = YES
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_SUPPORT       = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should set this
+# tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string);
+# versus func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
+
+BUILTIN_STL_SUPPORT    = YES
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+# The default value is: NO.
+
+CPP_CLI_SUPPORT        = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen
+# will parse them like normal C++ but will assume all classes use public instead
+# of private inheritance when no explicit protection keyword is present.
+# The default value is: NO.
+
+SIP_SUPPORT            = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to replace the get and set methods by a property in the documentation.
+# This will only work if the methods are indeed getting or setting a simple
+# type. If this is not the case, or you want to show the methods anyway, you
+# should set this option to NO.
+# The default value is: YES.
+
+IDL_PROPERTY_SUPPORT   = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# If one adds a struct or class to a group and this option is enabled, then also
+# any nested class or struct is added to the same group. By default this option
+# is disabled and one has to add nested compounds explicitly via \ingroup.
+# The default value is: NO.
+
+GROUP_NESTED_COMPOUNDS = NO
+
+# Set the SUBGROUPING tag to YES to allow class member groups of the same type
+# (for instance a group of public functions) to be put as a subgroup of that
+# type (e.g. under the Public Functions section). Set it to NO to prevent
+# subgrouping. Alternatively, this can be done per class using the
+# \nosubgrouping command.
+# The default value is: YES.
+
+SUBGROUPING            = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
+# are shown inside the group in which they are included (e.g. using \ingroup)
+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
+# and RTF).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef fields will be shown inline in
+# the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
+
+INLINE_SIMPLE_STRUCTS  = NO
+
+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
+# enum is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically be
+# useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can be
+# an expensive process and often the same symbol appears multiple times in the
+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
+# doxygen will become slower. If the cache is too large, memory is wasted. The
+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
+# symbols. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
+
+LOOKUP_CACHE_SIZE      = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
+
+EXTRACT_ALL            = YES
+
+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PACKAGE        = NO
+
+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
+
+EXTRACT_STATIC         = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
+# locally in source files will be included in the documentation. If set to NO,
+# only classes defined in header files are included. Does not have any effect
+# for Java sources.
+# The default value is: YES.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. If set to YES, local methods,
+# which are defined in the implementation section but not in the interface are
+# included in the documentation. If set to NO, only methods in the interface are
+# included.
+# The default value is: NO.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
+
+EXTRACT_ANON_NSPACES   = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO these
+# members will be included in the various overviews, but no documentation
+# section is generated. This option has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy. If set
+# to NO, these classes will be included in the various overviews. This option
+# has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# (class|struct|union) declarations. If set to NO, these declarations will be
+# included in the documentation.
+# The default value is: NO.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
+# documentation blocks found inside the body of a function. If set to NO, these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
+
+INTERNAL_DOCS          = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file
+# names in lower-case letters. If set to YES, upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# and Mac users are advised to set this option to NO.
+# The default value is: system dependent.
+
+CASE_SENSE_NAMES       = YES
+
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES, the
+# scope will be hidden.
+# The default value is: NO.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
+# append additional text to a page's title, such as Class Reference. If set to
+# YES the compound reference will be hidden.
+# The default value is: NO.
+
+HIDE_COMPOUND_REFERENCE= NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC  = NO
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
+# files with double quotes in the documentation rather than with sharp brackets.
+# The default value is: NO.
+
+FORCE_LOCAL_INCLUDES   = NO
+
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
+# (detailed) documentation of file and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order.
+# The default value is: YES.
+
+SORT_MEMBER_DOCS       = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
+
+SORT_BRIEF_DOCS        = YES
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
+# (brief and detailed) documentation of class members so that constructors and
+# destructors are listed first. If set to NO the constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: NO.
+
+SORT_MEMBERS_CTORS_1ST = YES
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
+# of group names into alphabetical order. If set to NO the group names will
+# appear in their defined order.
+# The default value is: NO.
+
+SORT_GROUP_NAMES       = YES
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
+# fully-qualified names, including namespaces. If set to NO, the class list will
+# be sorted only by class name, not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
+
+SORT_BY_SCOPE_NAME     = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
+# type resolution of all parameters of a function it will reject a match between
+# the prototype and the implementation of a member function even if there is
+# only one candidate or it is obvious which candidate to choose by doing a
+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
+# accept a match between prototype and implementation in such cases.
+# The default value is: NO.
+
+STRICT_PROTO_MATCHING  = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
+# list. This list is created by putting \todo commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
+# list. This list is created by putting \test commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
+# list. This list is created by putting \bug commands in the documentation.
+# The default value is: YES.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
+# the deprecated list. This list is created by putting \deprecated commands in
+# the documentation.
+# The default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if <section_label> ... \endif and \cond <section_label>
+# ... \endcond blocks.
+
+ENABLED_SECTIONS       =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have for it to appear in the
+# documentation. If the initializer consists of more lines than specified here
+# it will be hidden. Use a value of 0 to hide initializers completely. The
+# appearance of the value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
+# the bottom of the documentation of classes and structs. If set to YES, the
+# list will mention the files that were used to generate the documentation.
+# The default value is: YES.
+
+SHOW_USED_FILES        = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
+# will remove the Files entry from the Quick Index and from the Folder Tree View
+# (if specified).
+# The default value is: YES.
+
+SHOW_FILES             = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
+# page. This will remove the Namespaces entry from the Quick Index and from the
+# Folder Tree View (if specified).
+# The default value is: YES.
+
+SHOW_NAMESPACES        = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command command input-file, where command is the value of the
+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
+# by doxygen. Whatever the program writes to standard output is used as the file
+# version. For an example see the documentation.
+
+FILE_VERSION_FILTER    =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option. You can
+# optionally specify a file name after the option, if omitted DoxygenLayout.xml
+# will be used as the name of the layout file.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
+
+LAYOUT_FILE            =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This requires the bibtex tool
+# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info.
+# For LaTeX the style of the bibliography can be controlled using
+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
+# search path. See also \cite for info how to create references.
+
+CITE_BIB_FILES         =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
+
+QUIET                  = YES
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
+
+WARNINGS               = YES
+
+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: YES.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some parameters
+# in a documented function, or documenting parameters that don't exist or using
+# markup commands wrongly.
+# The default value is: YES.
+
+WARN_IF_DOC_ERROR      = YES
+
+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
+# are documented, but have no documentation for their parameters or return
+# value. If set to NO, doxygen will only warn about wrong or incomplete
+# parameter documentation, but not about the absence of documentation.
+# The default value is: NO.
+
+WARN_NO_PARAMDOC       = NO
+
+# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
+# a warning is encountered.
+# The default value is: NO.
+
+WARN_AS_ERROR          = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that doxygen
+# can produce. The string should contain the $file, $line, and $text tags, which
+# will be replaced by the file and line number from which the warning originated
+# and the warning text. Optionally the format may contain $version, which will
+# be replaced by the version of the file (if it could be obtained via
+# FILE_VERSION_FILTER)
+# The default value is: $file:$line: $text.
+
+WARN_FORMAT            = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning and error
+# messages should be written. If left blank the output is written to standard
+# error (stderr).
+
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag is used to specify the files and/or directories that contain
+# documented source files. You may enter file names like myfile.cpp or
+# directories like /usr/src/myproject. Separate the files or directories with
+# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
+# Note: If this tag is empty the current directory is searched.
+
+INPUT                  =
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see: http://www.gnu.org/software/libiconv) for the list of
+# possible encodings.
+# The default value is: UTF-8.
+
+INPUT_ENCODING         = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
+# *.h) to filter out the source-files in the directories.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# read by doxygen.
+#
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
+# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
+# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
+# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f, *.for, *.tcl,
+# *.vhd, *.vhdl, *.ucf, *.qsf, *.as and *.js.
+
+FILE_PATTERNS          = *.c \
+                         *.cc \
+                         *.h \
+                         *.hpp \
+                         *.dox
+
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
+
+RECURSIVE              = NO
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+#
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE                =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+# The default value is: NO.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS        =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or directories
+# that contain example code fragments that are included (see the \include
+# command).
+
+EXAMPLE_PATH           =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS       =
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be included in the documentation (see the
+# \image command).
+
+IMAGE_PATH             = ../../../../../doc/images
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command:
+#
+# <filter> <input-file>
+#
+# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the
+# name of an input file. Doxygen will then use the output that the filter
+# program writes to standard output. If FILTER_PATTERNS is specified, this tag
+# will be ignored.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+INPUT_FILTER           =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form: pattern=filter
+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
+# patterns match the file name, INPUT_FILTER is applied.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+FILTER_PATTERNS        =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
+
+FILTER_SOURCE_FILES    = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
+# it is also possible to disable source filtering for a specific pattern using
+# *.ext= (so without naming a filter).
+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
+
+FILTER_SOURCE_PATTERNS =
+
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER         = YES
+
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# function all documented functions referencing it will be listed.
+# The default value is: NO.
+
+REFERENCED_BY_RELATION = YES
+
+# If the REFERENCES_RELATION tag is set to YES then for each documented function
+# all documented entities called/used by that function will be listed.
+# The default value is: NO.
+
+REFERENCES_RELATION    = YES
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
+# to YES then the hyperlinks from functions in REFERENCES_RELATION and
+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
+# link to the documentation.
+# The default value is: YES.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS        = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code will
+# point to the HTML generated by the htags(1) tool instead of doxygen built-in
+# source browser. The htags tool is part of GNU's global source tagging system
+# (see http://www.gnu.org/software/global/global.html). You will need version
+# 4.8.6 or higher.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the config file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+USE_HTAGS              = NO
+
+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
+# verbatim copy of the header file for each class for which an include is
+# specified. Set to NO to disable this.
+# See also: Section \class.
+# The default value is: YES.
+
+VERBATIM_HEADERS       = YES
+
+# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the
+# clang parser (see: http://clang.llvm.org/) for more accurate parsing at the
+# cost of reduced performance. This can be particularly helpful with template
+# rich C++ code for which doxygen's built-in parser lacks the necessary type
+# information.
+# Note: The availability of this option depends on whether or not doxygen was
+# generated with the -Duse-libclang=ON option for CMake.
+# The default value is: NO.
+
+CLANG_ASSISTED_PARSING = NO
+
+# If clang assisted parsing is enabled you can provide the compiler with command
+# line options that you would normally use when invoking the compiler. Note that
+# the include paths will already be set by doxygen for the files and directories
+# specified with INPUT and INCLUDE_PATH.
+# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.
+
+CLANG_OPTIONS          =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
+# compounds will be generated. Enable this if the project contains a lot of
+# classes, structs, unions or interfaces.
+# The default value is: YES.
+
+ALPHABETICAL_INDEX     = YES
+
+# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in
+# which the alphabetical index list will be split.
+# Minimum value: 1, maximum value: 20, default value: 5.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+COLS_IN_ALPHA_INDEX    = 2
+
+# In case all classes in a project start with a common prefix, all classes will
+# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
+# can be used to specify a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+IGNORE_PREFIX          =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT            = ../html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER            =
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER            =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET        =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list). For an example see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET  =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES       =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a colorwheel, see
+# http://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE    = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use grayscales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT    = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA  = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to YES can help to show when doxygen was last run and thus if the
+# documentation is up to date.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP         = YES
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS  = YES
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see: http://developer.apple.com/tools/xcode/), introduced with
+# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a
+# Makefile in the HTML output directory. Running make will produce the docset in
+# that directory and running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html
+# for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET        = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME        = "Doxygen generated docs"
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID       = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME  = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on
+# Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP      = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE               =
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION           =
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the master .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI           = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING     =
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND             = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP           = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE               =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE          =
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual-
+# folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER     = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME   =
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS  =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS  =
+
+# The QHG_LOCATION tag can be used to specify the location of Qt's
+# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the
+# generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION           =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP   = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID         = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX          = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW      = YES
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE   = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH         = 180
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW    = NO
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE       = 10
+
+# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are
+# not supported properly for IE 6.0, but are supported on all modern browsers.
+# Note that when changing this option you need to delete any form_*.png files
+# in the HTML output before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_TRANSPARENT    = YES
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# http://www.mathjax.org) which uses client side Javascript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX            = NO
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT         = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from http://www.mathjax.org before deployment.
+# The default value is: http://cdn.mathjax.org/mathjax/latest.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH        = http://cdn.mathjax.org/mathjax/latest
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS     =
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE       =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SEARCHENGINE           = NO
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a web server instead of a web client using Javascript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SERVER_BASED_SEARCH    = NO
+
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH        = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/). See the section "External Indexing and
+# Searching" for details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL       =
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE        = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID     =
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS  =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
+# The default value is: YES.
+
+GENERATE_LATEX         = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked.
+#
+# Note that when enabling USE_PDFLATEX this option is only used for generating
+# bitmaps for formulas in the HTML output, but not in the Makefile that is
+# written to the output directory.
+# The default file is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PAPER_TYPE             = a4wide
+
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. The package can be specified just
+# by its name or with the correct syntax as to be used with the LaTeX
+# \usepackage command. To get the times font for instance you can specify :
+# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}
+# To use the option intlimits with the amsmath package you can specify:
+# EXTRA_PACKAGES=[intlimits]{amsmath}
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+EXTRA_PACKAGES         =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
+# generated LaTeX document. The header should contain everything until the first
+# chapter. If it is left blank doxygen will generate a standard header. See
+# section "Doxygen usage" for information on how to let doxygen write the
+# default header to a separate file.
+#
+# Note: Only use a user-defined header if you know what you are doing! The
+# following commands have a special meaning inside the header: $title,
+# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
+# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
+# string, for the replacement values of the other commands the user is referred
+# to HTML_HEADER.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HEADER           =
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
+# generated LaTeX document. The footer should contain everything after the last
+# chapter. If it is left blank doxygen will generate a standard footer. See
+# LATEX_HEADER for more information on how to generate a default footer and what
+# special commands can be used inside the footer.
+#
+# Note: Only use a user-defined footer if you know what you are doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_FOOTER           =
+
+# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# LaTeX style sheets that are included after the standard style sheets created
+# by doxygen. Using this option one can overrule certain style aspects. Doxygen
+# will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_STYLESHEET =
+
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES      =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
+# contain links (just like the HTML output) instead of page references. This
+# makes the output suitable for online browsing using a PDF viewer.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PDF_HYPERLINKS         = NO
+
+# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
+# the PDF file directly from the LaTeX files. Set this option to YES, to get a
+# higher quality PDF documentation.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+USE_PDFLATEX           = NO
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
+# command to the generated LaTeX files. This will instruct LaTeX to keep running
+# if errors occur, instead of asking the user for help. This option is also used
+# when generating formulas in HTML.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE        = NO
+
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HIDE_INDICES     = NO
+
+# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
+# code with syntax highlighting in the LaTeX output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_SOURCE_CODE      = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. See
+# http://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BIB_STYLE        = plain
+
+# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
+# page will contain the date and time when the page was generated. Setting this
+# to NO can help when comparing the output of multiple runs.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_TIMESTAMP        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
+# RTF output is optimized for Word 97 and may not look too pretty with other RTF
+# readers/editors.
+# The default value is: NO.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: rtf.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
+# contain hyperlink fields. The RTF file will contain links (just like the HTML
+# output) instead of page references. This makes the output suitable for online
+# browsing using Word or some other Word compatible readers that support those
+# fields.
+#
+# Note: WordPad (write) and others do not support links.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_HYPERLINKS         = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's config
+# file, i.e. a series of assignments. You only have to provide replacements,
+# missing definitions are set to their default value.
+#
+# See also section "Doxygen usage" for information on how to generate the
+# default style sheet that doxygen normally uses.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_STYLESHEET_FILE    =
+
+# Set optional variables used in the generation of an RTF document. Syntax is
+# similar to doxygen's config file. A template extensions file can be generated
+# using doxygen -e rtf extensionFile.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_EXTENSIONS_FILE    =
+
+# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
+# with syntax highlighting in the RTF output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_SOURCE_CODE        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
+# classes and files.
+# The default value is: NO.
+
+GENERATE_MAN           = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it. A directory man3 will be created inside the directory specified by
+# MAN_OUTPUT.
+# The default directory is: man.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to the generated
+# man pages. In case the manual section does not start with a number, the number
+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
+# optional.
+# The default value is: .3.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_EXTENSION          = .3
+
+# The MAN_SUBDIR tag determines the name of the directory created within
+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
+# MAN_EXTENSION with the initial . removed.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_SUBDIR             =
+
+# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
+# will generate one additional man file for each entity documented in the real
+# man page(s). These additional files only source the real man page, but without
+# them the man command would be unable to find the correct page.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
+# captures the structure of the code including all documentation.
+# The default value is: NO.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_OUTPUT             = xml
+
+# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
+# listings (including syntax highlighting and cross-referencing information) to
+# the XML output. Note that enabling this will significantly increase the size
+# of the XML output.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_PROGRAMLISTING     = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
+# that can be used to generate PDF.
+# The default value is: NO.
+
+GENERATE_DOCBOOK       = NO
+
+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
+# front of it.
+# The default directory is: docbook.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_OUTPUT         = docbook
+
+# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
+# program listings (including syntax highlighting and cross-referencing
+# information) to the DOCBOOK output. Note that enabling this will significantly
+# increase the size of the DOCBOOK output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_PROGRAMLISTING = NO
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
+# AutoGen Definitions (see http://autogen.sf.net) file that captures the
+# structure of the code including all documentation. Note that this feature is
+# still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
+# file that captures the structure of the code including all documentation.
+#
+# Note that this feature is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
+# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
+# output from the Perl module output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
+# formatted so it can be parsed by a human reader. This is useful if you want to
+# understand what is going on. On the other hand, if this tag is set to NO, the
+# size of the Perl module output will be much smaller and Perl will parse it
+# just the same.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file are
+# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
+# so different doxyrules.make files included by the same Makefile don't
+# overwrite each other's variables.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
+# C-preprocessor directives found in the sources and include files.
+# The default value is: YES.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
+# in the source code. If set to NO, only conditional compilation will be
+# performed. Macro expansion can be done in a controlled way by setting
+# EXPAND_ONLY_PREDEF to YES.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+MACRO_EXPANSION        = YES
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
+# the macro expansion is limited to the macros specified with the PREDEFINED and
+# EXPAND_AS_DEFINED tags.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_ONLY_PREDEF     = NO
+
+# If the SEARCH_INCLUDES tag is set to YES, the include files in the
+# INCLUDE_PATH will be searched if a #include is found.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by the
+# preprocessor.
+# This tag requires that the tag SEARCH_INCLUDES is set to YES.
+
+INCLUDE_PATH           =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will be
+# used.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+INCLUDE_FILE_PATTERNS  =
+
+# The PREDEFINED tag can be used to specify one or more macro names that are
+# defined before the preprocessor is started (similar to the -D option of e.g.
+# gcc). The argument of the tag is a list of macros of the form: name or
+# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
+# is assumed. To prevent a macro definition from being undefined via #undef or
+# recursively expanded use the := operator instead of the = operator.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+PREDEFINED             =
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
+# tag can be used to specify a list of macro names that should be expanded. The
+# macro definition that is found in the sources will be used. Use the PREDEFINED
+# tag if you want to use a different macro definition that overrules the
+# definition found in the source code.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_AS_DEFINED      =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
+# remove all references to function-like macros that are alone on a line, have
+# an all uppercase name, and do not end with a semicolon. Such function macros
+# are typically used for boiler-plate code, and will confuse the parser if not
+# removed.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES tag can be used to specify one or more tag files. For each tag
+# file the location of the external documentation should be added. The format of
+# a tag file without this location is as follows:
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where loc1 and loc2 can be relative or absolute paths or URLs. See the
+# section "Linking to external documentation" for more information about the use
+# of tag files.
+# Note: Each tag file must have a unique name (where the name does NOT include
+# the path). If a tag file is not located in the directory in which doxygen is
+# run, you must also specify the path to the tagfile here.
+
+TAGFILES               =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
+# tag file that is based on the input files it reads. See section "Linking to
+# external documentation" for more information about the usage of tag files.
+
+GENERATE_TAGFILE       =
+
+# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
+# the class index. If set to NO, only the inherited external classes will be
+# listed.
+# The default value is: NO.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will be
+# listed.
+# The default value is: YES.
+
+EXTERNAL_GROUPS        = YES
+
+# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
+# the related pages index. If set to NO, only the current project's pages will
+# be listed.
+# The default value is: YES.
+
+EXTERNAL_PAGES         = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script
+# interpreter (i.e. the result of 'which perl').
+# The default file (with absolute path) is: /usr/bin/perl.
+
+PERL_PATH              = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
+# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
+# NO turns the diagrams off. Note that this option also works with HAVE_DOT
+# disabled, but it is recommended to install and use dot, since it yields more
+# powerful graphs.
+# The default value is: YES.
+
+CLASS_DIAGRAMS         = YES
+
+# You can define message sequence charts within doxygen comments using the \msc
+# command. Doxygen will then run the mscgen tool (see:
+# http://www.mcternan.me.uk/mscgen/)) to produce the chart and insert it in the
+# documentation. The MSCGEN_PATH tag allows you to specify the directory where
+# the mscgen tool resides. If left empty the tool is assumed to be found in the
+# default search path.
+
+MSCGEN_PATH            =
+
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
+
+DIA_PATH               =
+
+# If set to YES the inheritance and collaboration graphs will hide inheritance
+# and usage relations if the target is undocumented or is not a class.
+# The default value is: YES.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz (see:
+# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
+# Bell Labs. The other options in this section have no effect if this option is
+# set to NO
+# The default value is: YES.
+
+HAVE_DOT               = YES
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
+# to run in parallel. When set to 0 doxygen will base this on the number of
+# processors available in the system. You can set it explicitly to a value
+# larger than 0 to get control over the balance between CPU load and processing
+# speed.
+# Minimum value: 0, maximum value: 32, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_NUM_THREADS        = 0
+
+# When you want a differently looking font in the dot files that doxygen
+# generates you can specify the font name using DOT_FONTNAME. You need to make
+# sure dot is able to find the font, which can be done by putting it in a
+# standard location or by setting the DOTFONTPATH environment variable or by
+# setting DOT_FONTPATH to the directory containing the font.
+# The default value is: Helvetica.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTNAME           = Helvetica
+
+# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
+# dot graphs.
+# Minimum value: 4, maximum value: 24, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTSIZE           = 10
+
+# By default doxygen will tell dot to use the default font as specified with
+# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
+# the path where dot can find it using this tag.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTPATH           =
+
+# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
+# each documented class showing the direct and indirect inheritance relations.
+# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
+# graph for each documented class showing the direct and indirect implementation
+# dependencies (inheritance, containment, and class references variables) of the
+# class with other documented classes.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+COLLABORATION_GRAPH    = NO
+
+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
+# groups, showing the direct groups dependencies.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LOOK               = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
+# class node. If there are many fields or methods and many nodes the graph may
+# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
+# number of items for each type to make the size more manageable. Set this to 0
+# for no limit. Note that the threshold may be exceeded by 50% before the limit
+# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
+# but if the number exceeds 15, the total amount of fields shown is limited to
+# 10.
+# Minimum value: 0, maximum value: 100, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LIMIT_NUM_FIELDS   = 10
+
+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
+# collaboration graphs will show the relations between templates and their
+# instances.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+TEMPLATE_RELATIONS     = NO
+
+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
+# YES then doxygen will generate a graph for each documented file showing the
+# direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDE_GRAPH          = YES
+
+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
+# set to YES then doxygen will generate a graph for each documented file showing
+# the direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable call graphs for selected
+# functions only using the \callgraph command. Disabling a call graph can be
+# accomplished by means of the command \hidecallgraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALL_GRAPH             = YES
+
+# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable caller graphs for selected
+# functions only using the \callergraph command. Disabling a caller graph can be
+# accomplished by means of the command \hidecallergraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALLER_GRAPH           = NO
+
+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
+# hierarchy of all classes instead of a textual one.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
+# dependencies a directory has on other directories in a graphical way. The
+# dependency relations are determined by the #include relations between the
+# files in the directories.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. For an explanation of the image formats see the section
+# output formats in the documentation of the dot tool (Graphviz (see:
+# http://www.graphviz.org/)).
+# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
+# to make the SVG files visible in IE 9+ (other browsers do not have this
+# requirement).
+# Possible values are: png, png:cairo, png:cairo:cairo, png:cairo:gd, png:gd,
+# png:gd:gd, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd, gif, gif:cairo,
+# gif:cairo:gd, gif:gd, gif:gd:gd, svg, png:gd, png:gd:gd, png:cairo,
+# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
+# png:gdiplus:gdiplus.
+# The default value is: png.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_IMAGE_FORMAT       = png
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+#
+# Note that this requires a modern browser other than Internet Explorer. Tested
+# and working are Firefox, Chrome, Safari, and Opera.
+# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
+# the SVG files visible. Older versions of IE do not have SVG support.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INTERACTIVE_SVG        = NO
+
+# The DOT_PATH tag can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_PATH               =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the \dotfile
+# command).
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOTFILE_DIRS           =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the \mscfile
+# command).
+
+MSCFILE_DIRS           =
+
+# The DIAFILE_DIRS tag can be used to specify one or more directories that
+# contain dia files that are included in the documentation (see the \diafile
+# command).
+
+DIAFILE_DIRS           =
+
+# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
+# path where java can find the plantuml.jar file. If left blank, it is assumed
+# PlantUML is not used or called during a preprocessing step. Doxygen will
+# generate a warning when it encounters a \startuml command in this case and
+# will not generate output for the diagram.
+
+PLANTUML_JAR_PATH      =
+
+# When using plantuml, the specified paths are searched for files specified by
+# the !include statement in a plantuml block.
+
+PLANTUML_INCLUDE_PATH  =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
+# that will be shown in the graph. If the number of nodes in a graph becomes
+# larger than this value, doxygen will truncate the graph, which is visualized
+# by representing a node as a red box. Note that doxygen if the number of direct
+# children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
+# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+# Minimum value: 0, maximum value: 10000, default value: 50.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_GRAPH_MAX_NODES    = 200
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
+# generated by dot. A depth value of 3 means that only nodes reachable from the
+# root by following a path via at most 3 edges will be shown. Nodes that lay
+# further from the root node will be omitted. Note that setting this option to 1
+# or 2 may greatly reduce the computation time needed for large code bases. Also
+# note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+# Minimum value: 0, maximum value: 1000, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not seem
+# to support this out of the box.
+#
+# Warning: Depending on the platform used, enabling this option may lead to
+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
+# read).
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_TRANSPARENT        = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10) support
+# this, this feature is disabled by default.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_MULTI_TARGETS      = NO
+
+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
+# explaining the meaning of the various boxes and arrows in the dot generated
+# graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot
+# files that are used to generate the various graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_CLEANUP            = YES
diff --git a/src/hooks/dhcp/high_availability/Doxyfile-xml b/src/hooks/dhcp/high_availability/Doxyfile-xml
new file mode 100644 (file)
index 0000000..ae5be8a
--- /dev/null
@@ -0,0 +1,7 @@
+# This is a doxygen configuration for generating XML output as well as HTML.
+#
+# Inherit everything from our default Doxyfile except GENERATE_XML, which
+# will be reset to YES
+
+@INCLUDE = Doxyfile
+GENERATE_XML           = YES
diff --git a/src/hooks/dhcp/high_availability/Makefile.am b/src/hooks/dhcp/high_availability/Makefile.am
new file mode 100644 (file)
index 0000000..3ed10ef
--- /dev/null
@@ -0,0 +1,79 @@
+SUBDIRS = . tests
+
+AM_CPPFLAGS  = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += $(BOOST_INCLUDES)
+AM_CXXFLAGS  = $(KEA_CXXFLAGS)
+
+# Define rule to build logging source files from message file
+ha_messages.h ha_messages.cc: s-messages
+s-messages: ha_messages.mes
+       $(top_builddir)/src/lib/log/compiler/kea-msg-compiler $(top_srcdir)/src/hooks/dhcp/high_availability/ha_messages.mes
+       touch $@
+
+# Tell automake that the message files are built as part of the build process
+# (so that they are built before the main library is built).
+BUILT_SOURCES = ha_messages.h ha_messages.cc
+
+# Ensure that the message file is included in the distribution
+EXTRA_DIST = ha_messages.mes
+
+# Get rid of generated message files on a clean
+CLEANFILES = *.gcno *.gcda ha_messages.h ha_messages.cc s-messages
+
+# convenience archive
+
+noinst_LTLIBRARIES = libha.la
+
+libha_la_SOURCES  = command_creator.cc command_creator.h
+libha_la_SOURCES += communication_state.cc communication_state.h
+libha_la_SOURCES += ha_callouts.cc
+libha_la_SOURCES += ha_config.cc ha_config.h
+libha_la_SOURCES += ha_config_parser.cc ha_config_parser.h
+libha_la_SOURCES += ha_impl.cc ha_impl.h
+libha_la_SOURCES += ha_log.cc ha_log.h
+libha_la_SOURCES += ha_server_type.h
+libha_la_SOURCES += ha_service.cc ha_service.h
+libha_la_SOURCES += ha_service_states.h
+libha_la_SOURCES += query_filter.cc query_filter.h
+libha_la_SOURCES += version.cc
+
+nodist_libha_la_SOURCES = ha_messages.cc ha_messages.h
+
+libha_la_CXXFLAGS = $(AM_CXXFLAGS)
+libha_la_CPPFLAGS = $(AM_CPPFLAGS)
+
+# install the shared object into $(libdir)/hooks
+lib_hooksdir = $(libdir)/hooks
+lib_hooks_LTLIBRARIES = libdhcp_ha.la
+
+libdhcp_ha_la_SOURCES  =
+libdhcp_ha_la_LDFLAGS  = $(AM_LDFLAGS)
+libdhcp_ha_la_LDFLAGS  += -avoid-version -export-dynamic -module
+
+libdhcp_ha_la_LIBADD  = libha.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/eval/libkea-eval.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/stats/libkea-stats.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/http/libkea-http.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la
+libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
+
+EXTRA_DIST += ha.dox Doxyfile Doxyfile-xml
+
+devel:
+       mkdir -p html
+       (cat Doxyfile; echo PROJECT_NUMBER=$(PACKAGE_VERSION)) | doxygen - > html/doxygen.log 2> html/doxygen-error.log
+       echo `grep -i ": warning:" html/doxygen-error.log | wc -l` warnings/errors detected.
+
+clean-local:
+       rm -rf html
diff --git a/src/hooks/dhcp/high_availability/command_creator.cc b/src/hooks/dhcp/high_availability/command_creator.cc
new file mode 100644 (file)
index 0000000..8fddbd7
--- /dev/null
@@ -0,0 +1,131 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <config.h>
+
+#include <command_creator.h>
+#include <cc/command_interpreter.h>
+#include <exceptions/exceptions.h>
+#include <boost/pointer_cast.hpp>
+
+using namespace isc::data;
+using namespace isc::dhcp;
+
+namespace isc {
+namespace ha {
+
+ConstElementPtr
+CommandCreator::createDHCPDisable(const unsigned int max_period,
+                                  const HAServerType& server_type) {
+    ElementPtr args;
+    // max-period is optional. A value of 0 means that it is not specified.
+    if (max_period > 0) {
+        args = Element::createMap();
+        args->set("max-period", Element::create(static_cast<long int>(max_period)));
+    }
+    ConstElementPtr command = config::createCommand("dhcp-disable", args);
+    insertService(command, server_type);
+    return (command);
+}
+
+ConstElementPtr
+CommandCreator::createDHCPEnable(const HAServerType& server_type) {
+    ConstElementPtr command = config::createCommand("dhcp-enable");
+    insertService(command, server_type);
+    return (command);
+}
+
+ConstElementPtr
+CommandCreator::createHeartbeat(const HAServerType& server_type) {
+    ConstElementPtr command = config::createCommand("ha-heartbeat");
+    insertService(command, server_type);
+    return (command);
+}
+
+ConstElementPtr
+CommandCreator::createLease4Update(const Lease4& lease4) {
+    ElementPtr lease_as_json = lease4.toElement();
+    insertLeaseExpireTime(lease_as_json);
+    lease_as_json->set("force-create", Element::create(true));
+    ConstElementPtr command = config::createCommand("lease4-update", lease_as_json);
+    insertService(command, HAServerType::DHCPv4);
+    return (command);
+}
+
+ConstElementPtr
+CommandCreator::createLease4Delete(const Lease4& lease4) {
+    ElementPtr lease_as_json = lease4.toElement();
+    insertLeaseExpireTime(lease_as_json);
+    ConstElementPtr command = config::createCommand("lease4-del", lease_as_json);
+    insertService(command, HAServerType::DHCPv4);
+    return (command);
+}
+
+ConstElementPtr
+CommandCreator::createLease4GetAll() {
+    ConstElementPtr command = config::createCommand("lease4-get-all");
+    insertService(command, HAServerType::DHCPv4);
+    return (command);
+}
+
+ConstElementPtr
+CommandCreator::createLease6Update(const Lease6& lease6) {
+    ElementPtr lease_as_json = lease6.toElement();
+    insertLeaseExpireTime(lease_as_json);
+    lease_as_json->set("force-create", Element::create(true));
+    ConstElementPtr command = config::createCommand("lease6-update", lease_as_json);
+    insertService(command, HAServerType::DHCPv6);
+    return (command);
+}
+
+ConstElementPtr
+CommandCreator::createLease6Delete(const Lease6& lease6) {
+    ElementPtr lease_as_json = lease6.toElement();
+    insertLeaseExpireTime(lease_as_json);
+    ConstElementPtr command = config::createCommand("lease6-del", lease_as_json);
+    insertService(command, HAServerType::DHCPv6);
+    return (command);
+}
+
+ConstElementPtr
+CommandCreator::createLease6GetAll() {
+    ConstElementPtr command = config::createCommand("lease6-get-all");
+    insertService(command, HAServerType::DHCPv6);
+    return (command);
+}
+
+void
+CommandCreator::insertLeaseExpireTime(ElementPtr& lease) {
+    if ((lease->getType() != Element::map) ||
+        (!lease->contains("cltt") || (lease->get("cltt")->getType() != Element::integer) ||
+         (!lease->contains("valid-lft") ||
+          (lease->get("valid-lft")->getType() != Element::integer)))) {
+        isc_throw(Unexpected, "invalid lease format");
+    }
+
+    int64_t cltt = lease->get("cltt")->intValue();
+    int64_t valid_lifetime = lease->get("valid-lft")->intValue();
+    int64_t expire = cltt + valid_lifetime;
+    lease->set("expire", Element::create(expire));
+    lease->remove("cltt");
+}
+
+void
+CommandCreator::insertService(ConstElementPtr& command,
+                              const HAServerType& server_type) {
+    ElementPtr service = Element::createList();
+    const std::string service_name = (server_type == HAServerType::DHCPv4 ? "dhcp4" : "dhcp6");
+    service->add(Element::create(service_name));
+
+    // We have no better way of setting a new element here than
+    // doing const pointer cast. That's another reason why this
+    // functionality could be moved to the core code. We don't
+    // do it however, because we want to minimize concurrent
+    // code changes in the premium and core Kea repos.
+    (boost::const_pointer_cast<Element>(command))->set("service", service);
+}
+
+} // end of namespace ha
+} // end of namespace isc
diff --git a/src/hooks/dhcp/high_availability/command_creator.h b/src/hooks/dhcp/high_availability/command_creator.h
new file mode 100644 (file)
index 0000000..495bbd1
--- /dev/null
@@ -0,0 +1,137 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#ifndef HA_COMMAND_CREATOR_H
+#define HA_COMMAND_CREATOR_H
+
+#include <ha_server_type.h>
+#include <cc/data.h>
+#include <dhcpsrv/lease.h>
+#include <string>
+
+namespace isc {
+namespace ha {
+
+/// @brief Holds a collection of functions which generate commands
+/// used for High Availability.
+class CommandCreator {
+public:
+
+    /// @brief Creates dhcp-disable command for DHCP server.
+    ///
+    /// @return Pointer to the JSON representation of the command.
+    static data::ConstElementPtr
+    createDHCPDisable(const unsigned int max_period,
+                      const HAServerType& server_type);
+
+    /// @brief Creates dhcp-enable command for DHCP server.
+    ///
+    /// @return Pointer to the JSON representation of the command.
+    static data::ConstElementPtr
+    createDHCPEnable(const HAServerType& server_type);
+
+    /// @brief Creates ha-heartbeat command for DHCP server.
+    ///
+    /// @return Pointer to the JSON representation of the command.
+    static data::ConstElementPtr
+    createHeartbeat(const HAServerType& server_type);
+
+    /// @brief Creates lease4-update command.
+    ///
+    /// It adds "force-create" parameter to the lease information to force
+    /// the remote server to create the lease if it doesn't exist in its
+    /// lease database.
+    ///
+    /// @param lease4 Reference to a lease for which the command should
+    /// be created.
+    ///
+    /// @return Pointer to the JSON representation of the command.
+    static data::ConstElementPtr
+    createLease4Update(const dhcp::Lease4& lease4);
+
+    /// @brief Creates lease4-del command.
+    ///
+    /// @param lease4 Reference to a lease for which the command should
+    /// be created.
+    ///
+    /// @return Pointer to the JSON representation of the command.
+    static data::ConstElementPtr
+    createLease4Delete(const dhcp::Lease4& lease4);
+
+    /// @brief Creates lease4-get-all command.
+    ///
+    /// @return Pointer to the JSON representation of the command.
+    static data::ConstElementPtr
+    createLease4GetAll();
+
+    /// @brief Creates lease6-update command.
+    ///
+    /// It adds "force-create" parameter to the lease information to force
+    /// the remote server to create the lease if it doesn't exist in its
+    /// lease database.
+    ///
+    /// @param lease6 Reference to a lease for which the command should
+    /// be created.
+    ///
+    /// @return Pointer to the JSON representation of the command.
+    static data::ConstElementPtr
+    createLease6Update(const dhcp::Lease6& lease6);
+
+    /// @brief Creates lease6-del command.
+    ///
+    /// @param lease6 Reference to a lease for which the command should
+    /// be created.
+    ///
+    /// @return Pointer to the JSON representation of the command.
+    static data::ConstElementPtr
+    createLease6Delete(const dhcp::Lease6& lease6);
+
+    /// @brief Creates lease6-get-all command.
+    ///
+    /// @return Pointer to the JSON representation of the command.
+    static data::ConstElementPtr
+    createLease6GetAll();
+
+private:
+
+    /// @brief Replaces "cltt" with "expire" value within the lease.
+    ///
+    /// The "lease_cmds" hooks library expects "expire" time to be provided
+    /// for a lease rather than "cltt". If the "expire" is not provided
+    /// it will use the current time for a cltt. We want to make sure that
+    /// the lease is inserted into the lease database untouched.
+    /// Hence, this method is used to replace "cltt" with "expire" time in
+    /// the lease.
+    ///
+    /// @param lease in the JSON format created using @c Lease::toElement
+    /// method.
+    static void insertLeaseExpireTime(data::ElementPtr& lease);
+
+    /// @brief Sets "service" parameter for the command.
+    ///
+    /// Commands generated by the HA hooks library are always sent to
+    /// DHCPv4 or DHCPv6 server via Control Agent. The Control Agent
+    /// requires a "service" parameter which provides the list of servers
+    /// to which the command should be forwarded. In our case, we always
+    /// send commands to a single server so this method appends a single
+    /// element list to the command.
+    ///
+    /// @todo We should consider moving this functionality to the main
+    /// Kea code.
+    ///
+    /// @param [out] command command to which the service parameter must
+    /// be inserted.
+    /// @param server_type DHCP server type, i.e. DHCPv4 or DHCPv6.
+    ///
+    /// @return Pointer to the command with service parameter inserted.
+    static void
+    insertService(data::ConstElementPtr& command,
+                  const HAServerType& server_type);
+};
+
+} // end of namespace ha
+} // end of namespace isc
+
+#endif
diff --git a/src/hooks/dhcp/high_availability/communication_state.cc b/src/hooks/dhcp/high_availability/communication_state.cc
new file mode 100644 (file)
index 0000000..5bce7da
--- /dev/null
@@ -0,0 +1,351 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <config.h>
+
+#include <communication_state.h>
+#include <ha_service_states.h>
+#include <exceptions/exceptions.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/option_int.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt6.h>
+#include <http/date_time.h>
+#include <boost/bind.hpp>
+#include <boost/pointer_cast.hpp>
+#include <sstream>
+#include <utility>
+
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::http;
+using namespace boost::posix_time;
+
+namespace {
+
+/// @brief Warning is issued if the clock skew exceeds this value.
+constexpr long WARN_CLOCK_SKEW = 30;
+
+/// @brief HA service terminates if the clock skew exceeds this value.
+constexpr long TERM_CLOCK_SKEW = 60;
+
+/// @brief Minimum time between two consecutive clock skew warnings.
+constexpr long MIN_TIME_SINCE_CLOCK_SKEW_WARN = 60;
+
+}
+
+namespace isc {
+namespace ha {
+
+CommunicationState::CommunicationState(const IOServicePtr& io_service,
+                                       const HAConfigPtr& config)
+    : io_service_(io_service), config_(config), timer_(), interval_(0),
+      poke_time_(boost::posix_time::microsec_clock::universal_time()),
+      heartbeat_impl_(0), partner_state_(-1), clock_skew_(0, 0, 0, 0),
+      last_clock_skew_warn_() {
+}
+
+CommunicationState::~CommunicationState() {
+    stopHeartbeat();
+}
+
+void
+CommunicationState::setPartnerState(const std::string& state) {
+    if (state == "hot-standby") {
+        partner_state_ = HA_HOT_STANDBY_ST;
+    } else if (state == "load-balancing") {
+        partner_state_ = HA_LOAD_BALANCING_ST;
+    } else if (state == "partner-down") {
+        partner_state_ = HA_PARTNER_DOWN_ST;
+    } else if (state == "ready") {
+        partner_state_ = HA_READY_ST;
+    } else if (state == "syncing") {
+        partner_state_ = HA_SYNCING_ST;
+    } else if (state == "terminated") {
+        partner_state_ = HA_TERMINATED_ST;
+    } else if (state == "waiting") {
+        partner_state_ = HA_WAITING_ST;
+    } else if (state == "unavailable") {
+        partner_state_ = HA_UNAVAILABLE_ST;
+    } else {
+        isc_throw(BadValue, "unsupported HA partner state returned "
+                  << state);
+    }
+}
+
+void
+CommunicationState::startHeartbeat(const long interval,
+                                   const boost::function<void()>& heartbeat_impl) {
+    startHeartbeatInternal(interval, heartbeat_impl);
+}
+
+void
+CommunicationState::startHeartbeatInternal(const long interval,
+                                           const boost::function<void()>& heartbeat_impl) {
+    bool settings_modified = false;
+
+    // If we're setting the heartbeat for the first time, it should
+    // be non-null.
+    if (heartbeat_impl) {
+        settings_modified = true;
+        heartbeat_impl_ = heartbeat_impl;
+
+    } else if (!heartbeat_impl_) {
+        // The heartbeat is re-scheduled but we have no historic implementation
+        // pointer we could re-use. This is a programmatic issue.
+        isc_throw(BadValue, "unable to start heartbeat when pointer"
+                  " to the heartbeat implementation is not specified");
+    }
+
+    // If we're setting the heartbeat for the first time, the interval
+    // should be greater than 0.
+    if (interval != 0) {
+        settings_modified |= (interval_ != interval);
+        interval_ = interval;
+
+    } else if (interval_ <= 0) {
+        // The heartbeat is re-scheduled but we have no historic interval
+        // which we could re-use. This is a programmatic issue.
+        heartbeat_impl_ = 0;
+        isc_throw(BadValue, "unable to start heartbeat when interval"
+                  " for the heartbeat timer is not specified");
+    }
+
+    if (!timer_) {
+        timer_.reset(new IntervalTimer(*io_service_));
+    }
+
+    if (settings_modified) {
+        timer_->setup(heartbeat_impl_, interval_, IntervalTimer::ONE_SHOT);
+    }
+}
+
+void
+CommunicationState::stopHeartbeat() {
+    if (timer_) {
+        timer_->cancel();
+        timer_.reset();
+        interval_ = 0;
+        heartbeat_impl_ = 0;
+    }
+}
+
+void
+CommunicationState::poke() {
+    // Remember previous poke time.
+    boost::posix_time::ptime prev_poke_time = poke_time_;
+    // Set poke time to the current time.
+    poke_time_ = boost::posix_time::microsec_clock::universal_time();
+
+    // If we have been tracking the unanswered DHCP messages directed to the
+    // partner, we need to clear any gathered information because the connection
+    // seems to be (re)established.
+    clearUnackedClients();
+
+    if (timer_) {
+        // Check the duration since last poke. If it is less than a second, we don't
+        // want to reschedule the timer. The only case when the poke time duration is
+        // lower than 1s is when we're performing lease updates. In order to avoid the
+        // overhead of re-scheduling the timer too frequently we reschedule it only if the
+        // duration is 1s or more. This matches the time resolution for heartbeats.
+        boost::posix_time::time_duration duration_since_poke = poke_time_ - prev_poke_time;
+        if (duration_since_poke.total_seconds() > 0) {
+            // A poke causes the timer to be re-scheduled to prevent it
+            // from triggering a heartbeat shortly after confirming the
+            // connection is ok, based on the lease update or another
+            // command.
+            startHeartbeatInternal();
+        }
+    }
+}
+
+int64_t
+CommunicationState::getDurationInMillisecs() const {
+    ptime now = boost::posix_time::microsec_clock::universal_time();
+    time_duration duration = now - poke_time_;
+    return (duration.total_milliseconds());
+}
+
+bool
+CommunicationState::isCommunicationInterrupted() const {
+    return (getDurationInMillisecs() > config_->getMaxResponseDelay());
+}
+
+bool
+CommunicationState::clockSkewShouldWarn() {
+    // First check if the clock skew is beyond the threshold.
+    if (isClockSkewGreater(WARN_CLOCK_SKEW)) {
+
+        // In order to prevent to frequent warnings we provide a gating mechanism
+        // which doesn't allow for issuing a warning earlier than 60 seconds after
+        // the previous one.
+
+        // Find the current time and the duration since last warning.
+        ptime now = boost::posix_time::microsec_clock::universal_time();
+        time_duration since_warn_duration = now - last_clock_skew_warn_;
+
+        // If the last warning was issued more than 60 seconds ago or it is a
+        // first warning, we need to update the last warning timestamp and return
+        // true to indicate that new warning should be issued.
+        if (last_clock_skew_warn_.is_not_a_date_time() ||
+            (since_warn_duration.total_seconds() > MIN_TIME_SINCE_CLOCK_SKEW_WARN)) {
+            last_clock_skew_warn_ = now;
+            return (true);
+        }
+    }
+
+    // The warning should not be issued.
+    return (false);
+}
+
+bool
+CommunicationState::clockSkewShouldTerminate() const {
+    // Issue a warning if the clock skew is greater than 60s.
+    return (isClockSkewGreater(TERM_CLOCK_SKEW));
+}
+
+bool
+CommunicationState::isClockSkewGreater(const long seconds) const {
+    return ((clock_skew_.total_seconds() > seconds) ||
+            (clock_skew_.total_seconds() < -seconds));
+}
+
+void
+CommunicationState::setPartnerTime(const std::string& time_text) {
+    HttpDateTime partner_time = HttpDateTime().fromRfc1123(time_text);
+    HttpDateTime current_time = HttpDateTime();
+
+    clock_skew_ = partner_time.getPtime() - current_time.getPtime();
+}
+
+std::string
+CommunicationState::logFormatClockSkew() const {
+    std::ostringstream s;
+
+    // If negative clock skew, the partner's time is behind our time.
+    if (clock_skew_.is_negative()) {
+        s << clock_skew_.invert_sign().total_seconds() << "s behind";
+
+    } else {
+        // Partner's time is ahead of ours.
+        s << clock_skew_.total_seconds() << "s ahead";
+    }
+
+    return (s.str());
+}
+
+CommunicationState4::CommunicationState4(const IOServicePtr& io_service,
+                                         const HAConfigPtr& config)
+    : CommunicationState(io_service, config), unacked_clients_() {
+}
+
+void
+CommunicationState4::analyzeMessage(const boost::shared_ptr<dhcp::Pkt>& message) {
+    // The DHCP message must successfully cast to a Pkt4 object.
+    Pkt4Ptr msg = boost::dynamic_pointer_cast<Pkt4>(message);
+    if (!msg) {
+        isc_throw(BadValue, "DHCP message to be analyzed is not a DHCPv4 message");
+    }
+
+    // Check value of the "secs" field by comparing it with the configured
+    // threshold.
+    uint16_t secs = msg->getSecs();
+
+    // It was observed that some Windows clients may send swapped bytes in the
+    // "secs" field. When the second byte is 0 and the first byte is non-zero
+    // we consider bytes to be swapped and so we correct them.
+    if ((secs > 255) && ((secs & 0xFF) == 0)) {
+        secs = ((secs >> 8) | (secs << 8));
+    }
+
+    // Check the value of the "secs" field. If it is below the threshold there
+    // is nothing to do. The "secs" field holds a value in seconds, hence we
+    // have to multiple by 1000 to get a value in milliseconds.
+    if (secs * 1000 <= config_->getMaxAckDelay()) {
+        return;
+    }
+
+    // The "secs" value is above the threshold so we should count it as unacked
+    // request, but we will first have to check if there is such request already
+    // recorded.
+    auto existing_requests = unacked_clients_.equal_range(msg->getHWAddr()->hwaddr_);
+
+    // Client identifier will be stored together with the hardware address. It
+    // may remain empty if the client hasn't specified it.
+    std::vector<uint8_t> client_id;
+    OptionPtr opt_client_id = msg->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
+    if (opt_client_id) {
+        client_id = opt_client_id->getData();
+    }
+
+    // Iterate over the requests we found so far and see if we have a match with
+    // the client identifier (this includes empty client identifiers).
+    for (auto r = existing_requests.first; r != existing_requests.second; ++r) {
+        if (r->second == client_id) {
+            // There is a match so we have already recorded this client as
+            // unacked.
+            return;
+        }
+    }
+
+    // New unacked client detected, so record the required information.
+    unacked_clients_.insert(std::make_pair(msg->getHWAddr()->hwaddr_, client_id));
+}
+
+bool
+CommunicationState4::failureDetected() const {
+    return ((config_->getMaxUnackedClients() == 0) ||
+            (unacked_clients_.size() > config_->getMaxUnackedClients()));
+}
+
+void
+CommunicationState4::clearUnackedClients() {
+    unacked_clients_.clear();
+}
+
+CommunicationState6::CommunicationState6(const IOServicePtr& io_service,
+                                         const HAConfigPtr& config)
+    : CommunicationState(io_service, config), unacked_clients_() {
+}
+
+void
+CommunicationState6::analyzeMessage(const boost::shared_ptr<dhcp::Pkt>& message) {
+    // The DHCP message must successfully cast to a Pkt6 object.
+    Pkt6Ptr msg = boost::dynamic_pointer_cast<Pkt6>(message);
+    if (!msg) {
+        isc_throw(BadValue, "DHCP message to be analyzed is not a DHCPv6 message");
+    }
+
+    // Check the value of the "elapsed time" option. If it is below the threshold
+    // there is nothing to do. The "elapsed time" option holds the time in
+    // 1/100 of second, hence we have to multiply by 10 to get a value in milliseconds.
+    OptionUint16Ptr elapsed_time = boost::dynamic_pointer_cast<
+        OptionUint16>(msg->getOption(D6O_ELAPSED_TIME));
+    if (!elapsed_time || elapsed_time->getValue() * 10 <= config_->getMaxAckDelay()) {
+        return;
+    }
+
+    // Get the DUID of the client to see if it hasn't been recorded already.
+    OptionPtr duid = msg->getOption(D6O_CLIENTID);
+    if (duid && unacked_clients_.count(duid->getData()) == 0) {
+        // New unacked client detected, so record the required information.
+        unacked_clients_.insert(duid->getData());
+    }
+}
+
+bool
+CommunicationState6::failureDetected() const {
+    return ((config_->getMaxUnackedClients() == 0) ||
+            (unacked_clients_.size() > config_->getMaxUnackedClients()));
+}
+
+void
+CommunicationState6::clearUnackedClients() {
+    unacked_clients_.clear();
+}
+
+} // end of namespace isc::ha
+} // end of namespace isc
diff --git a/src/hooks/dhcp/high_availability/communication_state.h b/src/hooks/dhcp/high_availability/communication_state.h
new file mode 100644 (file)
index 0000000..b178f12
--- /dev/null
@@ -0,0 +1,436 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#ifndef HA_COMMUNICATION_STATE_H
+#define HA_COMMUNICATION_STATE_H
+
+#include <ha_config.h>
+#include <ha_service_states.h>
+#include <asiolink/interval_timer.h>
+#include <asiolink/io_service.h>
+#include <dhcp/pkt.h>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <boost/function.hpp>
+#include <boost/shared_ptr.hpp>
+#include <map>
+#include <set>
+#include <string>
+
+namespace isc {
+namespace ha {
+
+/// @brief Holds communication state between the two HA peers.
+///
+/// The HA service constantly monitors the state of the connection between
+/// the two peers. If the connection is lost it is an indicator that
+/// the partner server may be down and failover actions should be triggered.
+///
+/// Any command successfully sent over the control channel is an indicator
+/// that the connection is healthy. The most common command sent over the
+/// control channel is a lease update. If the DHCP traffic is heavy, the
+/// number of generated lease updates is sufficient to determine whether
+/// the connection is healthy or not. There is no need to send heartbeat
+/// commands in this case. However, if the DHCP traffic is low there is
+/// a need to send heartbeat commands to the partner at the specified
+/// rate to keep up-to-date information about the state of the connection.
+///
+/// This class uses an interval timer to run heartbeat commands over the
+/// control channel. The implementation of the heartbeat is external to
+/// this class and is provided via @c CommunicationState::startHeartbeat
+/// method. This implementation is required to run the @c poke method
+/// in case of receiving a successful response to the heartbeat command.
+/// It must also run @c poke when the lease update is successful.
+///
+/// The @c poke method sets the "last poke time" to current time, thus
+/// indicating that the connection is healty. The @c getDurationInMillisecs
+/// method is used to check for how long the server hasn't been able
+/// to communicate with the partner. This duration is simply a time
+/// elapsed since last successful poke time. If this duration becomes
+/// greater than the configured threshold, the server assumes that the
+/// communication with the partner is interrupted.
+///
+/// The derivations of this class provide DHCPv4 and DHCPv6 specific
+/// mechanisms for detecting server failures based on the analysis of
+/// the received DHCP messages, i.e. how long the clients have been
+/// trying to communicate with the partner and message types they sent.
+/// In particular, the increased number of Rebind messages may indicate
+/// issues with the DHCP server.
+///
+/// This class is also used to monitor the clock skew between the active
+/// servers. Maintaining a reasonably low clock skew is essential for the
+/// HA service to function properly. This class calculates the clock
+/// skew by comparing local time of the server with the time returned by
+/// the partner in response to a heartbeat command. If this value exceeds
+/// the certain thresholds, the CommunicationState::clockSkewShouldWarn
+/// and the @c CommuicationState::clockSkewShouldTerminate indicate
+/// whether the HA service should continue to operate normally, should
+/// start issuing a warning about high clock skew or simply enter the
+/// "terminated" state refusing to further operate until the clocks
+/// are synchronized. This requires administrative intervention and the
+/// restart of the HA service.
+class CommunicationState {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param io_service pointer to the common IO service instance.
+    /// @param config pointer to the HA configuration.
+    CommunicationState(const asiolink::IOServicePtr& io_service,
+                       const HAConfigPtr& config);
+
+    /// @brief Destructor.
+    ///
+    /// Stops scheduled heartbeat.
+    virtual ~CommunicationState();
+
+    /// @brief Returns last known state of the partner.
+    ///
+    /// @return Partner's state if it is known, or a negative value otherwise.
+    int getPartnerState() const {
+        return (partner_state_);
+    }
+
+    /// @brief Sets partner state.
+    ///
+    /// @param state new partner's state in a textual form. Supported values are
+    /// those returned in response to a ha-heartbeat command.
+    /// @throw BadValue if unsupported state value was provided.
+    void setPartnerState(const std::string& state);
+
+    /// @brief Starts recurring heartbeat (public interface).
+    ///
+    /// @param interval heartbeat interval in milliseconds.
+    /// @param heartbeat_impl pointer to the heartbeat implementation
+    /// function.
+    void startHeartbeat(const long interval,
+                        const boost::function<void()>& heartbeat_impl);
+
+protected:
+
+    /// @brief Starts recurring heartbeat.
+    ///
+    /// @param interval heartbeat interval in milliseconds.
+    /// @param heartbeat_impl pointer to the heartbeat implementation
+    /// function.
+    void startHeartbeatInternal(const long interval = 0,
+                                const boost::function<void()>& heartbeat_impl = 0);
+
+public:
+
+    /// @brief Stops recurring heartbeat.
+    void stopHeartbeat();
+
+    /// @brief Checks if recurring heartbeat is running.
+    ///
+    /// @return true if heartbeat is running, false otherwise.
+    bool isHeartbeatRunning() const {
+        return (static_cast<bool>(timer_));
+    }
+
+    /// @brief Pokes the communication state.
+    ///
+    /// Sets the last poke time to current time. If the heartbeat timer
+    /// has been scheduled, it is reset (starts over measuring the time
+    /// to the next heartbeat).
+    void poke();
+
+    /// @brief Returns duration between the poke time and current time.
+    ///
+    /// @return Duration between the poke time and current time.
+    int64_t getDurationInMillisecs() const;
+
+    /// @brief Checks if communication with the partner is interrupted.
+    ///
+    /// This method checks if the communication with the partner appears
+    /// to be interrupted. This is the case when the time since last
+    /// successful communication is longer than the confgured
+    /// max-response-delay value.
+    ///
+    /// @return true if communication is interrupted, false otherwise.
+    bool isCommunicationInterrupted() const;
+
+    /// @brief Checks if the DHCP message appears to be unanswered.
+    ///
+    /// This method is used to provide the communication state with a
+    /// received DHCP message directed to the HA partner, to detect
+    /// if the partner fails to answer DHCP messages directed to it.
+    /// The DHCPv4 and DHCPv6 specific derivations implement this
+    /// functionality.
+    ///
+    /// This check is orthogonal to the heartbeat mechanism and is
+    /// usually triggered after several consecutive heartbeats fail
+    /// to be responded.
+    ///
+    /// The general approach to server failure detection is based on the
+    /// analysis of the "secs" field value (DHCPv4) and "elapsed time"
+    /// option value (DHCPv6). They indicate for how long the client
+    /// has been trying to complete the DHCP transaction. If these
+    /// values exceed a configured threshold, the client is considered
+    /// to fail to communicate with the server. This fact is recorded
+    /// by this object. If the number of distinct clients failing to
+    /// communicate with the partner exceeds a configured maximum
+    /// value, this server considers the partner to be offline. In this
+    /// case, this server will most likely start serving clients
+    /// which would normally be served by the partner.
+    ///
+    /// All information gathered by this method is cleared when the
+    /// @c poke method is invoked.
+    ///
+    /// @param message DHCP message to be analyzed. This must be the
+    /// message which belongs to the partner, i.e. the caller must
+    /// filter out messages belonging to the partner prior to calling
+    /// this method.
+    virtual void analyzeMessage(const boost::shared_ptr<dhcp::Pkt>& message) = 0;
+
+    /// @brief Checks if the partner failure has been detected based
+    /// on the DHCP traffic analysis.
+    ///
+    /// In the special case when max-unacked-clients is set to 0 this
+    /// method always returns true. Note that max-unacked-clients
+    /// set to 0 means that failure detection is not really performed.
+    /// Returning true in that case simplifies the code of the
+    /// @c HAService which doesn't need to check if the failure detection
+    /// is enabled or not. It simply calls this method in the
+    /// 'communications interrupted' situtation to check if the
+    /// server should be transitioned to the 'partner-down' state.
+    ///
+    /// @return true if the partner failure has been detected, false
+    /// otherwise.
+    virtual bool failureDetected() const = 0;
+
+protected:
+
+    /// @brief Removes information about clients which the partner server
+    /// failed to respond to.
+    ///
+    /// This information is cleared by the @c CommunicationState::poke.
+    /// The derivations of this class must provide DHCPv4 and DHCPv6 specific
+    /// implementations of this method. The @c poke method is called to
+    /// indicate that the connection has been successfully (re)established.
+    /// Therefore the clients counters are reset and the failure detection
+    /// procedure starts over.
+    ///
+    /// See @c CommunicationState::analyzeMessage for details.
+    virtual void clearUnackedClients() = 0;
+
+public:
+
+    /// @brief Indicates whether the HA service should issue a warning about
+    /// high clock skew between the active servers.
+    ///
+    /// The HA service monitors the clock skew between the active servers. The
+    /// clock skew is calculated from the local time and the time returned by
+    /// the partner in response to a heartbeat. When clock skew exceeds a certain
+    /// threshold the HA service starts issuing a warning message. This method
+    /// returns true if the HA service should issue this message.
+    ///
+    /// Currently, the warning threshold for the clock skew is hardcoded to
+    /// 30 seconds.  In the future it may become configurable.
+    ///
+    /// This method is called for each heartbeat. If we issue a warning for each
+    /// heartbeat it may flood logs with those messages. This method provides
+    /// a gating mechanism which prevents the HA service from logging the
+    /// warning more often than every 60 seconds. If the last warning was issued
+    /// less than 60 seconds ago this method will return false even if the clock
+    /// skew exceeds the 30 seconds threshold. The correction of the clock skew
+    /// will reset the gating counter.
+    ///
+    /// @return true if the warning message should be logged because of the clock
+    /// skew exceeding a warning thresdhold.
+    bool clockSkewShouldWarn();
+
+    /// @brief Indicates whether the HA service should enter "terminated"
+    /// state as a result of the clock skew exceeding maximum value.
+    ///
+    /// If the clocks on the active servers are not synchronized (perhaps as
+    /// a result of a warning message caused by @c clockSkewShouldWarn) and the
+    /// clocks further drift, the clock skew may exceed another threshold which
+    /// should cause the HA service to enter "terminated" state. In this state
+    /// the servers still respond to DHCP clients normally, but they will neither
+    /// send lease updates nor heartbeats. In this case, the administrator must
+    /// correct the problem (synchronize the clocks) and restart the service.
+    /// This method indicates whether the service should terminate or not.
+    ///
+    /// Currently, the terminal threshold for the clock skew is hardcoded to
+    /// 60 seconds.  In the future it may become configurable.
+    ///
+    /// @return true if the HA service should enter "terminated" state.
+    bool clockSkewShouldTerminate() const;
+
+protected:
+
+    /// @brief Checks if the clock skew is greater than the specified number
+    /// of seconds.
+    ///
+    /// @param seconds a positive value to compare the clock skew with.
+    /// @return true if the absolute clock skew is greater than the specified
+    /// number of seconds, false otherwise.
+    bool isClockSkewGreater(const long seconds) const;
+
+public:
+
+    /// @brief Provide partner's notion of time so the new clock skew can be
+    /// calculated.
+    ///
+    /// @param time_text Partner's time received in response to a heartbeat. The
+    /// time must be provided in the RFC 1123 format.
+    /// @throw isc::http::HttpTimeConversionError if the time format is invalid.
+    ///
+    /// @todo Consider some other time formats which include millisecond
+    /// precision.
+    void setPartnerTime(const std::string& time_text);
+
+    /// @brief Returns current clock skew value in the logger friendly format.
+    std::string logFormatClockSkew() const;
+
+protected:
+
+    /// @brief Pointer to the common IO service instance.
+    asiolink::IOServicePtr io_service_;
+
+    /// @brief High availability configuration.
+    HAConfigPtr config_;
+
+    /// @brief Interval timer triggering heartbeat commands.
+    asiolink::IntervalTimerPtr timer_;
+
+    /// @brief Interval specified for the heartbeat.
+    long interval_;
+
+    /// @brief Last poke time.
+    boost::posix_time::ptime poke_time_;
+
+    /// @brief Pointer to the function providing heartbeat implementation.
+    boost::function<void()> heartbeat_impl_;
+
+    /// @brief Last known state of the partner server.
+    ///
+    /// Negative value means that the partner's state is unknown.
+    int partner_state_;
+
+    /// @brief Clock skew between the active servers.
+    boost::posix_time::time_duration clock_skew_;
+
+    /// @brief Holds a time when last warning about too high clock skew
+    /// was issued.
+    boost::posix_time::ptime last_clock_skew_warn_;
+};
+
+/// @brief Type of the pointer to the @c CommunicationState object.
+typedef boost::shared_ptr<CommunicationState> CommunicationStatePtr;
+
+
+/// @brief Holds communication state between DHCPv4 servers.
+///
+/// This class implements DHCPv4 failure detection by monitoring the
+/// value of the "secs" field in received DHCPv4 messages as described
+/// in @c CommunicationState::analyzeMessage.
+class CommunicationState4 : public CommunicationState {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param io_service pointer to the common IO service instance.
+    /// @param config pointer to the HA configuration.
+    CommunicationState4(const asiolink::IOServicePtr& io_service,
+                        const HAConfigPtr& config);
+
+    /// @brief Checks if the DHCPv4 message appears to be unanswered.
+    ///
+    /// This method uses "secs" field value for detecting client
+    /// communication failures as described in the
+    /// @c CommunicationState::analyzeMessage. Some misbehaving Windows
+    /// clients were reported to swap "secs" field bytes. In this case
+    /// the first byte is set to non-zero byte and the second byte is
+    /// set to 0. This method handles such cases and corrects bytes
+    /// order before comparing against the threshold.
+    ///
+    /// @param message DHCPv4 message to be analyzed. This must be the
+    /// message which belongs to the partner, i.e. the caller must
+    /// filter out messages belonging to the partner prior to calling
+    /// this method.
+    virtual void analyzeMessage(const boost::shared_ptr<dhcp::Pkt>& message);
+
+    /// @brief Checks if the partner failure has been detected based
+    /// on the DHCP traffic analysis.
+    ///
+    /// @return true if the partner failure has been detected, false
+    /// otherwise.
+    virtual bool failureDetected() const;
+
+protected:
+
+    /// @brief Removes information about clients which the partner server
+    /// failed to respond to.
+    ///
+    /// See @c CommunicationState::analyzeMessage for details.
+    virtual void clearUnackedClients();
+
+    /// @brief Holds information about the clients which the partner server
+    /// failed to respond to.
+    ///
+    /// The key of the multimap holds hardware addresses of the clients.
+    /// The value of the multimap holds client identifiers of the
+    /// clients. The client identifiers may be empty.
+    std::multimap<std::vector<uint8_t>, std::vector<uint8_t> > unacked_clients_;
+};
+
+/// @brief Pointer to the @c CommunicationState4 object.
+typedef boost::shared_ptr<CommunicationState4> CommunicationState4Ptr;
+
+/// @brief Holds communication state between DHCPv6 servers.
+///
+/// This class implements DHCPv6 failure detection by monitoring the
+/// value of the "Elapsed Time" option in received DHCPv6 messages as described
+/// in @c CommunicationState::analyzeMessage.
+class CommunicationState6 : public CommunicationState {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param io_service pointer to the common IO service instance.
+    /// @param config pointer to the HA configuration.
+    CommunicationState6(const asiolink::IOServicePtr& io_service,
+                        const HAConfigPtr& config);
+
+    /// @brief Checks if the DHCPv6 message appears to be unanswered.
+    ///
+    /// See @c CommunicationState::analyzeMessage for details.
+    ///
+    /// @param message DHCPv6 message to be analyzed. This must be the
+    /// message which belongs to the partner, i.e. the caller must
+    /// filter out messages belonging to the partner prior to calling
+    /// this method.
+    virtual void analyzeMessage(const boost::shared_ptr<dhcp::Pkt>& message);
+
+    /// @brief Checks if the partner failure has been detected based
+    /// on the DHCP traffic analysis.
+    ///
+    /// @return true if the partner failure has been detected, false
+    /// otherwise.
+    virtual bool failureDetected() const;
+
+protected:
+
+    /// @brief Removes information about clients which the partner server
+    /// failed to respond to.
+    ///
+    /// See @c CommunicationState::analyzeMessage for details.
+    virtual void clearUnackedClients();
+
+    /// @brief Holds information about the clients which the partner server
+    /// failed to respond to.
+    ///
+    /// The value of the set holds DUIDs of the clients.
+    std::set<std::vector<uint8_t> > unacked_clients_;
+};
+
+/// @brief Pointer to the @c CommunicationState6 object.
+typedef boost::shared_ptr<CommunicationState6> CommunicationState6Ptr;
+
+} // end of namespace isc::ha
+} // end of namespace isc
+
+#endif
diff --git a/src/hooks/dhcp/high_availability/ha.dox b/src/hooks/dhcp/high_availability/ha.dox
new file mode 100644 (file)
index 0000000..81eae9a
--- /dev/null
@@ -0,0 +1,417 @@
+// Copyright (C) 2017-2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License Agreement.
+// See COPYING file in the premium/ directory.
+
+/**
+
+@mainpage Kea High Availability Hooks Library
+
+Welcome to Kea High Availability Hooks Library. This documentation is
+addressed at developers who are interested in internal operation of the
+library. This file provides information needed to understand and perhaps
+extend this library.
+
+This documentation is stand-alone: you should have read and
+understood <a href="http://git.kea.isc.org/~tester/kea/doxygen/">Kea
+Developer's Guide</a> and in particular its section about hooks: <a
+href="http://git.kea.isc.org/~tester/kea/doxygen/df/d46/hooksdgDevelopersGuide.html">
+Hooks Developer's Guide</a>.
+
+@section haOverview Overview
+
+The High Availability (HA) hooks library is inteded for DHCP deployments
+in which there is a need to sustain the DHCP service in the event if one
+of the servers becomes unavailable as a result of a crash, power outage or
+other unexpected sitution. The other server belonging to this setup should
+be able to handle the entire DHCP traffic directed to the system, including
+the traffic that would be normally handled by the server which became
+unavailable.
+
+Many of the concepts behind the HA hooks library are derived from the
+DHCP Failover protocol, however this solution has different architecture,
+uses different state machine and different message formats for communication
+between the participating servers. This solution is not a DHCP Failover
+implementation and, therefore, this documentation purposely avoids using
+the word "Failover" in the context of this library.
+
+The HA feature design can be found at
+<a href="https://kea.isc.org/wiki/HADesign">Kea HA Design page</a>.
+
+@section haWhyHookLibrary Why Hook Library?
+
+High Availability is a very important requirement for various DHCP
+deployments. It is a valid question why such a generic feature is
+placed in a hook library rather implemented as an integral part of the
+Kea DHCP servers. Besides business reasons to make the HA a premium Kea
+feature, there are also some technical reasons. If the HA is implemented
+in the loadable library, users who don't use HA or who don't want to
+use this particular solution for HA will simply not load this library.
+The server code without the HA implementation is lighter, easier
+to understand and debug. High Availability is a pretty complex feature
+and will certainly keep growing both in size and complexity. Keeping
+it in a separate code base makes it easier to maintain and use. Also,
+the HA hooks library requires Kea lease_cmds hook library to be loaded
+on the participating servers. It would clearly be a bad design to
+introduce the feature relying on the presence the loadable (lease_cmds)
+module in the main Kea code.
+
+@section haNotableDifferences Notable Differences to ISC DHCP
+
+It is worth to briefly explain what are the major differences between Kea HA
+implementation and the failover implemented in ISC DHCP.
+
+There are two protocols that IETF attempted to standarize:
+<a href="https://datatracker.ietf.org/doc/html/draft-ietf-dhc-failover">
+DHCPv4 Failover draft</a>, which was an Internet Draft status that had
+expired Sept. 2003. The other one is <a href="https://tools.ietf.org/html/rfc8156">
+RFC8156: DHCPv6 Failover</a>, which was published as Proposed Standard.
+ISC DHCP implemented the former, but not the latter. As such, ISC DHCP
+is able to provide failover for DHCPv4 only, not DHCPv6.
+
+The second major difference is that both IETF failover protocols are based on
+MCLT (or Maximum Client Lead Time), sometimes referenced to as lazy
+updates. This mechanism lets a server respond immediately, which improves
+latency, but it does so at the cost of greatly increased complexity. The lease
+is assigned with a very short lifetime, then an update is sent to the other
+server with a lifetime greater than the client requested. Once the other server
+confirms the lease, the client's renewal is being updated with a longer
+lifetime.  This approach generates more traffic and causes lease lifetimes to
+fluctuate greatly, despite an administrator setting it to a specific value. Kea
+HA does not implement this complexity. It is much simpler and easier to use and
+understand its operation, although the price to pay for this relative simplicity
+is a longer response time and somewhat decreased performance.
+
+Third difference is that in ISC DHCP the failover relationship is strictly
+a pair (i.e. two) of servers. On the other hand Kea HA is able to define additional
+backup servers. While they're not technically participating in the HA
+relationship, their databases are kept up to date and can be used are replacements
+that are almost ready to take over the traffic. However, replacing primary
+or secondary server with a backup requires manual administrator's intervention.
+
+The fourth difference is that Kea HA does not support pool rebalancing yet.
+When running in load balancing mode, Kea uses hashing mechanism to segregate
+clients into one of two pools. It is unlikely, but possible that a network
+would be visited by clients that are predominantly assigned to one server.
+As a result, this server could ran out of addresses, while its underutilized
+partner could still have many addresses available. This unfortunate, but
+unlikely limitation will be removed in the future Kea releases.
+
+@section haAyncCommunication Asynchronous Communication with Boost Asio
+
+One of the major technical problems with High Availability is that the
+participating servers must constantly communicate with each other.
+When one of the servers allocates a lease it must notify its peer about
+this allocation and provide it with a full information about the
+allocated lease. The server which has allocated the lease must not
+respond to the client until its partner confirms that it has saved
+the lease in its database. This guarantees that, at any given time,
+both servers hold the most current lease information and any of the
+servers can take responsibility for managing existing leases if the
+partner server becomes unavailable. This is similar to the requirement
+on a single DHCP server which must store the lease information on
+the persistent storage before responding to the client. Failing to do
+so may cause the lease information to get lost if the server crashes
+before writing it to the lease file.
+
+The requirement for the partner to store the lease in its lease database
+and confirming this fact to the server allocating the lease results in
+increased latency of the DHCP responses to the clients. In order to
+minimize the latency the idea of "parking" DHCP packets has been introduced.
+This is a solution for pseudo parallel processing of multiple DHCP packets
+and to prevent blocking wait during the communication with the other server.
+When the HA hooks library needs to send a lease update to the partner,
+the client's packet associated with this lease is "parked", waiting for
+the communication with the partner to complete. Meanwhile, other incoming
+DHCP packets are processed (and also parked if necessary). The client
+which sent the DHCP packet still has to wait for the communication with
+the partner to complete, but it doesn't have to wait for the server to
+receive its packet (and start processing it) while previous DHCP
+transaction is still in progress.
+
+This solution requires that the communication between the servers is
+asynchronous and the most obvious framework for this was Boost ASIO,
+as it is already used in many different areas of the code.
+
+The DHCP servers are processing incoming packets synchronously (in a
+loop), but each loop pass contains a call to:
+
+@code
+getIOService()->poll();
+@endcode
+
+which executes callbacks for completed asynchronous operations, such as
+timers, asynchronous sends and receives. The instance of the IOService
+is owned by the DHCP servers, but hooks libraries must have access to it
+and must use this instance to schedule asynchronous tasks. This is why
+the new hook points "dhcp4_srv_configured" and "dhcp6_srv_configured"
+have been introduced. These hook points are used by the DHCPv4 and the
+DHCPv6 servers respectively, to pass the instance of the IOService
+(via "io_context" argument) to the hooks libraries which require to
+schedule asynchronous tasks.
+
+It is also worth to note that the blocking reception of the DHCP packets
+may cause up to 1 second delays in the asynchronous operations. This is
+due to the structure of the main server loop:
+
+@code
+bool
+Dhcpv4Srv::run() {
+    while (!shutdown_) {
+        try {
+            run_one();
+            getIOService()->poll();
+        } catch (const std::exception& e) {
+            // General catch-all exception that are not caught by more specific
+            // catches. This one is for exceptions derived from std::exception.
+            LOG_ERROR(packet4_logger, DHCP4_PACKET_PROCESS_STD_EXCEPTION)
+                .arg(e.what());
+        } catch (...) {
+            // General catch-all exception that are not caught by more specific
+            // catches. This one is for other exceptions, not derived from
+            // std::exception.
+            LOG_ERROR(packet4_logger, DHCP4_PACKET_PROCESS_EXCEPTION);
+        }
+    }
+
+    return (true);
+}
+@endcode
+
+The @c run_one() call includes a @c select() invocation with a timeout of
+1 second. The @c poll() is not invoked for at most 1 second while the server
+is performing this blocking @c select(). Future Kea releases should mitigate
+this problem by introducing some mechanisms for concurrent reception and
+processing of the DHCP packets.
+
+
+@section haClientClassification Client Classification in Load Balancing
+
+One of the top requirements for the HA was to support load balancing between
+two participating servers. Even though, current implementation supports
+only 50/50 split of packets between two servers, the implementation can
+easily be extended to support different splits.
+
+Another supported mode of operation is the "hot-standby" mode in which
+one of the servers handles the entire traffic and the other server is
+simply receiving lease updates from it. In case of the failure of the
+first server, the standby server can automatically switch to handle the
+DHCP traffic directed to the system.
+
+The "load-balancing" mode is more complex in that it requires isolation
+of address/prefix pools from which the respective servers are allocating
+leases for the clients. If the two servers were sharing address pools
+they would frequently run into the conflict whereby both of them would
+allocate the same address to different clients. This is not a problem in
+the "hot-standby" mode because there is only one server allocating leases
+at the given time.
+
+The most challenging part in case of load balancing is the configuration
+of the address pools on respective servers. At the time when the HA design
+was created, there was no requirement on the HA hooks library to be able
+to rebalance the pools, e.g. in case one of the pools is nearly exhausted
+and the other pool include many available addresses or prefixes. This
+requirement may come in the future, in which case the current approach
+to the configuration may be enhanced.
+
+The current approach uses existing client classification mechanism to
+statically split allocations accross multiple pools. Client classification
+was designed to serve as a generic framework to support various scenarios
+in which clients need to be segregated and associated with selected
+pools, subnets and shared networks. The load balancing in HA hooks
+library is nothing else but another use case for client classification.
+Should new requirements be created for the HA hooks library in the
+future (e.g. rebalancing), the client classification will need to be
+extended to adopt those requirements.
+
+In fact, client classification was already extended for the Kea 1.4.0
+release to allow for selecting a specific pool based on combinations
+of classes, rather than a single class associated with the server
+by the HA load balancing algorithm. The examples of the pools split
+between different device types (e.g. laptops and telephones) and
+between load balancing servers (e.g. "server1" and "server2") can
+be found in the Kea Administrator's Manual.
+
+@section haCodeStructure HA Hooks Library Code Structure
+
+@subsection haService HA Service Class
+
+The @c isc::ha::HAService class is a heart of the HA system. It implements the
+HA state machine. It is derived from the @c isc::util::StateModel
+class. The states are documented both in the Kea Adminiatrator's
+Manual and the HA design. The declarations of the states can be
+found in the @c ha_service_states.h header file because they are
+used by multiple C++ classes.
+
+Besides running the state machine transitions, the @c HAService
+class serves the following purposes:
+
+- Assigns class to the received DHCP packet appropriate for the server
+  selected to process the DHCP packet as a result of load balancing.
+- Measures the clock skew between the active servers. If the clock skew
+  is too high, it can either log an error or stop the HA function.
+- Sends lease updates to the partner and receives responses.
+- Sends heartbeat command to the partner to verify partner's state
+  and its notion of time (for clock skew).
+- Controls whether the DHCP server should respond to the queries
+  from clients or not.
+- Synchronizes local lease database by fetching the leases from the
+  partner server.
+- Controls which packets the server responds to (HA scopes).
+
+As of Kea 1.4.0 release, there is only one instance of the @c HAService
+class created by the HA hooks library. In the future, multiple
+@c HAService instances may co-exist, each handling an independent HA
+relationship with another server. For example: a server could be
+configured to respond to devices in two subnets and establish a
+connection with two different servers for respective subnets. Lease
+updates pertaining to the first subnet would be sent via first
+connection and those pertaining to the second subnet would be sent
+via the second connection. As of Kea 1.4.0 release, there is exactly
+one relationship that the Kea server instance can participate in.
+
+@subsection haImplementation HA Implementation Class
+
+The @c isc::ha::HAImpl class implements callouts and command handlers supported
+by the HA hooks library. Its methods expect @c isc::hooks::CalloutHandle
+as arguments and are usually directly called by the callout functions
+such as @c pkt4_receive etc. This makes it more natural to unit test
+those implementations because the  tests can invoke methods of the @c HAImpl
+class, rather than the "extern" functions.
+
+Internally, the @c HAImpl class methods call methods of the @c HAService
+class to perform certain actions, such as triggering lease updates,
+sending heatbeat to another server etc. However, the @c HAImpl still
+includes a fair amont of logic to retrive and validate the arguments
+provided within the @c isc::hooks::CalloutHandle.
+
+The @c isc::ha::HAImpl::buffer4Receive and @c isc::ha::HAImpl::buffer6Receive
+functions deserve some detailed explanation, because not only do they retrieve
+the arguments provided to the callouts but also perform parsing of the received
+DHCP queries.
+
+The DHCP query parsing is normally performed by the server. In most
+cases a hooks library would not have to parse the DHCP packets on
+its own. If the hooks library needs to access some information, e.g.
+DHCP options or BOOTP message fields, it is sufficient to
+implement the @c pkt4_receive or @c pkt6_receive callout, which is
+invoked after the server has parsed the packet. However, this
+approach would not work in case of the HA hooks library. This
+library assigns classes as a result of the load balancing to the
+incoming packets. This assignment must take place before the server
+evaluates classes specified in the configuration file, i.e.
+before the @c pkt4_receive and @c pkt6_receive hook point. This
+implies that the HA specific classification must be performed within
+the @c buffer4_receive or @c buffer6_receive callouts. These callouts
+must parse (unpack) the received buffers to have an access into the
+data used by the load balancing algorithm, such as: MAC address, client
+identifier or DUID.
+
+@subsection haQueryFilter Query Filter Class
+
+The @c isc::ha::QueryFilter class is used to control which DHCP queries are
+to be processed by respective servers. It implements the load
+balancing algorithm which is triggered by cooperating servers against
+each incoming packet and results in assigning the packet to one of the
+served "scopes". Scopes are associated with the servers and are named
+after the servers. In the load balancing case there are two scopes,
+e.g. "server1" and "server2". The Load balancing algorithm selects
+one of the scopes for the packet. During the normal operation,
+each server handles its own scope. In the "partner-down" state, the
+surviving server would handle both scopes. The selection of the
+scopes to be served by the server instance is usually made
+automatically as a result of transitioning to some new state within
+the @c HAService class. However, the scopes assignment can also be
+made via control channel as a result of an administrative action.
+
+@subsection haCommunicationState Communication State Class
+
+The @c CommunicationState class is used by the @c HAService to
+control all aspects of the communication between the active servers,
+i.e.:
+
+- Scheduling periodic heartbeat commands using Boost ASIO timers.
+- Holding the state of the partner returned in response to the
+  heartbeat command.
+- Recording when the last successful heartbeat has been sent, i.e.
+  how long the partner server has been unresponsive.
+- Analyzing DHCP queries to detect whether the partner server is
+  not responsive by checking whether the values in the 'secs' field
+  or Elapsed Time option are too high.
+- Monitoring the clocks skew between the active servers, which is
+  calculated by substracting the current time (on the local
+  server) from the time returned by the partner in response to the
+  heartbeat comand.
+
+The large part of this class is common for the DHCPv4 and DHCPv6 servers.
+However, there are differences in how the DHCPv4 and the DHCPv6 messages
+are analyzed to detect whether the partner server has stopped responding:
+
+- The DHCPv4 server uses 'secs' field, while the DHCPv6 server looks
+  into the DHCPv6 specific Elapsed Time option.
+- When the DHCPv4 server records a client information in case if the
+  DHCPv4 server fails to respond the client's query, it records both the
+  client identifier and the MAC address. The DHCPv6 server uses the
+  DUID to record the client.
+
+Those differences led to creation of DHCPv4 and DHCPv6 specific
+derivations of the @c CommunicationState class, which differently
+deal with analysis of the queries.
+
+The clock skew is checked by the @c QueryFilter class every time
+it is updated as a result of receiving a response to the heartbeat.
+If the clock skew is in the range of 30 to 60 seconds, the
+@c clockSkewShouldWarn returns true to indicate to the @c HAService
+that a warning should be logged. In order to prevent too frequent
+warnings (especially when heartbeats are sent frequently), this
+method implements a simple gating algorithm, which would not return
+true (trigger the warning) more often than every 60 seconds.
+
+The @c isc::ha::CommunicationState::clockSkewShouldTerminate informs whether
+the clock skew has exceeded 60 seconds, in which case the
+@c HAService class would transition to the "terminated" state.
+
+@subsection haCommandCreator Command Creator Class
+
+The @c CommandCreator is a collection of static methods which
+create commands issued between the HA-enabled DHCP servers. These
+JSON commands are sent over the @c isc::http::HttpClient from the
+@c HAService class.
+
+@section haShortcomings Future HA Hooks Library Improvement Ideas
+
+The HA hooks library was first released with Kea 1.4.0. There are
+numerous enhancements to this library considered for the future releases.
+Some of them are briefly described in this section.
+
+@subsection haStateMachineControl Controlling State Machine
+
+As of Kea 1.4.0, there are no control commands allowing for setting or
+influencing the transitions between states. In particular, there is no
+way to pause the HA state machine on the selected state to perform
+some administrative actions before transitioning to the normal
+operation state.
+
+@subsection haNameUpdates DNS Updates are not Coordinated
+
+When one of the servers allocates the lease this server is responsible
+or sending a DNS update if configured to send such updates. The partner
+server receives the lease update (including the inserted hostname) so
+it knows that the hostname was stored in the DNS. When this lease
+subsequently expires, the hostname must be removed from the DNS. The
+HA hooks library, however, has no means to record which server has
+allocated this lease in the lease database. If recording such information
+had been possible, the same server which allocated the lease would have
+sent the removal name change request (NCR) to the D2. Because this
+information is unavailable, both servers will send the removal NCRs.
+One of those NCRs will succeed, another one will fail.
+
+Addressing this issue requires two enhancements:
+
+- Implementing "user context" for leases, which could be used for storing
+  custom type of information, e.g. server identifier, along with the leases.
+- Implementing callouts for the "lease4_expire" and "lease6_expire" hook
+  points via which the server removing the lease from the database could
+  notify the partner about such removal.
+
+*/
diff --git a/src/hooks/dhcp/high_availability/ha_callouts.cc b/src/hooks/dhcp/high_availability/ha_callouts.cc
new file mode 100644 (file)
index 0000000..b6bcc16
--- /dev/null
@@ -0,0 +1,231 @@
+// Copyright (C) 2017-2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+
+// Functions accessed by the hooks framework use C linkage to avoid the name
+// mangling that accompanies use of the C++ compiler as well as to avoid
+// issues related to namespaces.
+
+#include <config.h>
+
+#include <ha_impl.h>
+#include <ha_log.h>
+#include <asiolink/io_service.h>
+#include <cc/command_interpreter.h>
+#include <dhcpsrv/network_state.h>
+#include <hooks/hooks.h>
+
+namespace isc {
+namespace ha {
+
+HAImplPtr impl;
+
+} // end of namespace isc::ha
+} // end of namespace isc
+
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::ha;
+using namespace isc::hooks;
+
+extern "C" {
+
+/// @brief dhcp4_srv_configured callout implementation.
+///
+/// @param handle callout handle.
+int dhcp4_srv_configured(CalloutHandle& handle) {
+    try {
+        isc::asiolink::IOServicePtr io_service;
+        handle.getArgument("io_context", io_service);
+        isc::dhcp::NetworkStatePtr network_state;
+        handle.getArgument("network_state", network_state);
+        impl->startService(io_service, network_state, HAServerType::DHCPv4);
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(ha_logger, HA_DHCP4_START_SERVICE_FAILED)
+            .arg(ex.what());
+        return (1);
+    }
+    return (0);
+}
+
+/// @brief buffer4_receive callout implementation.
+///
+/// @param handle callout handle.
+int buffer4_receive(CalloutHandle& handle) {
+    try {
+        impl->buffer4Receive(handle);
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(ha_logger, HA_BUFFER4_RECEIVE_FAILED)
+            .arg(ex.what());
+        return (1);
+    }
+
+    return (0);
+}
+
+/// @brief leases4_committed callout implementation.
+///
+/// @param handle callout handle.
+int leases4_committed(CalloutHandle& handle) {
+    try {
+        impl->leases4Committed(handle);
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(ha_logger, HA_LEASES4_COMMITTED_FAILED)
+            .arg(ex.what());
+        return (1);
+    }
+
+    return (0);
+}
+
+/// @brief dhcp6_srv_configured callout implementation.
+///
+/// @param handle callout handle.
+int dhcp6_srv_configured(CalloutHandle& handle) {
+    try {
+        isc::asiolink::IOServicePtr io_service;
+        handle.getArgument("io_context", io_service);
+        isc::dhcp::NetworkStatePtr network_state;
+        handle.getArgument("network_state", network_state);
+        impl->startService(io_service, network_state, HAServerType::DHCPv6);
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(ha_logger, HA_DHCP6_START_SERVICE_FAILED)
+            .arg(ex.what());
+        return (1);
+    }
+    return (0);
+}
+
+/// @brief buffer6_receive callout implementation.
+///
+/// @param handle callout handle.
+int buffer6_receive(CalloutHandle& handle) {
+    try {
+        impl->buffer6Receive(handle);
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(ha_logger, HA_BUFFER6_RECEIVE_FAILED)
+            .arg(ex.what());
+        return (1);
+    }
+
+    return (0);
+}
+
+/// @brief leases6_committed callout implementation.
+///
+/// @param handle callout handle.
+int leases6_committed(CalloutHandle& handle) {
+    try {
+        impl->leases6Committed(handle);
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(ha_logger, HA_LEASES6_COMMITTED_FAILED)
+            .arg(ex.what());
+        return (1);
+    }
+
+    return (0);
+}
+
+/// @brief comand_processed callout implementation.
+///
+/// @param handle callout handle.
+int command_processed(CalloutHandle& handle) {
+    try {
+        impl->commandProcessed(handle);
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(ha_logger, HA_COMMAND_PROCESSED_FAILED)
+            .arg(ex.what());
+        return (1);
+    }
+
+    return (0);
+}
+
+/// @brief Heartbeat command handler implementation.
+int heartbeat_command(CalloutHandle& handle) {
+    try {
+        impl->heartbeatHandler(handle);
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(ha_logger, HA_HEARTBEAT_HANDLER_FAILED)
+            .arg(ex.what());
+        return (1);
+    }
+
+    return (0);
+}
+
+/// @brief ha-sync command handler implementation.
+int sync_command(CalloutHandle& handle) {
+    try {
+        impl->synchronizeHandler(handle);
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(ha_logger, HA_SYNC_HANDLER_FAILED)
+            .arg(ex.what());
+    }
+
+    return (0);
+}
+
+/// @brief ha-scopes command handler implementation.
+int scopes_command(CalloutHandle& handle) {
+    try {
+        impl->scopesHandler(handle);
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(ha_logger, HA_SCOPES_HANDLER_FAILED)
+            .arg(ex.what());
+    }
+
+    return (0);
+}
+
+/// @brief This function is called when the library is loaded.
+///
+/// @param handle library handle
+/// @return 0 when initialization is successful, 1 otherwise
+int load(LibraryHandle& handle) {
+    ConstElementPtr config = handle.getParameter("high-availability");
+    if (!config) {
+        LOG_ERROR(ha_logger, HA_MISSING_CONFIGURATION);
+        return (1);
+    }
+
+    try {
+        impl = boost::make_shared<HAImpl>();
+        impl->configure(config);
+
+        handle.registerCommandCallout("ha-heartbeat", heartbeat_command);
+        handle.registerCommandCallout("ha-sync", sync_command);
+        handle.registerCommandCallout("ha-scopes", scopes_command);
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(ha_logger, HA_CONFIGURATION_FAILED)
+            .arg(ex.what());
+        return (CONTROL_RESULT_ERROR);
+    }
+
+    LOG_INFO(ha_logger, HA_INIT_OK);
+    return (0);
+}
+
+/// @brief This function is called when the library is unloaded.
+///
+/// @return 0 if deregistration was successful, 1 otherwise
+int unload() {
+    LOG_INFO(ha_logger, HA_DEINIT_OK);
+    return (0);
+}
+
+
+} // end extern "C"
diff --git a/src/hooks/dhcp/high_availability/ha_config.cc b/src/hooks/dhcp/high_availability/ha_config.cc
new file mode 100644 (file)
index 0000000..78e26a4
--- /dev/null
@@ -0,0 +1,257 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <exceptions/exceptions.h>
+#include <util/strutil.h>
+#include <ha_config.h>
+#include <sstream>
+
+namespace isc {
+namespace ha {
+
+HAConfig::PeerConfig::PeerConfig()
+    : name_(), url_(""), role_(STANDBY), auto_failover_(false) {
+}
+
+void
+HAConfig::PeerConfig::setName(const std::string& name) {
+    // We want to make sure that someone didn't provide a name that consists of
+    // a single space, tab etc.
+    const std::string& s = util::str::trim(name);
+    if (s.empty()) {
+        isc_throw(BadValue, "peer name must not be empty");
+    }
+    name_ = s;
+}
+
+void
+HAConfig::PeerConfig::setRole(const std::string& role) {
+    role_ = stringToRole(role);
+}
+
+std::string
+HAConfig::PeerConfig::getLogLabel() const {
+    std::ostringstream label;
+    label << getName() << " (" << getUrl().toText() << ")";
+    return (label.str());
+}
+
+HAConfig::PeerConfig::Role
+HAConfig::PeerConfig::stringToRole(const std::string& role) {
+    if (role == "primary") {
+        return (HAConfig::PeerConfig::PRIMARY);
+
+    } else if (role == "secondary") {
+        return (HAConfig::PeerConfig::SECONDARY);
+
+    } else if (role == "standby") {
+        return (HAConfig::PeerConfig::STANDBY);
+
+    } else if (role == "backup") {
+        return (HAConfig::PeerConfig::BACKUP);
+
+    }
+
+    // Invalid value specified.
+    isc_throw(BadValue, "unsupported value '" << role << "' for role parameter");
+}
+
+std::string
+HAConfig::PeerConfig::roleToString(const HAConfig::PeerConfig::Role& role) {
+    switch (role) {
+    case HAConfig::PeerConfig::PRIMARY:
+        return ("primary");
+    case HAConfig::PeerConfig::SECONDARY:
+        return ("secondary");
+    case HAConfig::PeerConfig::STANDBY:
+        return ("standby");
+    case HAConfig::PeerConfig::BACKUP:
+        return ("backup");
+    default:
+        ;
+    }
+    return ("");
+}
+
+HAConfig::HAConfig()
+    : this_server_name_(), ha_mode_(HOT_STANDBY), send_lease_updates_(true),
+      sync_leases_(true), heartbeat_delay_(10), max_response_delay_(60),
+      max_ack_delay_(10), max_unacked_clients_(10), peers_() {
+}
+
+HAConfig::PeerConfigPtr
+HAConfig::selectNextPeerConfig(const std::string& name) {
+    // Check if there is a configuration for this server name alrady. We can't
+    // have two servers with the same name.
+    if (peers_.count(name) > 0) {
+        isc_throw(BadValue, "peer with name '" << name << "' already specified");
+    }
+
+    // Name appears to be unique, so let's add it.
+    PeerConfigPtr cfg(new PeerConfig());
+    cfg->setName(name);
+    peers_[name] = cfg;
+
+    // Return this to the caller so as the caller can set parsed configuration
+    // for this peer.
+    return (cfg);
+}
+
+void
+HAConfig::setThisServerName(const std::string& this_server_name) {
+    // Avoid names consisting of spaces, tabs etc.
+    std::string s = util::str::trim(this_server_name);
+    if (s.empty()) {
+        isc_throw(BadValue, "'this-server-name' value must not be empty");
+    }
+
+    this_server_name_ = s;
+}
+
+
+void
+HAConfig::setHAMode(const std::string& ha_mode) {
+    ha_mode_ = stringToHAMode(ha_mode);
+}
+
+HAConfig::HAMode
+HAConfig::stringToHAMode(const std::string& ha_mode) {
+    if (ha_mode == "load-balancing") {
+        return (LOAD_BALANCING);
+
+    } else if (ha_mode == "hot-standby") {
+        return (HOT_STANDBY);
+    }
+
+    isc_throw(BadValue, "unsupported value '" << ha_mode << "' for mode parameter");
+}
+
+std::string
+HAConfig::HAModeToString(const HAMode& ha_mode) {
+    switch (ha_mode) {
+    case LOAD_BALANCING:
+        return ("load-balancing");
+    case HOT_STANDBY:
+        return ("hot-standby");
+    default:
+        ;
+    }
+    return ("");
+}
+
+HAConfig::PeerConfigPtr
+HAConfig::getPeerConfig(const std::string& name) const {
+    auto peer = peers_.find(name);
+    if (peer == peers_.end()) {
+        isc_throw(InvalidOperation, "no configuration specified for server " << name);
+    }
+
+    return (peer->second);
+}
+
+HAConfig::PeerConfigPtr
+HAConfig::getFailoverPeerConfig() const {
+    PeerConfigMap servers = getOtherServersConfig();
+    for (auto peer = servers.begin(); peer != servers.end(); ++peer) {
+        if (peer->second->getRole() != HAConfig::PeerConfig::BACKUP) {
+            return (peer->second);
+        }
+    }
+
+    isc_throw(InvalidOperation, "no failover partner server found for this"
+              " server " << getThisServerName());
+}
+
+HAConfig::PeerConfigPtr
+HAConfig::getThisServerConfig() const {
+    return (getPeerConfig(getThisServerName()));
+}
+
+HAConfig::PeerConfigMap
+HAConfig::getOtherServersConfig() const {
+    PeerConfigMap copy = peers_;
+    copy.erase(getThisServerName());
+    return (copy);
+}
+
+void
+HAConfig::validate() const {
+    // Peers configurations must be provided.
+    if (peers_.count(getThisServerName()) == 0) {
+        isc_throw(HAConfigValidationError, "no peer configuration specified for the '"
+                  << getThisServerName() << "'");
+    }
+
+    // Gather all the roles and see how many occurrences of each role we get.
+    std::map<PeerConfig::Role, unsigned> peers_cnt;
+    for (auto p = peers_.begin(); p != peers_.end(); ++p) {
+        if (!p->second->getUrl().isValid()) {
+            isc_throw(HAConfigValidationError, "invalid URL: "
+                      << p->second->getUrl().getErrorMessage()
+                      << " for server " << p->second->getName());
+        }
+
+        ++peers_cnt[p->second->getRole()];
+    }
+
+    // Only one primary server allowed.
+    if (peers_cnt.count(PeerConfig::PRIMARY) && (peers_cnt[PeerConfig::PRIMARY] > 1)) {
+        isc_throw(HAConfigValidationError, "multiple primary servers specified");
+    }
+
+    // Only one secondary server allowed.
+    if (peers_cnt.count(PeerConfig::SECONDARY) && (peers_cnt[PeerConfig::SECONDARY] > 1)) {
+        isc_throw(HAConfigValidationError, "multiple secondary servers specified");
+    }
+
+    // Only one standby server allowed.
+    if (peers_cnt.count(PeerConfig::STANDBY) && (peers_cnt[PeerConfig::STANDBY] > 1)) {
+        isc_throw(HAConfigValidationError, "multiple standby servers specified");
+    }
+
+    if (ha_mode_ == LOAD_BALANCING) {
+        // Standby servers not allowed in load balancing configuration.
+        if (peers_cnt.count(PeerConfig::STANDBY) > 0) {
+            isc_throw(HAConfigValidationError, "standby servers not allowed in the load "
+                      "balancing configuration");
+        }
+
+        // Require one secondary server in the load balancing configuration.
+        if (peers_cnt.count(PeerConfig::SECONDARY) == 0) {
+            isc_throw(HAConfigValidationError, "secondary server required in the load"
+                      " balancing configuration");
+        }
+
+        // Require one primary server in the load balancing configuration.
+        if (peers_cnt.count(PeerConfig::PRIMARY) == 0) {
+            isc_throw(HAConfigValidationError, "primary server required in the load"
+                      " balancing configuration");
+        }
+
+    }
+
+    if (ha_mode_ == HOT_STANDBY) {
+        // Secondary servers not allowed in the hot standby configuration.
+        if (peers_cnt.count(PeerConfig::SECONDARY) > 0) {
+            isc_throw(HAConfigValidationError, "secondary servers not allowed in the hot"
+                      " standby configuration");
+        }
+
+        // Require one standby server in the hot standby configuration.
+        if (peers_cnt.count(PeerConfig::STANDBY) == 0) {
+            isc_throw(HAConfigValidationError, "standby server required in the hot"
+                      " standby configuration");
+        }
+
+        // Require one primary server in the hot standby configuration.
+        if (peers_cnt.count(PeerConfig::PRIMARY) == 0) {
+            isc_throw(HAConfigValidationError, "primary server required in the hot"
+                      " standby configuration");
+        }
+    }
+}
+
+} // end of namespace isc::ha
+} // end of namespace isc
diff --git a/src/hooks/dhcp/high_availability/ha_config.h b/src/hooks/dhcp/high_availability/ha_config.h
new file mode 100644 (file)
index 0000000..69bf5c0
--- /dev/null
@@ -0,0 +1,392 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#ifndef HA_CONFIG_H
+#define HA_CONFIG_H
+
+#include <exceptions/exceptions.h>
+#include <http/url.h>
+#include <boost/shared_ptr.hpp>
+#include <cstdint>
+#include <map>
+#include <string>
+
+namespace isc {
+namespace ha {
+
+/// @brief Exception thrown when configuration validation fails.
+class HAConfigValidationError : public Exception {
+public:
+    HAConfigValidationError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief Storage for High Availability configuration.
+class HAConfig {
+public:
+
+    /// @brief Mode of operation.
+    ///
+    /// Currently supported modes are:
+    /// - load balancing
+    /// - hot standby
+    enum HAMode {
+        LOAD_BALANCING,
+        HOT_STANDBY,
+    };
+
+    /// @brief HA peer configuration.
+    ///
+    /// It holds configuration of one of the servers participating in the
+    /// high availability configuration. It may represent configuration of
+    /// this server or its partner.
+    class PeerConfig {
+    public:
+
+        /// @brief Server's role in the High Availability setup.
+        ///
+        /// The following roles are supported:
+        /// - primary - server taking part in load balancing or hot standby setup,
+        ///   taking leadership over other servers. There must be exactly one primary
+        ///   server.
+        /// - secondary - server taking part in the load balancing setup. It is a slave
+        ///   server to primary. There must be exactly one secondary server in the
+        ///   load balancing setup.
+        /// - standby - standby server taking part in the hot standby operation. It
+        ///   doesn't run DHCP function until primary server crashes. There must be
+        ///   exactly one standby server in the hot standby setup.
+        /// - backup server - server receiving updates from other servers, but not
+        ///   performing any DHCP function until explicitly enabled to do so.
+        enum Role {
+            PRIMARY,
+            SECONDARY,
+            STANDBY,
+            BACKUP
+        };
+
+        /// @brief Constructor.
+        PeerConfig();
+
+        /// @brief Returns server name.
+        std::string getName() const {
+            return (name_);
+        }
+
+        /// @brief Sets server name.
+        ///
+        /// @param name Server name.
+        /// @throw BadValue if the server name is empty.
+        void setName(const std::string& name);
+
+        /// @brief Returns URL of the server's control channel.
+        http::Url getUrl() const {
+            return (url_);
+        }
+
+        /// @brief Sets server's URL.
+        ///
+        /// @param url URL value.
+        void setUrl(const http::Url& url) {
+            url_ = url;
+        }
+
+        /// @brief Returns a string identifying a server used in logging.
+        ///
+        /// The label is constructed from server name and server URL.
+        ///
+        /// @return String identifying a server.
+        std::string getLogLabel() const;
+
+        /// @brief Returns server's role.
+        Role getRole() const {
+            return (role_);
+        }
+
+        /// @brief Sets servers role.
+        ///
+        /// The following are the supported roles in the textual form:
+        /// - primary,
+        /// - secondary,
+        /// - standby
+        /// - backup
+        ///
+        /// @param role Server role in the textual form.
+        void setRole(const std::string& role);
+
+        /// @brief Decodes role provided as a string.
+        ///
+        /// @param role Role as string.
+        /// @return Server role converted from string.
+        /// @throw BadValue if the specified role is unsupported.
+        static Role stringToRole(const std::string& role);
+
+        /// @brief Returns role name.
+        ///
+        /// @param role Role which name should be returned.
+        /// @return Role name.
+        static std::string roleToString(const HAConfig::PeerConfig::Role& role);
+
+        /// @brief Checks if the auto-failover function is enabled for the
+        /// server.
+        ///
+        /// @return true if auto failover function has been enabled for the server.
+        bool isAutoFailover() const {
+            return (auto_failover_);
+        }
+
+        /// @brief Enables/disables auto-failover function for the server.
+        ///
+        /// @param auto_failover Boolean value indicating if auto-failover function
+        /// should be enabled/disabled for the server.
+        void setAutoFailover(const bool auto_failover) {
+            auto_failover_ = auto_failover;
+        }
+
+    private:
+
+        std::string name_;   ///< Server name.
+        http::Url url_;      ///< Server URL.
+        Role role_;          ///< Server role.
+        bool auto_failover_; ///< Auto failover state.
+
+    };
+
+    /// @brief Pointer to the server's configuration.
+    typedef boost::shared_ptr<PeerConfig> PeerConfigPtr;
+
+    /// @brief Map of the servers' configurations.
+    typedef std::map<std::string, PeerConfigPtr> PeerConfigMap;
+
+    /// @brief Constructor.
+    HAConfig();
+
+    /// @brief Creates and returns pointer to the new peer's configuration.
+    ///
+    /// This method is called during peers configuration parsing, when the
+    /// parser starts reading configuration of the next peer on the list.
+    /// It will store parsed values into this object.
+    ///
+    /// @param name Name of the server for which new configuration should be
+    /// created.
+    /// @throw BadValue if there is already a configuration for the given
+    /// server name.
+    PeerConfigPtr selectNextPeerConfig(const std::string& name);
+
+    /// @brief Returns name of this server.
+    std::string getThisServerName() const {
+        return (this_server_name_);
+    }
+
+    /// @brief Sets name of this server.
+    ///
+    /// @param this_server_name This server name.
+    /// @throw BadValue If the provided server name is empty.
+    void setThisServerName(const std::string& this_server_name);
+
+    /// @brief Returns mode of operation.
+    HAMode getHAMode() const {
+        return (ha_mode_);
+    }
+
+    /// @brief Sets new mode of operation.
+    ///
+    /// The following modes of operation are supported:
+    /// - load-balancing
+    /// - hot-standby
+    ///
+    /// @param ha_mode High Availability mode operation in textual form.
+    /// @throw BadValue if non-supported mode of operation has been specified.
+    void setHAMode(const std::string& ha_mode);
+
+    /// @brief Decodes HA mode provided as string.
+    ///
+    /// @param ha_mode HA mode as string.
+    /// @return HA mode converted from string.
+    /// @throw BadValue if specified HA mode name is unsupported.
+    static HAMode stringToHAMode(const std::string& ha_mode);
+
+    /// @brief Returns HA mode name.
+    ///
+    /// @param ha_mode HA mode which name should be returned.
+    /// @return HA mode name.
+    static std::string HAModeToString(const HAMode& ha_mode);
+
+    /// @brief Returns boolean flag indicating whether lease updates
+    /// should be sent to the partner.
+    bool amSendingLeaseUpdates() const {
+        return (send_lease_updates_);
+    }
+
+    /// @brief Sets boolean flag indicating whether lease updates should be
+    /// sent to the partner.
+    ///
+    /// Disabling lease updates is useful in cases when lease database
+    /// replication is enabled, e.g. MySQL database replication. The database
+    /// itself takes care of updating the backup database with new data.
+    /// Sending lease updates is enabled by default.
+    ///
+    /// @param send_lease_updates new value for the flag.
+    void setSendLeaseUpdates(const bool send_lease_updates) {
+        send_lease_updates_ = send_lease_updates;
+    }
+
+    /// @brief Returns boolean flag indicating whether the active servers
+    /// should synchronize their lease databases upon startup.
+    bool amSyncingLeases() const {
+        return (sync_leases_);
+    }
+
+    /// @brief Sets boolean flag indicating whether the active servers should
+    /// synchronize their lease databases upon startup.
+    ///
+    /// Disabling lease database synchronization is useful in cases when lease
+    /// database replication is enabled. See the description of the
+    /// @c setSendLeaseUpdates. Lease database synchronization is enabled by
+    /// default on active HA servers.
+    ///
+    /// @param sync_leases new value for the flag.
+    void setSyncLeases(const bool sync_leases) {
+        sync_leases_ = sync_leases;
+    }
+
+    /// @brief Returns heartbeat delay in milliseconds.
+    ///
+    /// This value indicates the delay in sending a heartbeat command after
+    /// last heartbeat or some other command to the partner. A value of zero
+    /// disables the heartbeat.
+    ///
+    /// @return Heartbeat delay in milliseconds.
+    uint32_t getHeartbeatDelay() const {
+        return (heartbeat_delay_);
+    }
+
+    /// @brief Sets new heartbeat delay in milliseconds.
+    ///
+    /// This value indicates the delay in sending a heartbeat command after
+    /// last heartbeat or some other command to the partner. A value of zero
+    /// disables the heartbeat.
+    ///
+    /// @param heartbeat_delay new heartbeat delay value.
+    void setHeartbeatDelay(const uint32_t heartbeat_delay) {
+        heartbeat_delay_ = heartbeat_delay;
+    }
+
+    /// @brief Returns max response delay.
+    ///
+    /// Max response delay is the maximum time that the server is waiting for
+    /// its partner to respond to the heartbeats (and lease updates) before it
+    /// assumes the communications interrupted state.
+    uint32_t getMaxResponseDelay() const {
+        return (max_response_delay_);
+    }
+
+    /// @brief Sets new max response delay.
+    ///
+    /// Max response delay is the maximum time that the server is waiting for
+    /// its partner to respond to the heartbeats (and lease updates) before it
+    /// assumes the communications interrupted state.
+    ///
+    /// @param max_response_delay
+    void setMaxResponseDelay(const uint32_t max_response_delay) {
+        max_response_delay_ = max_response_delay;
+    }
+
+    /// @brief Returns maximum time for a client trying to communicate with
+    /// DHCP server to complete the transaction.
+    ///
+    /// @return Maximum delay in milliseconds.
+    uint32_t getMaxAckDelay() const {
+        return (max_ack_delay_);
+    }
+
+    /// @brief Sets maximum time for a client trying to communicate with
+    /// DHCP server to completed the transaction.
+    ///
+    /// @param max_ack_delay maximum time in milliseconds.
+    void setMaxAckDelay(const uint32_t max_ack_delay) {
+        max_ack_delay_ = max_ack_delay;
+    }
+
+    /// @brief Returns maximum number of clients which may fail to communicate
+    /// with the DHCP server before entering partner down state.
+    ///
+    /// @return Maximum number of clients.
+    uint32_t getMaxUnackedClients() const {
+        return (max_unacked_clients_);
+    }
+
+    /// @brief Set maximum number of clients which may fail to communicate
+    /// with the DHCP server before entering partner down state.
+    ///
+    /// @param max_unacked_clients maximum number of clients.
+    void setMaxUnackedClients(const uint32_t max_unacked_clients) {
+        max_unacked_clients_ = max_unacked_clients;
+    }
+
+    /// @brief Returns configuration of the specified server.
+    ///
+    /// @param name Server name.
+    ///
+    /// @return Pointer to the partner's configuration.
+    /// @throw InvalidOperation if there is no suitable configuration found.
+    PeerConfigPtr getPeerConfig(const std::string& name) const;
+
+    /// @brief Returns configuration of the partner which takes part in
+    /// failover.
+    ///
+    /// The server for which the configuration is returned is a "primary",
+    /// "secondary" or "standby". This method is typically used to locate
+    /// the configuration of the server to which heartbeat command is to
+    /// be sent.
+    ///
+    /// @return Pointer to the partner's configuration.
+    /// @throw InvalidOperation if there is no suitable configuration found.
+    PeerConfigPtr getFailoverPeerConfig() const;
+
+    /// @brief Returns configuration of this server.
+    ///
+    /// @return Pointer to the configuration of this server.
+    PeerConfigPtr getThisServerConfig() const;
+
+    /// @brief Returns configuration of other servers.
+    ///
+    /// Returns a map of pointers to the configuration of all servers except
+    /// this.
+    ///
+    /// @return Map of pointers to the servers' configurations.
+    PeerConfigMap getOtherServersConfig() const;
+
+    /// @brief Returns configurations of all servers.
+    ///
+    /// @return Map of pointers to the servers' configurations.
+    PeerConfigMap getAllServersConfig() const {
+        return (peers_);
+    }
+
+    /// @brief Validates configuration.
+    ///
+    /// @throw HAConfigValidationError if configuration is invalid.
+    void validate() const;
+
+private:
+
+    std::string this_server_name_; ///< This server name.
+    HAMode ha_mode_;               ///< Mode of operation.
+    bool send_lease_updates_;      ///< Send lease updates to partner?
+    bool sync_leases_;             ///< Synchronize databases on startup?
+    uint32_t heartbeat_delay_;     ///< Heartbeat delay in milliseconds.
+    uint32_t max_response_delay_;  ///< Max delay in response to heartbeats.
+    uint32_t max_ack_delay_;       ///< Maximum DHCP message ack delay.
+    uint32_t max_unacked_clients_; ///< Maximum number of unacked clients.
+    PeerConfigMap peers_;          ///< Map of peers' configurations.
+};
+
+/// @brief Pointer to the High Availability configuration structure.
+typedef boost::shared_ptr<HAConfig> HAConfigPtr;
+
+} // end of namespace isc::ha
+} // end of namespace isc
+
+#endif
diff --git a/src/hooks/dhcp/high_availability/ha_config_parser.cc b/src/hooks/dhcp/high_availability/ha_config_parser.cc
new file mode 100644 (file)
index 0000000..ab3fd0c
--- /dev/null
@@ -0,0 +1,218 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <config.h>
+
+#include <ha_config_parser.h>
+#include <ha_log.h>
+#include <cc/dhcp_config_error.h>
+#include <limits>
+
+using namespace isc::data;
+using namespace isc::http;
+
+namespace {
+
+/// @brief Default values for HA configuration.
+const SimpleDefaults HA_CONFIG_DEFAULTS = {
+    { "send-lease-updates", Element::boolean, "true" },
+    { "sync-leases", Element::boolean, "true" },
+    { "heartbeat-delay", Element::integer, "10000" },
+    { "max-response-delay", Element::integer, "60000" },
+    { "max-ack-delay", Element::integer, "10000" },
+    { "max-unacked-clients", Element::integer, "10" }
+};
+
+/// @brief Default values for HA peer configuration.
+const SimpleDefaults HA_CONFIG_PEER_DEFAULTS = {
+    { "auto-failover", Element::boolean, "true" }
+};
+
+} // end of anonymous namespace
+
+namespace isc {
+namespace ha {
+
+void
+HAConfigParser::parse(const HAConfigPtr& config_storage,
+                      const ConstElementPtr& config) {
+    try {
+        // This may cause different types of exceptions. We catch them here
+        // and throw unified exception type.
+        parseInternal(config_storage, config);
+        logConfigStatus(config_storage);
+
+    } catch (const ConfigError& ex) {
+        throw;
+
+    } catch (const std::exception& ex) {
+        isc_throw(ConfigError, ex.what());
+    }
+}
+
+void
+HAConfigParser::parseInternal(const HAConfigPtr& config_storage,
+                              const ConstElementPtr& config) {
+    // Config must be provided.
+    if (!config) {
+        isc_throw(ConfigError, "HA configuration must not be null");
+    }
+
+    // Config must be a list. Each contains one relationship between servers in the
+    // HA configuration. Currently we support only one relationship.
+    if (config->getType() != Element::list) {
+        isc_throw(ConfigError, "HA configuration must be a list");
+    }
+
+    const auto& config_vec = config->listValue();
+    if (config_vec.size() != 1) {
+        isc_throw(ConfigError, "invalid number of configurations in the HA configuration"
+                  " list. Expected exactly one configuration");
+    }
+
+    // Get the HA configuration.
+    ElementPtr c = config_vec[0];
+
+    // Set default values.
+    setDefaults(c, HA_CONFIG_DEFAULTS);
+
+    // HA configuration must be a map.
+    if (c->getType() != Element::map) {
+        isc_throw(ConfigError, "expected list of maps in the HA configuration");
+    }
+
+    // It must contains peers section.
+    if (!c->contains("peers")) {
+        isc_throw(ConfigError, "'peers' parameter missing in HA configuration");
+    }
+
+    // Peers configuration must be a list of maps.
+    ConstElementPtr peers = c->get("peers");
+    if (peers->getType() != Element::list) {
+        isc_throw(ConfigError, "'peers' parameter must be a list");
+    }
+
+    // We have made major sanity checks, so let's try to gather some values.
+
+    // Get 'this-server-name'.
+    config_storage->setThisServerName(getString(c, "this-server-name"));
+
+    // Get 'mode'.
+    config_storage->setHAMode(getString(c, "mode"));
+
+    // Get 'send-lease-updates'.
+    config_storage->setSendLeaseUpdates(getBoolean(c, "send-lease-updates"));
+
+    // Get 'sync-leases'.
+    config_storage->setSyncLeases(getBoolean(c, "sync-leases"));
+
+    // Get 'heartbeat-delay'.
+    uint16_t heartbeat_delay = getAndValidateInteger<uint16_t>(c, "heartbeat-delay");
+    config_storage->setHeartbeatDelay(heartbeat_delay);
+
+    // Get 'max-response-delay'.
+    uint16_t max_response_delay = getAndValidateInteger<uint16_t>(c, "max-response-delay");
+    config_storage->setMaxResponseDelay(max_response_delay);
+
+    // Get 'max-ack-delay'.
+    uint16_t max_ack_delay = getAndValidateInteger<uint16_t>(c, "max-ack-delay");
+    config_storage->setMaxAckDelay(max_ack_delay);
+
+    // Get 'max-unacked-clients'.
+    uint32_t max_unacked_clients = getAndValidateInteger<uint32_t>(c, "max-unacked-clients");
+    config_storage->setMaxUnackedClients(max_unacked_clients);
+
+    // Peers configuration parsing.
+    const auto& peers_vec = peers->listValue();
+
+    // There must be at least two peers specified.
+    if (peers_vec.size() < 2) {
+        isc_throw(ConfigError, "peers configuration requires at least two peers"
+                  " to be specified");
+    }
+
+    // Go over configuration of each peer.
+    for (auto p = peers_vec.begin(); p != peers_vec.end(); ++p) {
+
+        // Peer configuration is held in a map.
+        if ((*p)->getType() != Element::map) {
+            isc_throw(ConfigError, "peer configuration must be a map");
+        }
+
+        setDefaults(*p, HA_CONFIG_PEER_DEFAULTS);
+
+        // Server name.
+        auto cfg = config_storage->selectNextPeerConfig(getString(*p, "name"));
+
+        // URL.
+        cfg->setUrl(Url(getString((*p), "url")));
+
+        // Role.
+        cfg->setRole(getString(*p, "role"));
+
+        // Auto failover configuration.
+        cfg->setAutoFailover(getBoolean(*p, "auto-failover"));
+    }
+
+    // We have gone over the entire configuration and stored it in the configuration
+    // storage. However, we need to still validate it to detect errors like:
+    // duplicate secondary/primary servers, no configuration for this server etc.
+    config_storage->validate();
+}
+
+template<typename T>
+T HAConfigParser::getAndValidateInteger(const ConstElementPtr& config,
+                                        const std::string& parameter_name) const {
+    int64_t value = getInteger(config, parameter_name);
+    if (value < 0) {
+        isc_throw(ConfigError, "'" << parameter_name << "' must not be negative");
+
+    } else if (value > std::numeric_limits<T>::max()) {
+        isc_throw(ConfigError, "'" << parameter_name << "' must not be greater than "
+                  << std::numeric_limits<T>::max());
+    }
+
+    return (static_cast<T>(value));
+}
+
+void
+HAConfigParser::logConfigStatus(const HAConfigPtr& config_storage) const {
+    LOG_INFO(ha_logger, HA_CONFIGURATION_SUCCESSFUL);
+
+    // If lease updates are disabled, we want to make sure that the user
+    // realizes that and that he has configured some other mechanism to
+    // populate leases.
+    if (!config_storage->amSendingLeaseUpdates()) {
+        LOG_WARN(ha_logger, HA_CONFIG_LEASE_UPDATES_DISABLED);
+    }
+
+    // Same as above but for lease database synchronization.
+    if (!config_storage->amSyncingLeases()) {
+        LOG_WARN(ha_logger, HA_CONFIG_LEASE_SYNCING_DISABLED);
+    }
+
+    // Unusual configuration.
+    if (config_storage->amSendingLeaseUpdates() !=
+        config_storage->amSyncingLeases()) {
+        LOG_WARN(ha_logger, HA_CONFIG_LEASE_UPDATES_AND_SYNCING_DIFFER)
+            .arg(config_storage->amSendingLeaseUpdates() ? "true" : "false")
+            .arg(config_storage->amSyncingLeases() ? "true" : "false");
+    }
+
+    // With this setting the server will not take ownership of the partner's
+    // scope in case of partner's failure. This setting is ok if the
+    // administrator desires to have more control over scopes selection.
+    // The administrator will need to send ha-scopes command to instruct
+    // the server to take ownership of the scope. In some cases he may
+    // also need to send dhcp-enable command to enable DHCP service
+    // (specifically hot-standby mode for standby server).
+    if (!config_storage->getThisServerConfig()->isAutoFailover()) {
+        LOG_WARN(ha_logger, HA_CONFIG_AUTO_FAILOVER_DISABLED)
+            .arg(config_storage->getThisServerName());
+    }
+}
+
+} // end of namespace ha
+} // end of namespace isc
diff --git a/src/hooks/dhcp/high_availability/ha_config_parser.h b/src/hooks/dhcp/high_availability/ha_config_parser.h
new file mode 100644 (file)
index 0000000..961a37b
--- /dev/null
@@ -0,0 +1,66 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#ifndef HA_CONFIG_PARSER_H
+#define HA_CONFIG_PARSER_H
+
+#include <ha_config.h>
+#include <cc/data.h>
+#include <cc/simple_parser.h>
+#include <string>
+
+namespace isc {
+namespace ha {
+
+/// @brief Configuration parser for High Availability.
+class HAConfigParser : public data::SimpleParser {
+public:
+
+    /// @brief Parses HA configuration.
+    ///
+    /// @param [out] config_storage Pointer to the object where parsed configuration
+    /// is going to be stored.
+    ///
+    /// @param config Specified configuration.
+    /// @throw ConfigError when parsing fails or configuration is invalid.
+    void parse(const HAConfigPtr& config_storage,
+               const data::ConstElementPtr& config);
+
+private:
+
+    /// @brief Parses HA configuration and can throw various exceptions..
+    ///
+    /// @param [out] config_storage Pointer to the object where parsed configuration
+    /// is going to be stored.
+    ///
+    /// @param config Specified configuration.
+    void parseInternal(const HAConfigPtr& config_storage,
+                       const data::ConstElementPtr& config);
+
+    /// @brief Validates and returns a value of the parameter.
+    ///
+    /// @param config configuration map from which the parameter should be
+    /// retrieved.
+    /// @param parameter_name parameter name to be fetched from the configuration.
+    /// @tparam T parameter type, e.g. @c uint16_t, @c uint32_t etc.
+    template<typename T>
+    T getAndValidateInteger(const data::ConstElementPtr& config,
+                            const std::string& parameter_name) const;
+
+    /// @brief Logs various information related to the successfully parsed
+    /// configuration.
+    ///
+    /// @param config_storage Pointer to the object where parsed configuration
+    /// is stored.
+    ///
+    /// One example of such information is a warning message indicating that
+    /// sending lease updates is disabled.
+    void logConfigStatus(const HAConfigPtr& config_storage) const;
+};
+
+} // end of namespace isc::ha
+} // end of namespace isc
+
+#endif
diff --git a/src/hooks/dhcp/high_availability/ha_impl.cc b/src/hooks/dhcp/high_availability/ha_impl.cc
new file mode 100644 (file)
index 0000000..a144c4c
--- /dev/null
@@ -0,0 +1,393 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <config.h>
+
+#include <ha_config_parser.h>
+#include <ha_impl.h>
+#include <ha_log.h>
+#include <asiolink/io_service.h>
+#include <cc/data.h>
+#include <cc/command_interpreter.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt6.h>
+#include <dhcpsrv/lease.h>
+#include <stats/stats_mgr.h>
+
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::hooks;
+using namespace isc::log;
+
+namespace isc {
+namespace ha {
+
+HAImpl::HAImpl()
+    : config_(new HAConfig()) {
+}
+
+void
+HAImpl::configure(const ConstElementPtr& input_config) {
+    HAConfigParser parser;
+    parser.parse(config_, input_config);
+}
+
+void
+HAImpl::startService(const IOServicePtr& io_service,
+                     const NetworkStatePtr& network_state,
+                     const HAServerType& server_type) {
+    // Create the HA service and crank up the state machine.
+    service_ = boost::make_shared<HAService>(io_service, network_state,
+                                             config_, server_type);
+}
+
+void
+HAImpl::buffer4Receive(hooks::CalloutHandle& callout_handle) {
+    Pkt4Ptr query4;
+    callout_handle.getArgument("query4", query4);
+
+    /// @todo Add unit tests to verify the behavior for different
+    /// malformed packets.
+    try {
+        // We have to unpack the query to get access into HW address which is
+        // used to load balance the packet.
+        query4->unpack();
+
+    } catch (const SkipRemainingOptionsError& ex) {
+        // An option failed to unpack but we are to attempt to process it
+        // anyway.  Log it and let's hope for the best.
+        LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC,
+                  HA_BUFFER4_RECEIVE_PACKET_OPTIONS_SKIPPED)
+            .arg(ex.what());
+
+    } catch (const std::exception& ex) {
+        // Packet parsing failed. Drop the packet.
+        LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_BUFFER4_RECEIVE_UNPACK_FAILED)
+            .arg(query4->getRemoteAddr().toText())
+            .arg(query4->getLocalAddr().toText())
+            .arg(query4->getIface())
+            .arg(ex.what());
+
+        // Increase the statistics of parse failures and dropped packets.
+        isc::stats::StatsMgr::instance().addValue("pkt4-parse-failed",
+                                                  static_cast<int64_t>(1));
+        isc::stats::StatsMgr::instance().addValue("pkt4-receive-drop",
+                                                  static_cast<int64_t>(1));
+
+
+        callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+        return;
+    }
+
+    // Check if we should process this query. If not, drop it.
+    if (!service_->inScope(query4)) {
+        LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_BUFFER4_RECEIVE_NOT_FOR_US)
+            .arg(query4->getLabel());
+        callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+
+    } else {
+        // We have successfully parsed the query so we have to signal
+        // to the server that it must not parse it.
+        callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+    }
+}
+
+void
+HAImpl::leases4Committed(CalloutHandle& callout_handle) {
+    // If the hook library is configured to not send lease updates to the
+    // partner, there is nothing to do because this whole callout is
+    // currently about sending lease updates.
+    if (!config_->amSendingLeaseUpdates()) {
+        // No need to log it, because it was already logged when configuration
+        // was applied.
+        return;
+    }
+
+    Pkt4Ptr query4;
+    Lease4CollectionPtr leases4;
+    Lease4CollectionPtr deleted_leases4;
+
+    // Get all arguments available for the leases4_committed hook point.
+    // If any of these arguments is not available this is a programmatic
+    // error. An exception will be thrown which will be caught by the
+    // caller and logged.
+    callout_handle.getArgument("query4", query4);
+
+    callout_handle.getArgument("leases4", leases4);
+    callout_handle.getArgument("deleted_leases4", deleted_leases4);
+
+    // In some cases we may have no leases, e.g. DHCPNAK.
+    if (leases4->empty() && deleted_leases4->empty()) {
+        LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_LEASES4_COMMITTED_NOTHING_TO_UPDATE)
+            .arg(query4->getLabel());
+        return;
+    }
+
+    // Get the parking lot for this hook point. We're going to remember this
+    // pointer until we unpark the packet.
+    ParkingLotHandlePtr parking_lot = callout_handle.getParkingLotHandlePtr();
+
+    // Asynchronously send lease updates. In some cases no updates will be sent,
+    // e.g. when this server is in the partner-down state and there are no backup
+    // servers. In those cases we simply return without parking the DHCP query.
+    // The response will be sent to the client immediately.
+    if (service_->asyncSendLeaseUpdates(query4, leases4, deleted_leases4, parking_lot) == 0) {
+        return;
+    }
+
+    // This is required step every time we ask the server to park the packet.
+    // The reference counting is required to keep the packet parked until
+    // all callouts call unpark. Then, the packet gets unparked and the
+    // associated callback is triggered. The callback resumes packet processing.
+    parking_lot->reference(query4);
+
+    // The callout returns this status code to indicate to the server that it
+    // should park the query packet.
+    callout_handle.setStatus(CalloutHandle::NEXT_STEP_PARK);
+}
+
+void
+HAImpl::buffer6Receive(hooks::CalloutHandle& callout_handle) {
+    Pkt6Ptr query6;
+    callout_handle.getArgument("query6", query6);
+
+    /// @todo Add unit tests to verify the behavior for different
+    /// malformed packets.
+    try {
+        // We have to unpack the query to get access into DUID which is
+        // used to load balance the packet.
+        query6->unpack();
+
+    } catch (const SkipRemainingOptionsError& ex) {
+        // An option failed to unpack but we are to attempt to process it
+        // anyway.  Log it and let's hope for the best.
+        LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC,
+                  HA_BUFFER6_RECEIVE_PACKET_OPTIONS_SKIPPED)
+            .arg(ex.what());
+
+    } catch (const std::exception& ex) {
+        // Packet parsing failed. Drop the packet.
+        LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_BUFFER6_RECEIVE_UNPACK_FAILED)
+            .arg(query6->getRemoteAddr().toText())
+            .arg(query6->getLocalAddr().toText())
+            .arg(query6->getIface())
+            .arg(ex.what());
+
+        // Increase the statistics of parse failures and dropped packets.
+        isc::stats::StatsMgr::instance().addValue("pkt6-parse-failed",
+                                                  static_cast<int64_t>(1));
+        isc::stats::StatsMgr::instance().addValue("pkt6-receive-drop",
+                                                  static_cast<int64_t>(1));
+
+
+        callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+        return;
+    }
+
+    // Check if we should process this query. If not, drop it.
+    if (!service_->inScope(query6)) {
+        LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_BUFFER6_RECEIVE_NOT_FOR_US)
+            .arg(query6->getLabel());
+        callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+
+    } else {
+        // We have successfully parsed the query so we have to signal
+        // to the server that it must not parse it.
+        callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+    }
+}
+
+void
+HAImpl::leases6Committed(CalloutHandle& callout_handle) {
+    // If the hook library is configured to not send lease updates to the
+    // partner, there is nothing to do because this whole callout is
+    // currently about sending lease updates.
+    if (!config_->amSendingLeaseUpdates()) {
+        // No need to log it, because it was already logged when configuration
+        // was applied.
+        return;
+    }
+
+    Pkt6Ptr query6;
+    Lease6CollectionPtr leases6;
+    Lease6CollectionPtr deleted_leases6;
+
+    // Get all arguments available for the leases6_committed hook point.
+    // If any of these arguments is not available this is a programmatic
+    // error. An exception will be thrown which will be caught by the
+    // caller and logged.
+    callout_handle.getArgument("query6", query6);
+
+    callout_handle.getArgument("leases6", leases6);
+    callout_handle.getArgument("deleted_leases6", deleted_leases6);
+
+    // In some cases we may have no leases.
+    if (leases6->empty() && deleted_leases6->empty()) {
+        LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_LEASES6_COMMITTED_NOTHING_TO_UPDATE)
+            .arg(query6->getLabel());
+        return;
+    }
+
+    // Get the parking lot for this hook point. We're going to remember this
+    // pointer until we unpark the packet.
+    ParkingLotHandlePtr parking_lot = callout_handle.getParkingLotHandlePtr();
+
+    // Asynchronously send lease updates. In some cases no updates will be sent,
+    // e.g. when this server is in the partner-down state and there are no backup
+    // servers. In those cases we simply return without parking the DHCP query.
+    // The response will be sent to the client immediately.
+    if (service_->asyncSendLeaseUpdates(query6, leases6, deleted_leases6, parking_lot) == 0) {
+        return;
+    }
+
+    // This is required step every time we ask the server to park the packet.
+    // The reference counting is required to keep the packet parked until
+    // all callouts call unpark. Then, the packet gets unparked and the
+    // associated callback is triggered. The callback resumes packet processing.
+    parking_lot->reference(query6);
+
+    // The callout returns this status code to indicate to the server that it
+    // should park the query packet.
+    callout_handle.setStatus(CalloutHandle::NEXT_STEP_PARK);
+}
+
+void
+HAImpl::commandProcessed(hooks::CalloutHandle& callout_handle) {
+    std::string command_name;
+    callout_handle.getArgument("name", command_name);
+    if (command_name == "dhcp-enable") {
+        service_->adjustNetworkState();
+    }
+}
+
+void
+HAImpl::heartbeatHandler(CalloutHandle& callout_handle) {
+    ConstElementPtr response = service_->processHeartbeat();
+    callout_handle.setArgument("response", response);
+}
+
+void
+HAImpl::synchronizeHandler(hooks::CalloutHandle& callout_handle) {
+    // Command must always be provided.
+    ConstElementPtr command;
+    callout_handle.getArgument("command", command);
+
+    // Retrieve arguments.
+    ConstElementPtr args;
+    static_cast<void>(parseCommand(args, command));
+
+    ConstElementPtr server_name;
+    unsigned int max_period_value = 0;
+
+    try {
+        // Arguments are required for the ha-sync command.
+        if (!args) {
+            isc_throw(BadValue, "arguments not found in the 'ha-sync' command");
+        }
+
+        // Arguments must be a map.
+        if (args->getType() != Element::map) {
+            isc_throw(BadValue, "arguments in the 'ha-sync' command are not a map");
+        }
+
+        // server-name is mandatory. Otherwise how can we know the server to
+        // communicate with.
+        server_name = args->get("server-name");
+        if (!server_name) {
+            isc_throw(BadValue, "'server-name' is mandatory for the 'ha-sync' command");
+        }
+
+        // server-name must obviously be a string.
+        if (server_name->getType() != Element::string) {
+            isc_throw(BadValue, "'server-name' must be a string in the 'ha-sync' command");
+        }
+
+        // max-period is optional. In fact it is optional for dhcp-disable command too.
+        ConstElementPtr max_period = args->get("max-period");
+        if (max_period) {
+            // If it is specified, it must be a positive integer.
+            if ((max_period->getType() != Element::integer) ||
+                (max_period->intValue() <= 0)) {
+                isc_throw(BadValue, "'max-period' must be a positive integer in the 'ha-sync' command");
+            }
+
+            max_period_value = static_cast<unsigned int>(max_period->intValue());
+        }
+
+    } catch (const std::exception& ex) {
+        // There was an error while parsing command arguments. Return an error status
+        // code to notify the user.
+        ConstElementPtr response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
+        callout_handle.setArgument("response", response);
+        return;
+    }
+
+    // Command parsing was successful, so let's process the command.
+    ConstElementPtr response = service_->processSynchronize(server_name->stringValue(),
+                                                            max_period_value);
+    callout_handle.setArgument("response", response);
+}
+
+void
+HAImpl::scopesHandler(hooks::CalloutHandle& callout_handle) {
+    // Command must always be provided.
+    ConstElementPtr command;
+    callout_handle.getArgument("command", command);
+
+    // Retrieve arguments.
+    ConstElementPtr args;
+    static_cast<void>(parseCommand(args, command));
+
+    std::vector<std::string> scopes_vector;
+
+    try {
+        // Arguments must be present.
+        if (!args) {
+            isc_throw(BadValue, "arguments not found in the 'ha-scopes' command");
+        }
+
+        // Arguments must be a map.
+        if (args->getType() != Element::map) {
+            isc_throw(BadValue, "arguments in the 'ha-scopes' command are not a map");
+        }
+
+        // scopes argument is mandatory.
+        ConstElementPtr scopes = args->get("scopes");
+        if (!scopes) {
+            isc_throw(BadValue, "'scopes' is mandatory for the 'ha-scopes' command");
+        }
+
+        // It contains a list of scope names.
+        if (scopes->getType() != Element::list) {
+            isc_throw(BadValue, "'scopes' must be a list in the 'ha-scopes' command");
+        }
+
+        // Retrieve scope names from this list. The list may be empty to clear the
+        // scopes.
+        for (size_t i = 0; i < scopes->size(); ++i) {
+            ConstElementPtr scope = scopes->get(i);
+            if (!scope || scope->getType() != Element::string) {
+                isc_throw(BadValue, "scope name must be a string in the 'scopes' argument");
+            }
+            scopes_vector.push_back(scope->stringValue());
+        }
+
+    } catch (const std::exception& ex) {
+        // There was an error while parsing command arguments. Return an error status
+        // code to notify the user.
+        ConstElementPtr response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
+        callout_handle.setArgument("response", response);
+        return;
+    }
+
+    // Command parsing was successful, so let's process the command.
+    ConstElementPtr response = service_->processScopes(scopes_vector);
+    callout_handle.setArgument("response", response);
+}
+
+} // end of namespace isc::ha
+} // end of namespace isc
diff --git a/src/hooks/dhcp/high_availability/ha_impl.h b/src/hooks/dhcp/high_availability/ha_impl.h
new file mode 100644 (file)
index 0000000..a4c8357
--- /dev/null
@@ -0,0 +1,155 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#ifndef HA_IMPL_H
+#define HA_IMPL_H
+
+#include <ha_config.h>
+#include <ha_service.h>
+#include <asiolink/io_service.h>
+#include <cc/data.h>
+#include <dhcpsrv/network_state.h>
+#include <hooks/hooks.h>
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+
+
+namespace isc {
+namespace ha {
+
+/// @brief High Availability hooks library implementation.
+///
+/// This object provides an interface between the HA hook library callouts
+/// and the HA state model implemented in the @c HAService. Callouts invoke
+/// respective methods of the @c HAImpl to configure the service, generate
+/// lease updates etc. The @c HAImpl retrieves and validates the arguments
+/// provided within @c CalloutHandle object and then invokes appropriate
+/// methods of the @c HAService class.
+class HAImpl : public boost::noncopyable {
+public:
+
+    /// @brief Constructor.
+    HAImpl();
+
+    /// @brief Parases configuration.
+    ///
+    /// @param input_config Configuration specified for the hooks library.
+    /// @throw ConfigError when configuration fails.
+    void configure(const data::ConstElementPtr& input_config);
+
+    /// @brief Creates high availability service using current configuration.
+    ///
+    /// The caller must ensure that the HA configuration is valid before
+    /// calling this function.
+    ///
+    /// @param io_service IO service object provided by the DHCP server.
+    /// @param network_state pointer to the object holding a state of the
+    /// DHCP service (enabled/disabled).
+    /// @param server_type DHCP server type for which the HA service should
+    /// be created.
+    void startService(const asiolink::IOServicePtr& io_service,
+                      const dhcp::NetworkStatePtr& network_state,
+                      const HAServerType& server_type);
+
+    /// @brief Returns parsed configuration.
+    HAConfigPtr getConfig() const {
+        return (config_);
+    }
+
+    /// @brief Implementation of the "buffer4_receive" callout.
+    ///
+    /// This callout uses HA service to check if the query should be processed
+    /// by this server or a partner. If the partner should process the query,
+    /// this callout sets the status to @c CalloutHandle::NEXT_STEP_DROP to
+    /// cause the server to drop the packet. Therefore it is important to note
+    /// that, if multiple hook libraries implementing @c buffer4_receive hook are
+    /// loaded, the order of loading the libraries may matter. If this library
+    /// sets the status to @c CalloutHandle::NEXT_STEP_DROP and the other library
+    /// overrides this status, the query will be processed by the server instead
+    /// of being dropped. This problem may be mitigated by loading the HA library
+    /// last, i.e. placing the library at the end of the "hooks-libraries"
+    /// list within a Kea configuration file.
+    ///
+    /// If the received query is to be processed by this server instance, the
+    /// @c CalloutHabndle::NEXT_STEP_SKIP status is set to prevent the server
+    /// from unpacking the query because the query is unpacked by the callout.
+    ///
+    /// @param callout_handle Callout handle provided to the callout.
+    void buffer4Receive(hooks::CalloutHandle& callout_handle);
+
+    /// @brief Implementation of the "leases4_committed" callout.
+    ///
+    /// @param callout_handle Callout handle provided to the callout.
+    void leases4Committed(hooks::CalloutHandle& callout_handle);
+
+    /// @brief Implementation of the "buffer6_receive" callout.
+    ///
+    /// This callout uses HA service to check if the query should be processed
+    /// by this server or a partner. If the partner should process the query,
+    /// this callout sets the status to @c CalloutHandle::NEXT_STEP_DROP to
+    /// cause the server to drop the packet. Therefore it is important to note
+    /// that, if multiple hook libraries implementing @c buffer6_receive hook are
+    /// loaded, the order of loading the libraries may matter. If this library
+    /// sets the status to @c CalloutHandle::NEXT_STEP_DROP and the other library
+    /// overrides this status, the query will be processed by the server instead
+    /// of being dropped. This problem may be mitigated by loading the HA library
+    /// last, i.e. placing the library at the end of the "hooks-libraries"
+    /// list within a Kea configuration file.
+    ///
+    /// If the received query is to be processed by this server instance, the
+    /// @c CalloutHabndle::NEXT_STEP_SKIP status is set to prevent the server
+    /// from unpacking the query because the query is unpacked by the callout.
+    ///
+    /// @param callout_handle Callout handle provided to the callout.
+    void buffer6Receive(hooks::CalloutHandle& callout_handle);
+
+    /// @brief Implementation of the "leases6_committed" callout.
+    ///
+    /// @param callout_handle Callout handle provided to the callout.
+    void leases6Committed(hooks::CalloutHandle& callout_handle);
+
+    /// @brief Implementation of the "command_processed" callout.
+    ///
+    /// This callout adjusts network state (DHCP service state) after receiving
+    /// a "dhcp-enable" commands. It is preventing a situation when the DHCP
+    /// service is enabled in a state for which this is not allowed, e.g.
+    /// waiting, syncing etc. We don't want to rely on the HA partner to do
+    /// a correct thing in that respect.
+    ///
+    /// @param callout_handle Callout handle provided to the callout.
+    void commandProcessed(hooks::CalloutHandle& callout_handle);
+
+    /// @brief Implements handle for the heartbeat command.
+    ///
+    /// @param callout_handle Callout handle provided to the callout.
+    void heartbeatHandler(hooks::CalloutHandle& callout_handle);
+
+    /// @brief Implements handler for the ha-sync command.
+    ///
+    /// @param callout_handle Callout handle provided to the callout.
+    void synchronizeHandler(hooks::CalloutHandle& callout_handle);
+
+    /// @brief Implements handler for the ha-scopes command.
+    ///
+    /// @param callout_handle Callout handle provided to the callout.
+    void scopesHandler(hooks::CalloutHandle& callout_handle);
+
+protected:
+
+    /// @brief Holds parsed configuration.
+    HAConfigPtr config_;
+
+    /// @brief Pointer to the high availability service (state machine).
+    HAServicePtr service_;
+
+};
+
+/// @brief Pointer to the High Availability hooks library implementation.
+typedef boost::shared_ptr<HAImpl> HAImplPtr;
+
+} // end of namespace isc::ha
+} // end of namespace isc
+
+#endif
diff --git a/src/hooks/dhcp/high_availability/ha_log.cc b/src/hooks/dhcp/high_availability/ha_log.cc
new file mode 100644 (file)
index 0000000..11df23c
--- /dev/null
@@ -0,0 +1,17 @@
+// Copyright (C) 2017-2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <config.h>
+
+#include <ha_log.h>
+
+namespace isc {
+namespace ha {
+
+isc::log::Logger ha_logger("ha-hooks");
+
+} // end of namespace isc::ha
+} // end of namespace isc
+
diff --git a/src/hooks/dhcp/high_availability/ha_log.h b/src/hooks/dhcp/high_availability/ha_log.h
new file mode 100644 (file)
index 0000000..d9d4827
--- /dev/null
@@ -0,0 +1,22 @@
+// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#ifndef HA_LOG_H
+#define HA_LOG_H
+
+#include <log/logger_support.h>
+#include <log/macros.h>
+#include <ha_messages.h>
+
+namespace isc {
+namespace ha {
+
+extern isc::log::Logger ha_logger;
+
+} // end of isc::ha
+} // end of isc namespace
+
+
+#endif
diff --git a/src/hooks/dhcp/high_availability/ha_messages.mes b/src/hooks/dhcp/high_availability/ha_messages.mes
new file mode 100644 (file)
index 0000000..efe6761
--- /dev/null
@@ -0,0 +1,329 @@
+# Copyright (C) 2017-2018 Internet Systems Consortium, Inc. ("ISC")
+
+$NAMESPACE isc::ha
+
+% HA_BUFFER4_RECEIVE_FAILED buffer4_receive callout failed: %1
+This error message is issued when the callout for the buffer4_receive hook
+point failed.  This may occur as a result of an internal server error.
+The argument contains a reason for the error.
+
+% HA_BUFFER4_RECEIVE_NOT_FOR_US %1: dropping query to be processed by another server
+This debug message is issued when the received DHCPv4 query is dropped
+by this server because it should be served by another server. This
+is the case when the remote server was designated to process the packet
+as a result of load balancing or because it is a primary server in the
+hot standby configuration. The argument provides client identification
+information retrieved from the query.
+
+% HA_BUFFER4_RECEIVE_PACKET_OPTIONS_SKIPPED an error upacking an option, caused subsequent options to be skipped: %1
+A debug message issued when an option failed to unpack correctly, making it
+impossible to unpack the remaining options in the DHCPv4 query. The server
+will still attempt to service the packet. The sole argument provides a
+reason for unpacking error.
+
+% HA_BUFFER4_RECEIVE_UNPACK_FAILED failed to parse query from %1 to %2, received over interface %3, reason: %4
+This debug message is issued when received DHCPv4 query is malformed and
+can't be parsed by the buffer4_receive callout. The query will be
+dropped by the server. The first three arguments specify source IP address,
+destination IP address and the interface. The last argument provides a
+reason for failure.
+
+% HA_BUFFER6_RECEIVE_FAILED buffer6_receive callout failed: %1
+This error message is issued when the callout for the buffer6_receive hook
+point failed. This may occur as a result of an internal server error.
+The argument contains a reason for the error.
+
+% HA_BUFFER6_RECEIVE_NOT_FOR_US %1: dropping query to be processed by another server
+This debug message is issued when the received DHCPv6 query is dropped
+by this server because it should be served by another server. This
+is the case when the remote server was designated to process the packet
+as a result of load balancing or because it is a primary server in the
+hot standby configuration. The argument provides client identification
+information retrieved from the query.
+
+% HA_BUFFER6_RECEIVE_PACKET_OPTIONS_SKIPPED an error upacking an option, caused subsequent options to be skipped: %1
+A debug message issued when an option failed to unpack correctly, making it
+impossible to unpack the remaining options in the DHCPv6 query. The server
+will still attempt to service the packet. The sole argument provides a
+reason for unpacking error.
+
+% HA_BUFFER6_RECEIVE_UNPACK_FAILED failed to parse query from %1 to %2, received over interface %3, reason: %4
+This debug message is issued when received DHCPv6 query is malformed and
+can't be parsed by the buffer6_receive callout. The query will be
+dropped by the server. The first three arguments specify source IP address,
+destination IP address and the interface. The last argument provides a
+reason for failure.
+
+% HA_COMMAND_PROCESSED_FAILED command_processed callout failed: %1
+This error message is issued when the callout for the command_processed hook
+point failed. The argument contains a reason for the error.
+
+% HA_CONFIGURATION_FAILED failed to configure High Availability hooks library: %1
+This error message is issued when there is an error configuring the HA hooks
+library. The argument provides the detailed error message.
+
+% HA_CONFIGURATION_SUCCESSFUL HA hook library has been successfully configured
+This informational message is issued when the HA hook library configuration
+parser successfully parses and validates the new configuration.
+
+% HA_CONFIG_AUTO_FAILOVER_DISABLED auto-failover disabled for %1
+This warning message is issued to indicate that the 'auto-failover' parameter
+was administratively disabled for the specified server. The server will not
+automatically start serving partner's scope when the partner failure is detected.
+The server administrator will need to enable this scope manually by
+sending appropriate ha-scopes command.
+
+% HA_CONFIG_LEASE_SYNCING_DISABLED lease database synchronization between HA servers is disabled
+This warning message is issued when the lease database synchronization is
+administratively disabled. This is valid configuration if the leases are
+replicated between lease databases via some other mechanism, e.g. SQL
+database replication.
+
+% HA_CONFIG_LEASE_SYNCING_DISABLED_REMINDER bypassing SYNCING state because lease database synchronization is administratively disabled
+This informational message is issued as a reminder that lease database
+synchronization is administratively disabled and therefore the server
+transitions directly from the "waiting" to "ready" state.
+
+% HA_CONFIG_LEASE_UPDATES_AND_SYNCING_DIFFER unusual configuration where "send-lease-updates": %1 and "sync-leases": %2
+This warning message is issued when the configuration values of the
+send-lease-updates and sync-leases parameters differ. This may be a
+valid configuration but is unusual. Normally, if the lease database
+with replication is in use, both values are set to false. If a lease
+database without replication is in use (e.g. memfile), both values
+are set to true. Providing different values for those parameters means
+that an administrator either wants the server to not synchronize
+leases upon startup but later send lease updates to the partner, or
+the lease database should be synchronized upon startup, but no lease
+updates are later sent as a result of leases allocation.
+
+% HA_CONFIG_LEASE_UPDATES_DISABLED lease updates will not be generated
+This warning message is issued when the lease updates are administratively
+disabled. This is valid configuration if the leases are replicated to the
+partner's database via some other mechanism, e.g. SQL database replication.
+
+% HA_CONFIG_LEASE_UPDATES_DISABLED_REMINDER lease updates are administratively disabled and will not be generated while in %1 state
+This informational message is issued as a reminder that the lease updates
+are administratively disabled and will not be issued in the HA state to
+which the server has transitioned. The sole argument specifies the state
+into which the server has transitioned.
+
+% HA_DEINIT_OK unloading High Availability hooks library successful
+This informational message indicates that the High Availability hooks library
+has been unloaded successfully.
+
+% HA_DHCP4_START_SERVICE_FAILED failed to start DHCPv4 HA service in dhcp4_srv_configured callout: %1
+This error message is issued when an attempt to start High Availability service
+for the DHCPv4 server failed in the dhcp4_srv_configured callout. This
+is internal server error and a bug report should be created.
+
+% HA_DHCP6_START_SERVICE_FAILED failed to start DHCPv4 HA service in dhcp6_srv_configured callout: %1
+This error message is issued when an attempt to start High Availability service
+for the DHCPv4 server failed in the dhcp4_srv_configured callout. This
+is internal server error and a bug report should be created.
+
+% HA_DHCP_DISABLE_COMMUNICATIONS_FAILED failed to send request to disable DHCP service of %1: %2
+This warning message indicates that there was a problem in communication with a
+HA peer while sending the dhcp-disable command. The first argument provides the
+remote server's name. The second argument provides a reason for failure.
+
+% HA_DHCP_DISABLE_FAILED failed to disable DHCP service of %1: %2
+This warning message indicates that a peer returned an error status code
+in response to a dhcp-disable command.  The first argument provides the
+remote server's name. The second argument provides a reason for failure.
+
+% HA_DHCP_ENABLE_COMMUNICATIONS_FAILED failed to send request to enable DHCP service of %1: %2
+This warning message indicates that there was a problem in communication with a
+HA peer while sending the dhcp-enable command. The first argument provides the
+remote server's name. The second argument provides a reason for failure.
+
+% HA_DHCP_ENABLE_FAILED failed to enable DHCP service of %1: %2
+This warning message indicates that a peer returned an error status code
+in response to a dhcp-enable command.  The first argument provides the
+remote server's name. The second argument provides a reason for failure.
+
+% HA_HEARTBEAT_COMMUNICATIONS_FAILED failed to send heartbeat to %1: %2
+This warning message indicates that there was a problem in communication with a
+HA peer while sending a heartbeat. This is a first sign that the peer may be
+down. The server will keep trying to send heartbeats until it considers that
+communication is interrupted.
+
+% HA_HEARTBEAT_FAILED heartbeat to %1 failed: %2
+This warning message indicates that a peer returned an error status code
+in response to a heartbeat. This is the sign that the peer may not function
+properly. The server will keep trying to send heartbeats until it considers
+that communication is interrupted.
+
+% HA_HEARTBEAT_HANDLER_FAILED heartbeat command failed: %1
+This error message is issued to indicate that the heartbeat command handler
+failed while processing the command. The argument provides the reason for
+failure.
+
+% HA_HIGH_CLOCK_SKEW partner's clock is %1, please synchronize clocks!
+This warning message is issued when the clock skew between the active servers
+exceeds 30 seconds. The HA service continues to operate but may not function
+properly, especially for low lease lifetimes. The administrator should
+should synchronize the clocks, e.g. using NTP. If the clock skew exceeds
+60 seconds, the HA service will terminate.
+
+% HA_HIGH_CLOCK_SKEW_CAUSES_TERMINATION partner's clock is %1, causing HA service to terminate
+This warning message is issued when the clock skew between the active servers
+exceeds 60 seconds. The HA service stops. The servers will continue to respond
+to the DHCP queries but won't exchange lease updates or send heartbeats.
+The administrator is required to synchronize the clocks and then restart the
+servers to resume the HA service.
+
+% HA_INIT_OK loading High Availability hooks library successful
+This informational message indicates that the High Availability hooks library
+has been loaded successfully.
+
+% HA_LEASES4_COMMITTED_FAILED leases4_committed callout failed: %1
+This error message is issued when the callout for the leases4_committed hook
+point failed. This includes unexpected errors like wrong arguments provided to
+the callout by the DHCP server (unlikely internal server error).
+The argument contains a reason for the error.
+
+% HA_LEASES4_COMMITTED_NOTHING_TO_UPDATE %1: leases4_committed callout was invoked without any leases
+This debug message is issued when the "leases4_committed" callout returns
+because there are neither new leases nor deleted leases for which updates
+should be sent. The sole argument specifies the details of the client
+which sent the packet.
+
+% HA_LEASES6_COMMITTED_FAILED leases6_committed callout failed: %1
+This error message is issued when the callout for the leases6_committed hook
+point failed. This includes unexpected errors like wrong arguments provided to
+the callout by the DHCP server (unlikely internal server error).
+The argument contains a reason for the error.
+
+% HA_LEASES6_COMMITTED_NOTHING_TO_UPDATE %1: leases6_committed callout was invoked without any leases
+This debug message is issued when the "leases6_committed" callout returns
+because there are neither new leases nor deleted leases for which updates
+should be sent. The sole argument specifies the details of the client
+which sent the packet.
+
+% HA_LEASES_SYNC_COMMUNICATIONS_FAILED failed to communicate with %1 while syncing leases: %2
+This error message is issued to indicate that there was a communication error
+with a partner server while trying to fetch leases from its lease database.
+The argument contains a reason for the error.
+
+% HA_LEASES_SYNC_FAILED failed to synchronize leases with %1: %2
+This error message is issued to indicate that there was a problem while
+parsing a response from the server from which leases have been fetched for
+local database synchronization. The argument contains a reason for the error.
+
+% HA_LEASE_SYNC_FAILED synchronization failed for lease: %1, reason: %2
+This warning message is issued when creating or updating a lease in the
+local lease database fails. The lease information in the JSON format is
+provided as a first argument. The second argument provides a reason for
+the failure.
+
+% HA_LEASE_SYNC_STALE_LEASE4_SKIP skipping stale lease %1 in subnet %2
+This debug message is issued during lease database synchronization, when
+fetched IPv4 lease instance appears to be older than the instance in the
+local database. The newer instance is left in the database and the fetched
+lease is dropped. The remote server will still hold the older lease instance
+until it synchronizes its database with this server. The first argument specifies
+leased address. The second argument specifies a subnet to which the lease
+belongs.
+
+% HA_LEASE_SYNC_STALE_LEASE6_SKIP skipping stale lease %1 in subnet %2
+This debug message is issued during lease database synchronization, when
+fetched IPv6 lease instance appears to be older than the instance in the
+local database. The newer instance is left in the database and the fetched
+lease is dropped. The remote server will still hold the older lease instance
+until it synchronizes its database with this server. The first argument specifies
+leased address. The second argument specifies a subnet to which the lease
+belongs.
+
+% HA_LEASE_UPDATES_DISABLED lease updates will not be sent to the partner while in %1 state
+This informational message is issued to indicate that lease updates will
+not be sent to the partner while the server is in the current state. The
+argument specifies the server's current state name. The lease updates
+are still sent to the backup servers if they are configured but any
+possible errors in communication with the backup servers are ignored.
+
+% HA_LEASE_UPDATES_ENABLED lease updates will be sent to the partner while in %1 state
+This informational message is issued to indicate that lease updates will
+be sent to the partner while the server is in the current state. The
+argument specifies the server's current state name.
+
+% HA_LEASE_UPDATE_COMMUNICATIONS_FAILED %1: failed to communicate with %2: %3
+This warning message indicates that there was a problem in communication with a
+HA peer while processing a DHCP client query and sending lease update. The
+client's DHCP message will be dropped.
+
+% HA_LEASE_UPDATE_FAILED %1: lease update to %2 failed: %3
+This warning message indicates that a peer returned an error status code
+in response to a lease update. The client's DHCP message will be dropped.
+
+% HA_LOAD_BALANCING_DUID_MISSING load balancing failed for the DHCPv6 message (transaction id: %1) because DUID is missing
+This debug message is issued when the HA hook library was unable to load
+balance an incoming DHCPv6 query because neither client identifier nor
+HW address was included in the query. The query will be dropped. The
+sole argument contains transaction id.
+
+% HA_LOAD_BALANCING_IDENTIFIER_MISSING load balancing failed for the DHCPv4 message (transaction id: %1) because HW address and client identifier are missing
+This debug message is issued when the HA hook library was unable to load
+balance an incoming DHCPv4 query because neither client identifier nor
+HW address was included in the query. The query will be dropped. The
+sole argument contains transaction id.
+
+% HA_LOCAL_DHCP_DISABLE local DHCP service is disabled while the %1 is in the %2 state
+This informational message is issued to indicate that the local DHCP service
+is disabled because the server remains in a state in which the server
+should not respond to DHCP clients, e.g. the server hasn't synchronized
+its lease database. The first argument specifies server name. The second
+argument specifies server's state.
+
+% HA_LOCAL_DHCP_ENABLE local DHCP service is enabled while the %1 is in the %2 state
+This informational message is issued to indicate that the local DHCP service
+is enabled because the server remains in a state in which it should
+respond to the DHCP clients. The first argument specifies server name.
+The second argument specifies server's state.
+
+% HA_MISSING_CONFIGURATION high-availability parameter not specified for High Availability hooks library
+This error message is issued to indicate that the configuration for the
+High Availability hooks library hasn't been specified. The 'high-availability'
+parameter must be specified for the hooks library to load properly.
+
+% HA_SCOPES_HANDLER_FAILED ha-scopes command failed: %1
+This error message is issued to indicate that the ha-scopes command handler
+failed while processing the command. The argument provides reason for
+the failure.
+
+% HA_SERVICE_STARTED started high availability service in %1 mode as %2 server
+This informational message is issued when the HA service is started as a result
+of server startup or reconfiguration. The first argument provides the HA mode.
+The second argument specifies the role of this server instance in this
+configuration.
+
+% HA_STATE_TRANSITION server transitions from %1 to %2 state, partner state is %3
+This informational message is issued when the server transitions to a new
+state as a result of some interaction (or lack of thereof) with its partner.
+The arguments specify initial server state, new server state and the partner's
+state.
+
+% HA_SYNC_FAILED lease database synchronization with %1 failed: %2
+This error message is issued to indicate that the lease database synchronization
+failed. The first argument provides partner server's name. The second argument
+provides a reason for the failure.
+
+% HA_SYNC_HANDLER_FAILED ha-sync command failed: %1
+This error message is issued to indicate that the ha-sync command handler
+failed while processing the command. The argument provides the reason for
+failure.
+
+% HA_SYNC_START starting lease database synchronization with %1
+This informational message is issued when the server starts lease database
+synchronization with a partner. The name of the partner is specified with the
+sole argument.
+
+% HA_SYNC_SUCCESSFUL lease database synchronization with %1 completed successfully in %2
+This informational message is issued when the server successfully completed
+lease database synchronization with the partner. The first argument specifies
+the name of the partner server. The second argument specifies the duration of
+the synchronization.
+
+% HA_TERMINATED HA service terminated because of the unacceptable clock skew; fix the problem and restart!
+This error message is issued to indicate that the HA service has been stopped
+due to unacceptable clock skew. The error can be fixed by synchronizing the
+clocks on the active servers and restarting Kea.
diff --git a/src/hooks/dhcp/high_availability/ha_server_type.h b/src/hooks/dhcp/high_availability/ha_server_type.h
new file mode 100644 (file)
index 0000000..eae39e8
--- /dev/null
@@ -0,0 +1,22 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#ifndef HA_SERVER_TYPE_H
+#define HA_SERVER_TYPE_H
+
+namespace isc {
+namespace ha {
+
+/// @brief Lists possible server types for which HA service is created.
+enum class HAServerType {
+    DHCPv4,
+    DHCPv6
+};
+
+} // end of namespace isc::ha
+} // end of namespace isc
+
+#endif // HA_SERVER_TYPE_H
+
diff --git a/src/hooks/dhcp/high_availability/ha_service.cc b/src/hooks/dhcp/high_availability/ha_service.cc
new file mode 100644 (file)
index 0000000..02a1c01
--- /dev/null
@@ -0,0 +1,1376 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <config.h>
+
+#include <command_creator.h>
+#include <ha_log.h>
+#include <ha_service.h>
+#include <ha_service_states.h>
+#include <cc/command_interpreter.h>
+#include <cc/data.h>
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <http/date_time.h>
+#include <http/response_json.h>
+#include <http/post_request_json.h>
+#include <util/stopwatch.h>
+#include <boost/pointer_cast.hpp>
+#include <boost/bind.hpp>
+#include <boost/make_shared.hpp>
+#include <boost/weak_ptr.hpp>
+#include <sstream>
+
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::hooks;
+using namespace isc::http;
+using namespace isc::log;
+using namespace isc::util;
+
+namespace isc {
+namespace ha {
+
+const int HAService::HA_HEARTBEAT_COMPLETE_EVT;
+const int HAService::HA_LEASE_UPDATES_COMPLETE_EVT;
+const int HAService::HA_SYNCING_FAILED_EVT;
+const int HAService::HA_SYNCING_SUCCEEDED_EVT;
+
+HAService::HAService(const IOServicePtr& io_service, const NetworkStatePtr& network_state,
+                     const HAConfigPtr& config, const HAServerType& server_type)
+    : io_service_(io_service), network_state_(network_state), config_(config),
+      server_type_(server_type), client_(*io_service), communication_state_(),
+      query_filter_(config), pending_requests_() {
+
+    if (server_type == HAServerType::DHCPv4) {
+        communication_state_.reset(new CommunicationState4(io_service_, config));
+
+    } else {
+        communication_state_.reset(new CommunicationState6(io_service_, config));
+    }
+
+    startModel(HA_WAITING_ST);
+
+    LOG_INFO(ha_logger, HA_SERVICE_STARTED)
+        .arg(HAConfig::HAModeToString(config->getHAMode()))
+        .arg(HAConfig::PeerConfig::roleToString(config->getThisServerConfig()->getRole()));
+}
+
+void
+HAService::defineEvents() {
+    StateModel::defineEvents();
+
+    defineEvent(HA_HEARTBEAT_COMPLETE_EVT, "HA_HEARTBEAT_COMPLETE_EVT");
+    defineEvent(HA_LEASE_UPDATES_COMPLETE_EVT, "HA_LEASE_UPDATES_COMPLETE_EVT");
+    defineEvent(HA_SYNCING_FAILED_EVT, "HA_SYNCING_FAILED_EVT");
+    defineEvent(HA_SYNCING_SUCCEEDED_EVT, "HA_SYNCING_SUCCEEDED_EVT");
+}
+
+void
+HAService::verifyEvents() {
+    StateModel::verifyEvents();
+
+    getEvent(HA_HEARTBEAT_COMPLETE_EVT);
+    getEvent(HA_LEASE_UPDATES_COMPLETE_EVT);
+    getEvent(HA_SYNCING_FAILED_EVT);
+    getEvent(HA_SYNCING_SUCCEEDED_EVT);
+}
+
+void
+HAService::defineStates() {
+    StateModel::defineStates();
+
+    defineState(HA_BACKUP_ST, "backup",
+                boost::bind(&HAService::backupStateHandler, this));
+
+    defineState(HA_HOT_STANDBY_ST, "hot-standby",
+                boost::bind(&HAService::normalStateHandler, this));
+
+    defineState(HA_LOAD_BALANCING_ST, "load-balancing",
+                boost::bind(&HAService::normalStateHandler, this));
+
+    defineState(HA_PARTNER_DOWN_ST, "partner-down",
+                boost::bind(&HAService::partnerDownStateHandler, this));
+
+    defineState(HA_READY_ST, "ready",
+                boost::bind(&HAService::readyStateHandler, this));
+
+    defineState(HA_SYNCING_ST, "syncing",
+                boost::bind(&HAService::syncingStateHandler, this));
+
+    defineState(HA_TERMINATED_ST, "terminated",
+                boost::bind(&HAService::terminatedStateHandler, this));
+
+    defineState(HA_WAITING_ST, "waiting",
+                boost::bind(&HAService::waitingStateHandler, this));
+}
+
+void
+HAService::backupStateHandler() {
+    if (doOnEntry()) {
+        query_filter_.serveNoScopes();
+        adjustNetworkState();
+    }
+
+    // There is nothing to do in that state. This server simply receives
+    // lease updates from the partners.
+    postNextEvent(NOP_EVT);
+}
+
+void
+HAService::normalStateHandler() {
+    // If we are transitioning from another state, we have to define new
+    // serving scopes appropriate for the new state. We don't do it if
+    // we remain in this state.
+    if (doOnEntry()) {
+        query_filter_.serveDefaultScopes();
+        adjustNetworkState();
+    }
+
+    scheduleHeartbeat();
+
+    // Check if the clock skew is still acceptable. If not, transition to
+    // the terminated state.
+    if (shouldTerminate()) {
+        verboseTransition(HA_TERMINATED_ST);
+        return;
+    }
+
+    switch (communication_state_->getPartnerState()) {
+    case HA_PARTNER_DOWN_ST:
+        verboseTransition(HA_WAITING_ST);
+        break;
+
+    case HA_TERMINATED_ST:
+        verboseTransition(HA_TERMINATED_ST);
+        break;
+
+    case HA_UNAVAILABLE_ST:
+        if (shouldPartnerDown()) {
+            verboseTransition(HA_PARTNER_DOWN_ST);
+
+        } else {
+            postNextEvent(NOP_EVT);
+        }
+        break;
+
+    default:
+        postNextEvent(NOP_EVT);
+    }
+}
+
+void
+HAService::partnerDownStateHandler() {
+    // If we are transitioning from another state, we have to define new
+    // serving scopes appropriate for the new state. We don't do it if
+    // we remain in this state.
+    if (doOnEntry()) {
+        // It may be administratively disabled to handle partner's scope
+        // in case of failure. If this is the case we'll just handle our
+        // default scope (or no scope at all). The user will need to
+        // manually enable this server to handle partner's scope.
+        if (config_->getThisServerConfig()->isAutoFailover()) {
+            query_filter_.serveFailoverScopes();
+        } else {
+            query_filter_.serveDefaultScopes();
+        }
+        adjustNetworkState();
+    }
+
+    scheduleHeartbeat();
+
+    // Check if the clock skew is still acceptable. If not, transition to
+    // the terminated state.
+    if (shouldTerminate()) {
+        verboseTransition(HA_TERMINATED_ST);
+        return;
+    }
+
+    switch (communication_state_->getPartnerState()) {
+    case HA_HOT_STANDBY_ST:
+    case HA_LOAD_BALANCING_ST:
+    case HA_PARTNER_DOWN_ST:
+        verboseTransition(HA_WAITING_ST);
+        break;
+
+    case HA_READY_ST:
+        verboseTransition((config_->getHAMode() == HAConfig::LOAD_BALANCING ?
+                    HA_LOAD_BALANCING_ST : HA_HOT_STANDBY_ST));
+        break;
+
+    case HA_TERMINATED_ST:
+        verboseTransition(HA_TERMINATED_ST);
+        break;
+
+    default:
+        postNextEvent(NOP_EVT);
+    }
+}
+
+void
+HAService::readyStateHandler() {
+    // If we are transitioning from another state, we have to define new
+    // serving scopes appropriate for the new state. We don't do it if
+    // we remain in this state.
+    if (doOnEntry()) {
+        query_filter_.serveNoScopes();
+        adjustNetworkState();
+    }
+
+    scheduleHeartbeat();
+
+    // Check if the clock skew is still acceptable. If not, transition to
+    // the terminated state.
+    if (shouldTerminate()) {
+        verboseTransition(HA_TERMINATED_ST);
+        return;
+    }
+
+    switch (communication_state_->getPartnerState()) {
+    case HA_HOT_STANDBY_ST:
+        verboseTransition(HA_HOT_STANDBY_ST);
+        break;
+        
+    case HA_LOAD_BALANCING_ST:
+        verboseTransition(HA_LOAD_BALANCING_ST);
+        break;
+
+    case HA_READY_ST:
+        // If both servers are ready, the primary server "wins" and is
+        // transitioned first.
+        if (config_->getThisServerConfig()->getRole() == HAConfig::PeerConfig::PRIMARY) {
+            verboseTransition((config_->getHAMode() == HAConfig::LOAD_BALANCING ?
+                       HA_LOAD_BALANCING_ST : HA_HOT_STANDBY_ST));
+        } else {
+            postNextEvent(NOP_EVT);
+        }
+        break;
+
+    case HA_TERMINATED_ST:
+        verboseTransition(HA_TERMINATED_ST);
+        break;
+
+    case HA_UNAVAILABLE_ST:
+        if (shouldPartnerDown()) {
+            verboseTransition(HA_PARTNER_DOWN_ST);
+
+        } else {
+            postNextEvent(NOP_EVT);
+        }
+        break;
+
+    default:
+        postNextEvent(NOP_EVT);
+    }
+}
+
+void
+HAService::syncingStateHandler() {
+    // If we are transitioning from another state, we have to define new
+    // serving scopes appropriate for the new state. We don't do it if
+    // we remain in this state.
+    if (doOnEntry()) {
+        query_filter_.serveNoScopes();
+        adjustNetworkState();
+    }
+
+    // Check if the clock skew is still acceptable. If not, transition to
+    // the terminated state.
+    if (shouldTerminate()) {
+        verboseTransition(HA_TERMINATED_ST);
+        return;
+    }
+
+    // We don't want to perform synchronous attempt to synchronize with
+    // a partner until we know that the partner is responding. Therefore,
+    // we wait for the heartbeat to complete successfully before we
+    // initiate the synchronization.
+    switch (communication_state_->getPartnerState()) {
+    case HA_TERMINATED_ST:
+        verboseTransition(HA_TERMINATED_ST);
+        return;
+
+    case HA_UNAVAILABLE_ST:
+        // If the partner appears to be offline, let's transition to the partner
+        // down state. Otherwise, we'd be stuck trying to synchronize with a
+        // dead partner.
+        if (shouldPartnerDown()) {
+            verboseTransition(HA_PARTNER_DOWN_ST);
+
+        } else {
+            postNextEvent(NOP_EVT);
+        }
+        break;
+
+    default:
+        // We don't want the heartbeat to interfere with the synchronization,
+        // so let's temporarily stop it.
+        communication_state_->stopHeartbeat();
+
+        // Perform synchronous leases update.
+        std::string status_message;
+        int sync_status = synchronize(status_message,
+                                      config_->getFailoverPeerConfig()->getName(),
+                                      60);
+
+       // If the leases synchronization was successful, let's transition
+        // to the ready state.
+        if (sync_status == CONTROL_RESULT_SUCCESS) {
+            verboseTransition(HA_READY_ST);
+
+        } else {
+            // If the synchronization was unsuccessful we're back to the
+            // situation that the partner is unavailable and therefore
+            // we stay in the syncing state.
+            postNextEvent(NOP_EVT);
+        }
+    }
+
+    // Make sure that the heartbeat is re-enabled.
+    scheduleHeartbeat();
+}
+
+void
+HAService::terminatedStateHandler() {
+    // If we are transitioning from another state, we have to define new
+    // serving scopes appropriate for the new state. We don't do it if
+    // we remain in this state.
+    if (doOnEntry()) {
+        query_filter_.serveDefaultScopes();
+        adjustNetworkState();
+
+        // In the terminated state we don't send heartbeat.
+        communication_state_->stopHeartbeat();
+
+        LOG_ERROR(ha_logger, HA_TERMINATED);
+    }
+
+    postNextEvent(NOP_EVT);
+}
+
+void
+HAService::waitingStateHandler() {
+    // If we are transitioning from another state, we have to define new
+    // serving scopes appropriate for the new state. We don't do it if
+    // we remain in this state.
+    if (doOnEntry()) {
+        query_filter_.serveNoScopes();
+        adjustNetworkState();
+    }
+
+    // Backup server must remain in its own state.
+    if (config_->getThisServerConfig()->getRole() == HAConfig::PeerConfig::BACKUP) {
+        verboseTransition(HA_BACKUP_ST);
+        return;
+    }
+
+    scheduleHeartbeat();
+
+    // Check if the clock skew is still acceptable. If not, transition to
+    // the terminated state.
+    if (shouldTerminate()) {
+        verboseTransition(HA_TERMINATED_ST);
+        return;
+    }
+
+    switch (communication_state_->getPartnerState()) {
+    case HA_HOT_STANDBY_ST:
+    case HA_LOAD_BALANCING_ST:
+    case HA_PARTNER_DOWN_ST:
+    case HA_READY_ST:
+        // If we're configured to not synchronize lease database, proceed directly
+        // to the "ready" state.
+        verboseTransition(config_->amSyncingLeases() ? HA_SYNCING_ST : HA_READY_ST);
+        break;
+
+    case HA_SYNCING_ST:
+        postNextEvent(NOP_EVT);
+        break;
+
+    case HA_TERMINATED_ST:
+        verboseTransition(HA_TERMINATED_ST);
+        break;
+
+    case HA_WAITING_ST:
+        // If both servers are waiting, the primary server 'wins' and is
+        // transitioned to the next state first.
+        if (config_->getThisServerConfig()->getRole() == HAConfig::PeerConfig::PRIMARY) {
+            // If we're configured to not synchronize lease database, proceed directly
+            // to the "ready" state.
+            verboseTransition(config_->amSyncingLeases() ? HA_SYNCING_ST : HA_READY_ST);
+
+        } else {
+            postNextEvent(NOP_EVT);
+        }
+        break;
+
+    case HA_UNAVAILABLE_ST:
+        if (shouldPartnerDown()) {
+            verboseTransition(HA_PARTNER_DOWN_ST);
+
+        } else {
+            postNextEvent(NOP_EVT);
+        }
+        break;
+
+    default:
+        postNextEvent(NOP_EVT);
+    }
+}
+
+void
+HAService::verboseTransition(const unsigned state) {
+    auto partner_state = communication_state_->getPartnerState();
+
+    // Get current and new state name.
+    std::string current_state_name = getStateLabel(getCurrState());
+    std::string new_state_name = getStateLabel(state);
+    std::string partner_state_name = getStateLabel(partner_state);
+
+    // Turn them to upper case so as they are better visible in the logs.
+    boost::to_upper(current_state_name);
+    boost::to_upper(new_state_name);
+    boost::to_upper(partner_state_name);
+
+    // Log the transition.
+    LOG_INFO(ha_logger, HA_STATE_TRANSITION)
+        .arg(current_state_name)
+        .arg(new_state_name)
+        .arg(partner_state_name);
+
+    // If we're transitioning directly from the "waiting" to "ready"
+    // state it indicates that the database synchronization is
+    // administratively disabled. Let's remind the user about this
+    // configuration setting.
+    if ((state == HA_READY_ST) && (getCurrState() == HA_WAITING_ST)) {
+        LOG_INFO(ha_logger, HA_CONFIG_LEASE_SYNCING_DISABLED_REMINDER);
+    }
+
+    // Do the actual transition.
+    transition(state, getNextEvent());
+
+    // Inform the administrator whether or not lease updates are generated.
+    // Updates are never generated by a backup server so it doesn't make
+    // sense to log anything for the backup server.
+    if (config_->getThisServerConfig()->getRole() != HAConfig::PeerConfig::BACKUP) {
+        if (shouldSendLeaseUpdates(config_->getFailoverPeerConfig())) {
+            LOG_INFO(ha_logger, HA_LEASE_UPDATES_ENABLED)
+                .arg(new_state_name);
+
+        } else if (!config_->amSendingLeaseUpdates()) {
+            // Lease updates are administratively disabled.
+            LOG_INFO(ha_logger, HA_CONFIG_LEASE_UPDATES_DISABLED_REMINDER)
+                .arg(new_state_name);
+
+        } else {
+            // Lease updates are not administratively disabled, but they
+            // are not issued because this is the backup server or because
+            // in this state the server should not generate lease updates.
+            LOG_INFO(ha_logger, HA_LEASE_UPDATES_DISABLED)
+                .arg(new_state_name);
+        }
+    }
+}
+
+void
+HAService::serveDefaultScopes() {
+    query_filter_.serveDefaultScopes();
+}
+
+bool
+HAService::inScope(dhcp::Pkt4Ptr& query4) {
+    return (inScopeInternal(query4));
+}
+
+bool
+HAService::inScope(dhcp::Pkt6Ptr& query6) {
+    return (inScopeInternal(query6));
+}
+
+template<typename QueryPtrType>
+bool
+HAService::inScopeInternal(QueryPtrType& query) {
+    // Check if the query is in scope (should be processed by this server).
+    std::string scope_class;
+    const bool in_scope = query_filter_.inScope(query, scope_class);
+    // Whether or not the query is going to be processed by this server,
+    // we associate the query with the appropriate class.
+    query->addClass(dhcp::ClientClass(scope_class));
+    // The following is the part of the server failure detection algorithm.
+    // If the query should be processed by the partner we need to check if
+    // the partner responds. If the number of unanswered queries exceeds a
+    // configured threshold, we will consider the partner to be offline.
+    if (!in_scope && communication_state_->isCommunicationInterrupted()) {
+        communication_state_->analyzeMessage(query);
+    }
+    // Indicate if the query is in scope.
+    return (in_scope);
+}
+
+void
+HAService::adjustNetworkState() {
+    std::string current_state_name = getStateLabel(getCurrState());
+    boost::to_upper(current_state_name);
+
+    // If the server is serving no scopes, it means that we're in the state
+    // in which DHCP service should be disabled.
+    if (query_filter_.getServedScopes().empty() &&
+        network_state_->isServiceEnabled()) {
+        std::string current_state_name = getStateLabel(getCurrState());
+        boost::to_upper(current_state_name);
+        LOG_INFO(ha_logger, HA_LOCAL_DHCP_DISABLE)
+            .arg(config_->getThisServerName())
+            .arg(current_state_name);
+        network_state_->disableService();
+
+    } else if (!query_filter_.getServedScopes().empty() &&
+               !network_state_->isServiceEnabled()) {
+        std::string current_state_name = getStateLabel(getCurrState());
+        boost::to_upper(current_state_name);
+        LOG_INFO(ha_logger, HA_LOCAL_DHCP_ENABLE)
+            .arg(config_->getThisServerName())
+            .arg(current_state_name);
+        network_state_->enableService();
+    }
+}
+
+bool
+HAService::shouldPartnerDown() const {
+    // Checking whether the communication with the partner is ok is the
+    // first step towards verifying if the server is up.
+    if (communication_state_->isCommunicationInterrupted()) {
+        // If the communication is interrupted, we also have to check
+        // whether the partner answers DHCP requests. The only cases
+        // when we don't (can't) do it are: the hot standby configuration
+        // in which this server is a primary and when the DHCP service is
+        // disabled so we can't analyze incoming traffic. Note that the
+        // primary server can't check delayed responses to the partner
+        // because the partner doesn't respond to any queries in this
+        // configuration.
+        if (network_state_->isServiceEnabled() &&
+            ((config_->getHAMode() == HAConfig::LOAD_BALANCING) ||
+             (config_->getThisServerConfig()->getRole() == HAConfig::PeerConfig::STANDBY))) {
+            return (communication_state_->failureDetected());
+        }
+
+        // Hot standby / primary case.
+        return (true);
+    }
+
+    // Shouldn't transition to the partner down state.
+    return (false);
+}
+
+bool
+HAService::shouldTerminate() const {
+    if (communication_state_->clockSkewShouldTerminate()) {
+        LOG_ERROR(ha_logger, HA_HIGH_CLOCK_SKEW_CAUSES_TERMINATION)
+            .arg(communication_state_->logFormatClockSkew());
+        return (true);
+
+    } else if (communication_state_->clockSkewShouldWarn()) {
+        LOG_WARN(ha_logger, HA_HIGH_CLOCK_SKEW)
+            .arg(communication_state_->logFormatClockSkew());
+    }
+
+    return (false);
+}
+
+size_t
+HAService::asyncSendLeaseUpdates(const dhcp::Pkt4Ptr& query,
+                                 const dhcp::Lease4CollectionPtr& leases,
+                                 const dhcp::Lease4CollectionPtr& deleted_leases,
+                                 const hooks::ParkingLotHandlePtr& parking_lot) {
+
+    // Get configurations of the peers. Exclude this instance.
+    HAConfig::PeerConfigMap peers_configs = config_->getOtherServersConfig();
+
+    size_t sent_num = 0;
+
+    // Schedule sending lease updates to each peer.
+    for (auto p = peers_configs.begin(); p != peers_configs.end(); ++p) {
+        HAConfig::PeerConfigPtr conf = p->second;
+
+        // Check if the lease update should be sent to the server. If we're in
+        // the partner-down state we don't send lease updates to the partner.
+        if (!shouldSendLeaseUpdates(conf)) {
+            continue;
+        }
+
+        // Count contacted servers.
+        ++sent_num;
+
+        // Lease updates for deleted leases.
+        for (auto l = deleted_leases->begin(); l != deleted_leases->end(); ++l) {
+            asyncSendLeaseUpdate(query, conf, CommandCreator::createLease4Delete(**l),
+                                 parking_lot);
+        }
+
+        // Lease updates for new allocations and updated leases.
+        for (auto l = leases->begin(); l != leases->end(); ++l) {
+            asyncSendLeaseUpdate(query, conf, CommandCreator::createLease4Update(**l),
+                                 parking_lot);
+        }
+    }
+
+    return (sent_num);
+}
+
+size_t
+HAService::asyncSendLeaseUpdates(const dhcp::Pkt6Ptr& query,
+                                 const dhcp::Lease6CollectionPtr& leases,
+                                 const dhcp::Lease6CollectionPtr& deleted_leases,
+                                 const hooks::ParkingLotHandlePtr& parking_lot) {
+
+    // Get configurations of the peers. Exclude this instance.
+    HAConfig::PeerConfigMap peers_configs = config_->getOtherServersConfig();
+
+    size_t sent_num = 0;
+
+    // Schedule sending lease updates to each peer.
+    for (auto p = peers_configs.begin(); p != peers_configs.end(); ++p) {
+        HAConfig::PeerConfigPtr conf = p->second;
+
+        // Check if the lease update should be sent to the server. If we're in
+        // the partner-down state we don't send lease updates to the partner.
+        if (!shouldSendLeaseUpdates(conf)) {
+            continue;
+        }
+
+        // Count contacted servers.
+        ++sent_num;
+
+        // Lease updates for deleted leases.
+        for (auto l = deleted_leases->begin(); l != deleted_leases->end(); ++l) {
+            asyncSendLeaseUpdate(query, conf, CommandCreator::createLease6Delete(**l),
+                                 parking_lot);
+        }
+
+        // Lease updates for new allocations and updated leases.
+        for (auto l = leases->begin(); l != leases->end(); ++l) {
+            asyncSendLeaseUpdate(query, conf, CommandCreator::createLease6Update(**l),
+                                 parking_lot);
+        }
+    }
+
+    return (sent_num);
+}
+
+template<typename QueryPtrType>
+void
+HAService::asyncSendLeaseUpdate(const QueryPtrType& query,
+                          const HAConfig::PeerConfigPtr& config,
+                          const ConstElementPtr& command,
+                          const ParkingLotHandlePtr& parking_lot) {
+    // Create HTTP/1.1 request including our command.
+    PostHttpRequestJsonPtr request = boost::make_shared<PostHttpRequestJson>
+        (HttpRequest::Method::HTTP_POST, "/", HttpVersion::HTTP_11());
+    request->setBodyAsJson(command);
+    request->finalize();
+
+    // Response object should also be created because the HTTP client needs
+    // to know the type of the expected response.
+    HttpResponseJsonPtr response = boost::make_shared<HttpResponseJson>();
+
+    // When possible we prefer to pass weak pointers to the queries, rather
+    // than shared pointers, to avoid memory leaks in case cross reference
+    // between the pointers.
+    boost::weak_ptr<typename QueryPtrType::element_type> weak_query(query);
+
+    // Schedule asynchronous HTTP request.
+    client_.asyncSendRequest(config->getUrl(), request, response,
+        [this, weak_query, parking_lot, config]
+            (const boost::system::error_code& ec,
+             const HttpResponsePtr& response,
+             const std::string& error_str) {
+            // Get the shared pointer of the query. The server should keep the
+            // pointer to the query and then park it. Therefore, we don't really
+            // expect it to be null. If it is null, something is really wrong.
+            QueryPtrType query = weak_query.lock();
+            if (!query) {
+                isc_throw(Unexpected, "query is null while receiving response from"
+                          " HA peer. This is programmatic error");
+            }
+
+            // There are three possible groups of errors during the lease update.
+            // One is the IO error causing issues in communication with the peer.
+            // Another one is an HTTP parsing error. The last type of error is
+            // when non-success error code is returned in the response carried
+            // in the HTTP message or if the JSON response is otherwise broken.
+
+            bool lease_update_success = true;
+
+            // Handle first two groups of errors.
+            if (ec || !error_str.empty()) {
+                LOG_WARN(ha_logger, HA_LEASE_UPDATE_COMMUNICATIONS_FAILED)
+                    .arg(query->getLabel())
+                    .arg(config->getLogLabel())
+                    .arg(ec ? ec.message() : error_str);
+
+                // Communication error, so let's drop parked packet. The DHCP
+                // response will not be sent.
+                lease_update_success = false;
+
+            } else {
+
+                // Handle third group of errors.
+                try {
+                    verifyAsyncResponse(response);
+
+                } catch (const std::exception& ex) {
+                    LOG_WARN(ha_logger, HA_LEASE_UPDATE_FAILED)
+                        .arg(query->getLabel())
+                        .arg(config->getLogLabel())
+                        .arg(ex.what());
+
+                    // Error while doing an update. The DHCP response will not be sent.
+                    lease_update_success = false;
+                }
+            }
+
+            // We don't care about the result of the lease update to the backup server.
+            // It is a best effort update.
+            if (config->getRole() != HAConfig::PeerConfig::BACKUP) {
+                if (lease_update_success) {
+                    // If the lease update was successful and we have sent it to the server
+                    // to which we also send heartbeats (primary, secondary or standby) we
+                    // can assume that the server is online and we can defer next heartbeat.
+                    communication_state_->poke();
+
+                } else {
+                    // Lease update was unsuccessful, so drop the parked DHCP packet.
+                    parking_lot->drop(query);
+                    communication_state_->setPartnerState("unavailable");
+                }
+            }
+
+            auto it = pending_requests_.find(query);
+
+            // If there are no more pending requests for this query, let's unpark
+            // the DHCP packet.
+            if (it == pending_requests_.end() || (--pending_requests_[query] <= 0)) {
+                parking_lot->unpark(query);
+
+                // If we have unparked the packet we can clear pending requests for
+                // this query.
+                if (it != pending_requests_.end()) {
+                    pending_requests_.erase(it);
+                }
+
+                // If we have finished sending the lease updates we need to run the
+                // state machine until the state machine finds that additional events
+                // are required, such as next heartbeat or a lease update. The runModel()
+                // may transition to another state, schedule asynchronous tasks etc.
+                // Then it returns control to the DHCP server.
+                runModel(HA_LEASE_UPDATES_COMPLETE_EVT);
+            }
+        });
+
+    // Request scheduled, so update the request counters for the query.
+    if (pending_requests_.count(query) == 0) {
+        pending_requests_[query] = 1;
+
+    } else {
+        ++pending_requests_[query];
+    }
+}
+
+bool
+HAService::shouldSendLeaseUpdates(const HAConfig::PeerConfigPtr& peer_config) const {
+    // Never send lease updates if they are administratively disabled.
+    if (!config_->amSendingLeaseUpdates()) {
+        return (false);
+    }
+
+    // Always send updates to the backup server.
+    if (peer_config->getRole() == HAConfig::PeerConfig::BACKUP) {
+        return (true);
+    }
+
+    // Never send updates if this is a backup server.
+    if (config_->getThisServerConfig()->getRole() == HAConfig::PeerConfig::BACKUP) {
+        return (false);
+    }
+
+    // In other case, whether we send lease updates or not depends on our
+    // state.
+    switch (getCurrState()) {
+    case HA_HOT_STANDBY_ST:
+    case HA_LOAD_BALANCING_ST:
+        return (true);
+
+    default:
+        ;
+    }
+
+    return (false);
+}
+
+ConstElementPtr
+HAService::processHeartbeat() {
+    ElementPtr arguments = Element::createMap();
+    std::string state_label = getState(getCurrState())->getLabel();
+    arguments->set("state", Element::create(state_label));
+
+    std::string date_time = HttpDateTime().rfc1123Format();
+    arguments->set("date-time", Element::create(date_time));
+
+    return (createAnswer(CONTROL_RESULT_SUCCESS, "HA peer status returned.",
+                         arguments));
+}
+
+void
+HAService::asyncSendHeartbeat() {
+    HAConfig::PeerConfigPtr partner_config = config_->getFailoverPeerConfig();
+
+    // Create HTTP/1.1 request including our command.
+    PostHttpRequestJsonPtr request = boost::make_shared<PostHttpRequestJson>
+        (HttpRequest::Method::HTTP_POST, "/", HttpVersion::HTTP_11());
+    request->setBodyAsJson(CommandCreator::createHeartbeat(server_type_));
+    request->finalize();
+
+    // Response object should also be created because the HTTP client needs
+    // to know the type of the expected response.
+    HttpResponseJsonPtr response = boost::make_shared<HttpResponseJson>();
+
+    // Schedule asynchronous HTTP request.
+    client_.asyncSendRequest(partner_config->getUrl(), request, response,
+        [this, partner_config]
+            (const boost::system::error_code& ec,
+             const HttpResponsePtr& response,
+             const std::string& error_str) {
+
+            // There are three possible groups of errors during the heartneat.
+            // One is the IO error causing issues in communication with the peer.
+            // Another one is an HTTP parsing error. The last type of error is
+            // when non-success error code is returned in the response carried
+            // in the HTTP message or if the JSON response is otherwise broken.
+
+            bool heartbeat_success = true;
+
+            // Handle first two groups of errors.
+            if (ec || !error_str.empty()) {
+                LOG_WARN(ha_logger, HA_HEARTBEAT_COMMUNICATIONS_FAILED)
+                    .arg(partner_config->getLogLabel())
+                    .arg(ec ? ec.message() : error_str);
+                heartbeat_success = false;
+
+            } else {
+
+                // Handle third group of errors.
+                try {
+                    // Response must contain arguments and the arguments must
+                    // be a map.
+                    ConstElementPtr args = verifyAsyncResponse(response);
+                    if (!args || args->getType() != Element::map) {
+                        isc_throw(CtrlChannelError, "returned arguments in the response"
+                                  " must be a map");
+                    }
+                    // Response must include partner's state.
+                    ConstElementPtr state = args->get("state");
+                    if (!state || state->getType() != Element::string) {
+                        isc_throw(CtrlChannelError, "server state not returned in response"
+                                  " to a ha-heartbeat command or it is not a string");
+                    }
+                    // Remember the partner's state. This may throw if the returned
+                    // state is invalid.
+                    communication_state_->setPartnerState(state->stringValue());
+
+                    ConstElementPtr date_time = args->get("date-time");
+                    if (!date_time || date_time->getType() != Element::string) {
+                        isc_throw(CtrlChannelError, "date-time not returned in response"
+                                  " to a ha-heartbeat command or it is not a string");
+                    }
+                    // Note the time returned by the partner to calculate the clock skew.
+                    communication_state_->setPartnerTime(date_time->stringValue());
+
+                } catch (const std::exception& ex) {
+                    LOG_WARN(ha_logger, HA_HEARTBEAT_FAILED)
+                        .arg(partner_config->getLogLabel())
+                        .arg(ex.what());
+                    heartbeat_success = false;
+                }
+            }
+
+            // If heartbeat was successful, let's mark the connection with the
+            // peer as healthy.
+            if (heartbeat_success) {
+                communication_state_->poke();
+
+            } else {
+                // We were unable to retrieve partner's state, so let's mark it
+                // as unavailable.
+                communication_state_->setPartnerState("unavailable");
+            }
+
+            // Whatever the result of the heartbeat was, the state machine needs
+            // to react to this. Let's run the state machine until the state machine
+            // finds that some new events are required, i.e. next heartbeat or
+            // lease update.  The runModel() may transition to another state, schedule
+            // asynchronous tasks etc. Then it returns control to the DHCP server.
+            startHeartbeat();
+            runModel(HA_HEARTBEAT_COMPLETE_EVT);
+      });
+}
+
+void
+HAService::scheduleHeartbeat() {
+    if (!communication_state_->isHeartbeatRunning()) {
+        startHeartbeat();
+    }
+}
+
+void
+HAService::startHeartbeat() {
+    if (config_->getHeartbeatDelay() > 0) {
+        communication_state_->startHeartbeat(config_->getHeartbeatDelay(),
+                                             boost::bind(&HAService::asyncSendHeartbeat,
+                                                         this));
+    }
+}
+
+void
+HAService::asyncDisable(HttpClient& http_client,
+                        const std::string& server_name,
+                        const unsigned int max_period,
+                        const PostRequestCallback& post_request_action) {
+    HAConfig::PeerConfigPtr remote_config = config_->getPeerConfig(server_name);
+
+    // Create HTTP/1.1 request including our command.
+    PostHttpRequestJsonPtr request = boost::make_shared<PostHttpRequestJson>
+        (HttpRequest::Method::HTTP_POST, "/", HttpVersion::HTTP_11());
+    request->setBodyAsJson(CommandCreator::createDHCPDisable(max_period, server_type_));
+    request->finalize();
+
+    // Response object should also be created because the HTTP client needs
+    // to know the type of the expected response.
+    HttpResponseJsonPtr response = boost::make_shared<HttpResponseJson>();
+
+    // Schedule asynchronous HTTP request.
+    http_client.asyncSendRequest(remote_config->getUrl(), request, response,
+        [this, remote_config, post_request_action]
+            (const boost::system::error_code& ec,
+             const HttpResponsePtr& response,
+             const std::string& error_str) {
+
+             // There are three possible groups of errors during the heartneat.
+             // One is the IO error causing issues in communication with the peer.
+             // Another one is an HTTP parsing error. The last type of error is
+             // when non-success error code is returned in the response carried
+             // in the HTTP message or if the JSON response is otherwise broken.
+
+             std::string error_message;
+
+             // Handle first two groups of errors.
+             if (ec || !error_str.empty()) {
+                 error_message = (ec ? ec.message() : error_str);
+                 LOG_ERROR(ha_logger, HA_DHCP_DISABLE_COMMUNICATIONS_FAILED)
+                     .arg(remote_config->getLogLabel())
+                     .arg(error_message);
+
+             } else {
+
+                 // Handle third group of errors.
+                 try {
+                     static_cast<void>(verifyAsyncResponse(response));
+
+                 } catch (const std::exception& ex) {
+                     error_message = ex.what();
+                     LOG_ERROR(ha_logger, HA_DHCP_DISABLE_FAILED)
+                         .arg(remote_config->getLogLabel())
+                         .arg(error_message);
+                 }
+             }
+
+             // If there was an error communicating with the partner, mark the
+             // partner as unavailable.
+             if (!error_message.empty()) {
+                 communication_state_->setPartnerState("unavailable");
+             }
+
+             // Invoke post request action if it was specified.
+             if (post_request_action) {
+                 post_request_action(error_message.empty(),
+                                     error_message);
+             }
+    });
+}
+
+void
+HAService::asyncEnable(HttpClient& http_client,
+                       const std::string& server_name,
+                       const PostRequestCallback& post_request_action) {
+    HAConfig::PeerConfigPtr remote_config = config_->getPeerConfig(server_name);
+
+    // Create HTTP/1.1 request including our command.
+    PostHttpRequestJsonPtr request = boost::make_shared<PostHttpRequestJson>
+        (HttpRequest::Method::HTTP_POST, "/", HttpVersion::HTTP_11());
+    request->setBodyAsJson(CommandCreator::createDHCPEnable(server_type_));
+    request->finalize();
+
+    // Response object should also be created because the HTTP client needs
+    // to know the type of the expected response.
+    HttpResponseJsonPtr response = boost::make_shared<HttpResponseJson>();
+
+    // Schedule asynchronous HTTP request.
+    http_client.asyncSendRequest(remote_config->getUrl(), request, response,
+        [this, remote_config, post_request_action]
+            (const boost::system::error_code& ec,
+             const HttpResponsePtr& response,
+             const std::string& error_str) {
+
+             // There are three possible groups of errors during the heartneat.
+             // One is the IO error causing issues in communication with the peer.
+             // Another one is an HTTP parsing error. The last type of error is
+             // when non-success error code is returned in the response carried
+             // in the HTTP message or if the JSON response is otherwise broken.
+
+             std::string error_message;
+
+             // Handle first two groups of errors.
+             if (ec || !error_str.empty()) {
+                 error_message = (ec ? ec.message() : error_str);
+                 LOG_ERROR(ha_logger, HA_DHCP_ENABLE_COMMUNICATIONS_FAILED)
+                     .arg(remote_config->getLogLabel())
+                     .arg(error_message);
+
+             } else {
+
+                 // Handle third group of errors.
+                 try {
+                     static_cast<void>(verifyAsyncResponse(response));
+
+                 } catch (const std::exception& ex) {
+                     error_message = ex.what();
+                     LOG_ERROR(ha_logger, HA_DHCP_ENABLE_FAILED)
+                         .arg(remote_config->getLogLabel())
+                         .arg(error_message);
+                 }
+             }
+
+             // If there was an error communicating with the partner, mark the
+             // partner as unavailable.
+             if (!error_message.empty()) {
+                 communication_state_->setPartnerState("unavailable");
+             }
+
+             // Invoke post request action if it was specified.
+             if (post_request_action) {
+                 post_request_action(error_message.empty(),
+                                     error_message);
+             }
+    });
+}
+
+void
+HAService::localDisable() {
+    network_state_->disableService();
+}
+
+void
+HAService::localEnable() {
+    network_state_->enableService();
+}
+
+void
+HAService::asyncSyncLeases() {
+    PostRequestCallback null_action;
+    asyncSyncLeases(client_, null_action);
+}
+
+void
+HAService::asyncSyncLeases(http::HttpClient& http_client,
+                           const PostRequestCallback& post_sync_action) {
+    HAConfig::PeerConfigPtr partner_config = config_->getFailoverPeerConfig();
+
+    // Create HTTP/1.1 request including our command.
+    PostHttpRequestJsonPtr request = boost::make_shared<PostHttpRequestJson>
+        (HttpRequest::Method::HTTP_POST, "/", HttpVersion::HTTP_11());
+    if (server_type_ == HAServerType::DHCPv4) {
+        request->setBodyAsJson(CommandCreator::createLease4GetAll());
+
+    } else {
+        request->setBodyAsJson(CommandCreator::createLease6GetAll());
+    }
+    request->finalize();
+
+    // Response object should also be created because the HTTP client needs
+    // to know the type of the expected response.
+    HttpResponseJsonPtr response = boost::make_shared<HttpResponseJson>();
+
+    // Schedule asynchronous HTTP request.
+    http_client.asyncSendRequest(partner_config->getUrl(), request, response,
+        [this, partner_config, post_sync_action]
+            (const boost::system::error_code& ec,
+             const HttpResponsePtr& response,
+             const std::string& error_str) {
+
+            // There are three possible groups of errors during the heartneat.
+            // One is the IO error causing issues in communication with the peer.
+            // Another one is an HTTP parsing error. The last type of error is
+            // when non-success error code is returned in the response carried
+            // in the HTTP message or if the JSON response is otherwise broken.
+
+            std::string error_message;
+
+            // Handle first two groups of errors.
+            if (ec || !error_str.empty()) {
+                error_message = (ec ? ec.message() : error_str);
+                LOG_ERROR(ha_logger, HA_LEASES_SYNC_COMMUNICATIONS_FAILED)
+                    .arg(partner_config->getLogLabel())
+                    .arg(error_message);
+
+            } else {
+                // Handle third group of errors.
+                try {
+                    ConstElementPtr args = verifyAsyncResponse(response);
+
+                    // Arguments must be a map.
+                    if (args && (args->getType() != Element::map)) {
+                        isc_throw(CtrlChannelError,
+                                  "arguments in the received response must be a map");
+                    }
+
+                    ConstElementPtr leases = args->get("leases");
+                    if (!leases || (leases->getType() != Element::list)) {
+                        isc_throw(CtrlChannelError,
+                                  "server response does not contain leases argument or this"
+                                  " argument is not a list");
+                    }
+
+                    // Iterate over the leases and update the database as appropriate.
+                    const auto& leases_element = leases->listValue();
+                    for (auto l = leases_element.begin(); l != leases_element.end(); ++l) {
+                        try {
+                            if (server_type_ == HAServerType::DHCPv4) {
+                                Lease4Ptr lease = Lease4::fromElement(*l);
+
+                                // Check if there is such lease in the database already.
+                                Lease4Ptr existing_lease = LeaseMgrFactory::instance().getLease4(lease->addr_);
+                                if (!existing_lease) {
+                                    // There is no such lease, so let's add it.
+                                    LeaseMgrFactory::instance().addLease(lease);
+
+                                } else if (existing_lease->cltt_ < lease->cltt_) {
+                                    // If the existing lease is older than the fetched lease, update
+                                    // the lease in our local database.
+                                    LeaseMgrFactory::instance().updateLease4(lease);
+
+                                } else {
+                                    LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_LEASE_SYNC_STALE_LEASE4_SKIP)
+                                        .arg(lease->addr_.toText())
+                                        .arg(lease->subnet_id_);
+                                }
+
+                            } else {
+                                Lease6Ptr lease = Lease6::fromElement(*l);
+
+                                // Check if there is such lease in the database already.
+                                Lease6Ptr existing_lease = LeaseMgrFactory::instance().getLease6(lease->type_,
+                                                                                                 lease->addr_);
+                                if (!existing_lease) {
+                                    // There is no such lease, so let's add it.
+                                    LeaseMgrFactory::instance().addLease(lease);
+
+                                } else if (existing_lease->cltt_ < lease->cltt_) {
+                                    // If the existing lease is older than the fetched lease, update
+                                    // the lease in our local database.
+                                    LeaseMgrFactory::instance().updateLease6(lease);
+
+                                } else {
+                                    LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_LEASE_SYNC_STALE_LEASE6_SKIP)
+                                        .arg(lease->addr_.toText())
+                                        .arg(lease->subnet_id_);
+                                }
+                            }
+
+                        } catch (const std::exception& ex) {
+                            LOG_WARN(ha_logger, HA_LEASE_SYNC_FAILED)
+                                .arg((*l)->str())
+                                .arg(ex.what());
+                        }
+                    }
+
+                } catch (const std::exception& ex) {
+                    error_message = ex.what();
+                    LOG_ERROR(ha_logger, HA_LEASES_SYNC_FAILED)
+                        .arg(partner_config->getLogLabel())
+                        .arg(error_message);
+                }
+            }
+
+             // If there was an error communicating with the partner, mark the
+             // partner as unavailable.
+             if (!error_message.empty()) {
+                 communication_state_->setPartnerState("unavailable");
+             }
+
+            // Invoke post synchronization action if it was specified.
+            if (post_sync_action) {
+                post_sync_action(error_message.empty(),
+                                 error_message);
+            }
+    });
+}
+
+ConstElementPtr
+HAService::processSynchronize(const std::string& server_name,
+                              const unsigned int max_period) {
+    std::string answer_message;
+    int sync_status = synchronize(answer_message, server_name, max_period);
+    return (createAnswer(sync_status, answer_message));
+}
+
+int
+HAService::synchronize(std::string& status_message, const std::string& server_name,
+                       const unsigned int max_period) {
+    IOService io_service;
+    HttpClient client(io_service);
+
+    // Synchronization starts with a command to disable DHCP service of the
+    // peer from which we're fetching leases. We don't want the other server
+    // to allocate new leases while we fetch from it. The DHCP service will
+    // be disabled for a certain amount of time and will be automatically
+    // re-enabled if we die during the synchronization.
+    asyncDisable(client, server_name, max_period,
+                 [&](const bool success, const std::string& error_message) {
+        // If we have successfully disabled the DHCP service on the peer,
+        // we can start fetching the leases.
+        if (success) {
+            asyncSyncLeases(client, [&](const bool success,
+                                        const std::string& error_message) {
+                // If there was a fatal error while fetching the leases, let's
+                // log an error message so as it can be included in the response
+                // to the controlling client.
+                if (!success) {
+                    status_message = error_message;
+                }
+
+                // Whether or not there was an error while fetching the leases,
+                // we need to re-enable the DHCP service on the peer.
+                asyncEnable(client, server_name,
+                            [&](const bool success,
+                                const std::string& error_message) {
+                    // It is possible that we have already recorded an error
+                    // message while synchronizing the lease database. Don't
+                    // override the existing error message.
+                    if (!success && status_message.empty()) {
+                        status_message = error_message;
+                    }
+                    // The synchronization process is completed, so let's break
+                    // the IO service so as we can return the response to the
+                    // controlling client.
+                    io_service.stop();
+                });
+            });
+
+        } else {
+            // We have failed to disable the DHCP service of the peer. Let's
+            // record the error message and break the IO service so as we can
+            // return the response to the controlling client.
+            status_message = error_message;
+            io_service.stop();
+        }
+    });
+
+    LOG_INFO(ha_logger, HA_SYNC_START).arg(server_name);
+
+    // Measure duration of the synchronization.
+    Stopwatch stopwatch;
+
+    // Run the IO service until it is stopped by any of the callbacks. This
+    // makes it synchronous.
+    io_service.run();
+
+    // End measuring duration.
+    stopwatch.stop();
+
+    // If an error message has been recorded, return an error to the controlling
+    // client.
+    if (!status_message.empty()) {
+        postNextEvent(HA_SYNCING_FAILED_EVT);
+
+        LOG_ERROR(ha_logger, HA_SYNC_FAILED)
+            .arg(server_name)
+            .arg(status_message);
+
+        return (CONTROL_RESULT_ERROR);
+
+    }
+
+    // Everything was fine, so let's return a success.
+    status_message = "Lease database synchronization complete.";
+    postNextEvent(HA_SYNCING_SUCCEEDED_EVT);
+
+    LOG_INFO(ha_logger, HA_SYNC_SUCCESSFUL)
+        .arg(server_name)
+        .arg(stopwatch.logFormatLastDuration());
+
+    return (CONTROL_RESULT_SUCCESS);
+}
+
+
+ConstElementPtr
+HAService::processScopes(const std::vector<std::string>& scopes) {
+    try {
+        query_filter_.serveScopes(scopes);
+        adjustNetworkState();
+
+    } catch (const std::exception& ex) {
+        return (createAnswer(CONTROL_RESULT_ERROR, ex.what()));
+    }
+
+    return (createAnswer(CONTROL_RESULT_SUCCESS, "New HA scopes configured."));
+}
+
+ConstElementPtr
+HAService::verifyAsyncResponse(const HttpResponsePtr& response) {
+    // The response must cast to JSON type.
+    HttpResponseJsonPtr json_response =
+        boost::dynamic_pointer_cast<HttpResponseJson>(response);
+    if (!json_response) {
+        isc_throw(CtrlChannelError, "no valid HTTP response found");
+    }
+
+    // Body holds the response to our command.
+    ConstElementPtr body = json_response->getBodyAsJson();
+    if (!body) {
+        isc_throw(CtrlChannelError, "no body found in the response");
+    }
+
+    // Body must contain a list of responses form multiple servers.
+    if (body->getType() != Element::list) {
+        isc_throw(CtrlChannelError, "body of the response must be a list");
+    }
+
+    // There must be at least one response.
+    if (body->empty()) {
+        isc_throw(CtrlChannelError, "list of responses must not be empty");
+    }
+
+    // Check if the status code of the first response. We don't support multiple
+    // at this time, because we always send a request to a single location.
+    int rcode = 0;
+    ConstElementPtr args = parseAnswer(rcode, body->get(0));
+    if ((rcode != CONTROL_RESULT_SUCCESS) &&
+        (rcode != CONTROL_RESULT_EMPTY)) {
+        std::ostringstream s;
+        // Include an error text if available.
+        if (args && args->getType() == Element::string) {
+            s << args->stringValue() << ", ";
+        }
+        // Include an error code.
+        s << "error code " << rcode;
+        isc_throw(CtrlChannelError, s.str());
+    }
+
+    return (args);
+}
+
+
+} // end of namespace isc::ha
+} // end of namespace isc
diff --git a/src/hooks/dhcp/high_availability/ha_service.h b/src/hooks/dhcp/high_availability/ha_service.h
new file mode 100644 (file)
index 0000000..8818ff3
--- /dev/null
@@ -0,0 +1,654 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#ifndef HA_SERVICE_H
+#define HA_SERVICE_H
+
+#include <communication_state.h>
+#include <ha_config.h>
+#include <ha_server_type.h>
+#include <query_filter.h>
+#include <asiolink/io_service.h>
+#include <cc/data.h>
+#include <dhcp/pkt4.h>
+#include <http/response.h>
+#include <dhcp/pkt4.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/network_state.h>
+#include <hooks/parking_lots.h>
+#include <http/client.h>
+#include <util/state_model.h>
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+#include <functional>
+#include <map>
+#include <vector>
+
+namespace isc {
+namespace ha {
+
+/// @brief High availability service.
+///
+/// This class derives from the @c util::StateModel and implements a
+/// state machine for the high availability service in the Kea DHCP
+/// server instance.
+class HAService : public boost::noncopyable, public util::StateModel {
+public:
+
+    /// Finished heartbeat commannd.
+    static const int HA_HEARTBEAT_COMPLETE_EVT = SM_DERIVED_EVENT_MIN + 1;
+
+    /// Finished lease updates commands.
+    static const int HA_LEASE_UPDATES_COMPLETE_EVT = SM_DERIVED_EVENT_MIN + 2;
+
+    /// Lease database synchronization failed.
+    static const int HA_SYNCING_FAILED_EVT = SM_DERIVED_EVENT_MIN + 3;
+
+    /// Lease database synchroniation succeeded.
+    static const int HA_SYNCING_SUCCEEDED_EVT = SM_DERIVED_EVENT_MIN + 4;
+
+protected:
+
+    /// @brief Callback invoked when request was sent and a response received
+    /// or an error occurred.
+    ///
+    /// The first arguments indicates if the operation passed (when true).
+    /// The second argument holds error message.
+    typedef std::function<void(const bool, const std::string&)> PostRequestCallback;
+
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param io_service Pointer to the IO service used by the DHCP server.
+    /// @param config Parsed HA hook library configuration.
+    /// @param network_state Objec holding state of the DHCP service
+    /// (enabled/disabled).
+    /// @param server_type Server type, i.e. DHCPv4 or DHCPv6 server.
+    HAService(const asiolink::IOServicePtr& io_service,
+              const dhcp::NetworkStatePtr& network_state,
+              const HAConfigPtr& config,
+              const HAServerType& server_type = HAServerType::DHCPv4);
+
+    /// @brief Returns HA server type used in object construction.
+    HAServerType getServerType() const {
+        return (server_type_);
+    }
+
+    /// @brief Defines events used by the HA service.
+    virtual void defineEvents();
+
+    /// @brief Verifies events used by the HA service.
+    virtual void verifyEvents();
+
+    /// @brief Defines states of the HA service.
+    virtual void defineStates();
+
+    /// @brief Handler for the "backup" state.
+    ///
+    /// This is the normal operation state for a backup server. Only
+    /// the backup server can be transitioned to this state. The
+    /// DHCP service is disabled in this state and the server merely
+    /// receives lease updates from the active servers. The backup
+    /// server may be manually instructed to enable DHCP service and
+    /// serve selected scopes, e.g. when both primary and secondary
+    /// (or standby) servers are down.
+    ///
+    /// This handler disables DHCP service on the first pass. It is
+    /// no-op during all subsequent passes.
+    void backupStateHandler();
+
+    /// @brief Handler for the "hot-standby" and "load-balancing"
+    /// states.
+    ///
+    /// This is a handler invoked for the servers running in the
+    /// hot-standby or load-balancing mode, both for the primary
+    /// and the standby (or secondary) server.
+    ///
+    /// In the hot-standby mode, the primary server responds to all
+    /// DHCP queries from the clients. The standby server receives
+    /// lease updates from the primary, but it doesn't respond to any
+    /// DHCP queries. Both servers exchange heartbeats to monitor
+    /// each other states. If any of the servers detects a failure
+    /// of its partner, it transitions to the "partner-down" state.
+    ///
+    /// In the load-balancing mode, both servers respond to the DHCP
+    /// queries and exchange the heartbeats and lease updates.
+    /// If any of the servers detects a failure of its partner,
+    /// it transitions to the "partner-down" state.
+    ///
+    /// If any of the servers being in the "hot-standby" or
+    /// "load-balancing" state detects that its partner is in the
+    /// "partner-down" state, the server transitions to the
+    /// "waiting" state. Such situation may occur if the Control
+    /// Agent of this server crashes but the DHCP daemon continues
+    /// to run. The partner will transition to the "partner-down"
+    /// state if the failure detection algorithm (based on "secs"
+    /// field or "elapsed time" option monitoring) and this server
+    /// is considered to be offline based solely on the fact that
+    /// it doesn't respond to heartbeats.
+    void normalStateHandler();
+
+    /// @brief Handler for "partner-down" state.
+    ///
+    /// This is a handler invoked for the server which detected a failure
+    /// of its partner. The partner was not responding to heartbeats and
+    /// did not respond to a number of DHCP queries directed to it. In
+    /// some configurations, the server may transition to this state when
+    /// the server is not responding to the heartbeats, without checking
+    /// whether it responds to DHCP queries ("max-unacked-clients" parameter
+    /// is set to 0).
+    ///
+    /// In the "partner-down" state the server responds to all DHCP queries,
+    /// i.e. the queries it would normally respond to and to the queries
+    /// to which its partner would respond.
+    ///
+    /// The backup server would never transition to this state.
+    ///
+    /// The server will transition from the "partner-down" state to the
+    /// "load-balancing" or "hot-standby" state if its partner is in the
+    /// "ready" state. In this state, the partner indicates that it has
+    /// synchronized its database and is ready to enable its DHCP service.
+    ///
+    /// If this server finds that the partner is in an unexpected state,
+    /// i.e. "load-balancing", "hot-standby" or "partner-down", it transitions
+    /// to the "waiting" state to try to resolve the conflict with the partner.
+    void partnerDownStateHandler();
+
+    /// @brief Handler for "ready" state.
+    ///
+    /// This a handler invoked for the server which finished synchronizing
+    /// its lease database with the partner and is indicating readiness to
+    /// start normal operation, i.e. load balancing or hot standby. The
+    /// partner being in the "partner-down" state will transition to the
+    /// "load-balancing" or "hot-standby" state. The "ready" server will
+    /// also transition to one of these states following the transition
+    /// of the partner.
+    ///
+    /// If both servers appear to be in the "ready" state, the primary
+    /// server transitions to the "load-balancing" or "hot-standby" state
+    /// first.
+    ///
+    /// The server in the "ready" state is not responding to the DHCP queries.
+    void readyStateHandler();
+
+    /// @brief Handler for "syncing" state.
+    ///
+    /// This is a handler invoked for the server in the "syncing" state.
+    /// The server being in this state is trying to retrieve leases from
+    /// the partner's database and update its local database. Every
+    /// primary, secondary and standby server must transition via this
+    /// state to retrieve up to date lease information from the active
+    /// partner. If the partner is offline the server will eventually
+    /// transition to the "partner-down" state without synchronizing
+    /// the lease database.
+    ///
+    /// The lease database synchronization is performed synchronously,
+    /// i.e. the handler doesn't return until the synchronization completes
+    /// or a communication failure occurs.
+    ///
+    /// The server in the "syncing" state is not responding to the DHCP queries.
+    void syncingStateHandler();
+
+    /// @brief Handler for "terminated" state.
+    ///
+    /// This is a handler invoked for the server in the "terminated" state.
+    /// This indicates that the HA service is disabled, typically as a result
+    /// of an unrecoverable error such as detecting that clocks skew between
+    /// the active HA servers being too large. This situation requires
+    /// manual intervation of an administrator. When the problem is corrected,
+    /// the HA service needs to be restarted.
+    ///
+    /// @note Currently, restarting the HA service requires restarting the
+    /// DHCP server. In the future, we will provide a command to restart
+    /// the HA service.
+    ///
+    /// The server being in the "terminated" state will respond to DHCP clients
+    /// as if it was in a hot-standby or load-balancing state. However, it will
+    /// neither send nor receive lease updates. It also won't send heartbeats
+    /// to the partner.
+    void terminatedStateHandler();
+
+    /// @brief Handler for "waiting" state.
+    ///
+    /// This is a handler invoked for the server in the "waiting" state.
+    /// This is the first state of every server after its startup. The
+    /// server initiates a heartbeat to learn the state of its partner.
+    /// If the partner is operating (e.g. is in the "partner-down" state),
+    /// the server will transition to the "syncing" state to fetch
+    /// lease information from the partner. If leases synchronization is
+    /// administratively disabled with 'sync-leases' parameter, the server
+    /// will transition directly to the "ready" state. If both servers are
+    /// in the "waiting" state the primary transitions to the "syncing" or
+    /// "ready" state first. If the partner is in the "syncing" state,
+    /// this server will remain in the "waiting" state until the partner
+    /// completes synchronization.
+    ///
+    /// If the server starts, but the partner appears to be offline, the
+    /// server transitions to the "partner-down" state.
+    ///
+    /// A backup server transitions from the "waiting" to the "backup"
+    /// state directly.
+    ///
+    /// The server in the "waiting" state is not responding to the DHCP
+    /// queries.
+    void waitingStateHandler();
+
+protected:
+
+    /// @brief Transitions to a desired state and logs it.
+    ///
+    /// @param state the new value to assign to the current state.
+    void verboseTransition(const unsigned state);
+
+public:
+
+    /// @brief Instructs the HA service to serve default scopes.
+    ///
+    /// This method is mostly useful for unit testing. The scopes need to be
+    /// enabled to test @c inScope methods invoked via @c HAImpl class.
+    void serveDefaultScopes();
+
+    /// @brief Checks if the DHCPv4 query should be processed by this server.
+    ///
+    /// It also associates the DHCPv4 query with required classes appropriate
+    /// to the server that should process the packet and increments counters
+    /// of unanswered DHCP queries when in communications interrupted state.
+    ///
+    /// @param [out] query4 pointer to the DHCPv4 query received. A client class
+    /// will be appended to this query instance, appropriate for the server to
+    /// process this query, e.g. "HA_server1" if the "server1" should process
+    /// the query etc.
+    ///
+    /// @return true if DHCPv4 query should be processed by this server instance,
+    /// false otherwise.
+    bool inScope(dhcp::Pkt4Ptr& query4);
+
+    /// @brief Checks if the DHCPv6 query should be processed by this server.
+    ///
+    /// It also associates the DHCPv6 query with required classes appropriate
+    /// to the server that should process the packet and increments counters
+    /// of unanswered DHCP queries when in communications interrupted state.
+    ///
+    /// @param [out] query6 pointer to the DHCPv6 query received. A client class
+    /// will be appended to this query instance, appropriate for the server to
+    /// process this query, e.g. "HA_server1" if the "server1" should process
+    /// the query etc.
+    ///
+    /// @return true if DHCPv6 query should be processed by this server instance,
+    /// false otherwise.
+    bool inScope(dhcp::Pkt6Ptr& query6);
+
+private:
+
+    /// @brief Checks if the DHCP query should be processed by this server.
+    ///
+    /// This is a generic implementation of the public @c inScope method
+    /// variants.
+    ///
+    /// @tparam type of the pointer to the DHCP query.
+    /// @param [out] query6 pointer to the DHCP query received. A client class
+    /// will be appended to this query instance, appropriate for the server to
+    /// process this query, e.g. "HA_server1" if the "server1" should process
+    /// the query etc.
+    ///
+    /// @return true if DHCP query should be processed by this server instance,
+    /// false otherwise.
+    template<typename QueryPtrType>
+    bool inScopeInternal(QueryPtrType& query);
+
+public:
+
+    /// @brief Enables or disables network state depending on the served scopes.
+    ///
+    /// This method is called in each HA state to enable/disable DHCP service
+    /// as appropriate for that state.
+    void adjustNetworkState();
+
+protected:
+
+    /// @brief Indicates if the server should transition to the partner down
+    /// state.
+    ///
+    /// It indicates that the server should transition to the partner down
+    /// state when the communications is interrupted (over the control channel)
+    /// and the partner is not answering DHCP queries in the load balancing
+    /// case and in the hot standby case, when this server is a secondary.
+    ///
+    /// In the hot standby case, when the server is primary, the communications
+    /// interrupted is enough to transition to the partner down state.
+    ///
+    /// @return true if the server should transition to the partner down state,
+    /// false otherwise.
+    bool shouldPartnerDown() const;
+
+    /// @brief Indicates if the server should transition to the terminated
+    /// state as a result of high clock skew.
+    ///
+    /// It indicates that the server should transition to the terminated
+    /// state because of the clock skew being too high. If the clock skew is
+    /// is higher than 30 seconds but lower than 60 seconds this method
+    /// only logs a warning. In case, the clock skew exceeds 60 seconds, this
+    /// method logs a warning and returns true.
+    ///
+    /// @return true if the server should transition to the terminated state,
+    /// false otherwise.
+    bool shouldTerminate() const;
+
+public:
+
+    /// @brief Schedules asynchronous IPv4 leases updates.
+    ///
+    /// This method schedules asynchronous lease updates as a result of the
+    /// "leases4_committed" callout. The lease updates are transmitted over
+    /// HTTP to the peers specified in the configuration (except self).
+    /// If the server is in the partner-down state the lease updates are not
+    /// sent to the partner but they are sent to all backup servers.
+    /// In other states in which the server responds to DHCP queries, the
+    /// lease updates are sent to all servers. The scheduled lease updates
+    /// are performed after the callouts return. The server parks the
+    /// processed DHCP packet and runs IO service shared between the server
+    /// and the hook library.
+    ////
+    /// If the lease update to the partner (primary, secondary or standby)
+    /// fails, the parked packet is dropped. If the lease update to any of
+    /// the backup server fails, an error message is logged but the DHCP
+    /// packet is not dropped.
+    ///
+    /// This method must be called only if there is at least one lease
+    /// altered.
+    ///
+    /// @param query Pointer to the processed DHCP client message.
+    /// @param leases Pointer to a collection of the newly allocated or
+    /// updated leases.
+    /// @param deleted_leases Pointer to a collection of the released leases.
+    /// @param [out] parking_lot Pointer to the parking lot handle available
+    /// for the "leases4_committed" hook point. This is where the DHCP client
+    /// message is parked. This method calls @c unpark() on this object when
+    /// the asynchronous updates are completed.
+    ///
+    /// @return Number of peers to whom lease updates have been scheduled
+    /// to be sent. This value is used to determine if the DHCP query
+    /// should be parked while waiting for the lease update to complete.
+    size_t asyncSendLeaseUpdates(const dhcp::Pkt4Ptr& query,
+                                 const dhcp::Lease4CollectionPtr& leases,
+                                 const dhcp::Lease4CollectionPtr& deleted_leases,
+                                 const hooks::ParkingLotHandlePtr& parking_lot);
+
+
+    /// @brief Schedules asynchronous IPv6 lease updates.
+    ///
+    /// This method schedules asynchronous IPv6 lease updates as a result of the
+    /// "leases6_committed" callout. It works analogously to the IPv4 version of
+    /// this function.
+    ///
+    /// @param query Pointer to the processed DHCP client message.
+    /// @param leases Pointer to a collection of the newly allocated or
+    /// updated leases.
+    /// @param deleted_leases Pointer to a collection of the released leases.
+    /// @param [out] parking_lot Pointer to the parking lot handle available
+    /// for the "leases6_committed" hook point. This is where the DHCP client
+    /// message is parked. This method calls @c unpark() on this object when
+    /// the asynchronous updates are completed.
+    ///
+    /// @return Number of peers to whom lease updates have been scheduled
+    /// to be sent. This value is used to determine if the DHCP query
+    /// should be parked while waiting for the lease update to complete.
+    size_t asyncSendLeaseUpdates(const dhcp::Pkt6Ptr& query,
+                                 const dhcp::Lease6CollectionPtr& leases,
+                                 const dhcp::Lease6CollectionPtr& deleted_leases,
+                                 const hooks::ParkingLotHandlePtr& parking_lot);
+
+protected:
+
+    /// @brief Asynchronously sends lease update to the peer.
+    ///
+    /// @param query Pointer to the DHCP client's query.
+    /// @param config Pointer to the configuration of the server to which the
+    /// command should be sent.
+    /// @param command Pointer to the command to be sent.
+    /// @param [out] parking_lot Parking lot where the query is parked.
+    /// This method uses this handle to unpark the packet when all asynchronous
+    /// requests have been completed.
+    /// @tparam QueryPtrType Type of the pointer to the DHCP client's message,
+    /// i.e. Pkt4Ptr or Pkt6Ptr.
+    /// @throw Unexpected when an unexpected error occurs.
+    template<typename QueryPtrType>
+    void asyncSendLeaseUpdate(const QueryPtrType& query,
+                              const HAConfig::PeerConfigPtr& config,
+                              const data::ConstElementPtr& command,
+                              const hooks::ParkingLotHandlePtr& parking_lot);
+
+    /// @brief Checks if the lease updates should be sent as result of leases
+    /// allocation or release.
+    ///
+    /// This method checks if the lease updates should be sent by the server
+    /// while this server is in the given state. Note that the backup server
+    /// will never send lease updates.
+    ///
+    /// @param peer_config pointer to the configuration of the peer to which
+    /// the updates are to be sent.
+    /// @return true if the server should send lease updates, false otherwise.
+    bool shouldSendLeaseUpdates(const HAConfig::PeerConfigPtr& peer_config) const;
+
+public:
+
+    /// @brief Processes ha-heartbeat command and returns a response.
+    ///
+    /// This method processes a ha-heartbeat command sent by a peer. This
+    /// command is sent periodically to the server to detect its state. The
+    /// servers use the heartbeat mechanism to detect peers' failures and to
+    /// synchronize their operations when they start up after the failure or
+    /// a restart.
+    ///
+    /// The ha-heartbeat command takes no arguments. The response contains
+    /// a server state and timestamp in the following format:
+    ///
+    /// @code
+    /// {
+    ///     "arguments": {
+    ///         "date-time": "Thu, 01 Feb 2018 21:18:26 GMT",
+    ///         "state": "waiting"
+    ///     },
+    ///     "result": 0,
+    ///     "text": "HA peer status returned."
+    /// }
+    /// @endcode
+    ///
+    /// @return Pointer to the response to the heartbeat.
+    data::ConstElementPtr processHeartbeat();
+
+protected:
+
+    /// @brief Starts asynchronous heartbeat to a peer.
+    void asyncSendHeartbeat();
+
+    /// @brief Schedules asynchronous heartbeat to a peer if it is not scheduled.
+    ///
+    /// The heartbeat will be sent according to the value of the heartbeat-delay
+    /// setting in the HA configuration. This is one shot heartbeat. The callback
+    /// will reschedule it.
+    void scheduleHeartbeat();
+
+    /// @brief Unconditionally starts one heartbeat to a peer.
+    void startHeartbeat();
+
+    /// @brief Schedules asynchronous "dhcp-disable" command to the specified
+    /// server.
+    ///
+    /// @param http_client reference to the client to be used to communicate
+    /// with the other server.
+    /// @param server_name name of the server to which the command should be
+    /// sent.
+    /// @param max_period maximum number of seconds for which the DHCP service
+    /// should be disabled.
+    /// @param post_request_action pointer to the function to be executed when
+    /// the request is completed.
+    void asyncDisable(http::HttpClient& http_client,
+                      const std::string& server_name,
+                      const unsigned int max_period,
+                      const PostRequestCallback& post_request_action);
+
+    /// @brief Schedules asynchronous "dhcp-enable" command to the specified
+    /// server.
+    ///
+    /// @param http_client reference to the client to be used to communicate
+    /// with the other server.
+    /// @param server_name name of the server to which the command should be
+    /// sent.
+    /// @param post_request_action pointer to the function to be executed when
+    /// the request is completed.
+    void asyncEnable(http::HttpClient& http_client,
+                     const std::string& server_name,
+                     const PostRequestCallback& post_request_action);
+
+    /// @brief Disables local DHCP service.
+    void localDisable();
+
+    /// @brief Enables local DHCP service.
+    void localEnable();
+
+    /// @brief Asynchronously reads leases from a peer and updates local
+    /// lease database.
+    ///
+    /// This method asynchronously sends lease4-get-all command to fetch all
+    /// leases from the HA peer database. When the response is received, the
+    /// callback function iterates over the returned leases and inserts those
+    /// that are not present in the local database and replaces any existing
+    /// leases if the fetched lease instance is newer (based on cltt) than
+    /// the instance in the local lease database.
+    ///
+    /// If there is an error while inserting or updating any of the leases
+    /// a warning message is logged and the process continues for the
+    /// remaining leases.
+    ///
+    /// This method variant uses default HTTP client for communication.
+    void asyncSyncLeases();
+
+    /// @brief Asynchronously reads leases from a peer and updates local
+    /// lease database using a provided client instance.
+    ///
+    /// This method asynchronously sends lease4-get-all command to fetch all
+    /// leases from the HA peer database. When the response is received, the
+    /// callback function iterates over the returned leases and inserts those
+    /// that are not present in the local database and replaces any existing
+    /// leases if the fetched lease instance is newer (based on cltt) than
+    /// the instance in the local lease database.
+    ///
+    /// If there is an error while inserting or updating any of the leases
+    /// a warning message is logged and the process continues for the
+    /// remaining leases.
+    ///
+    /// @param http_client reference to the client to be used to communicate
+    /// with the other server.
+    /// @param post_sync_action pointer to the function to be executed when
+    /// lease database synchronization is complete. If this is null, no
+    /// post synchronization action is invoked.
+    void asyncSyncLeases(http::HttpClient& http_client,
+                         const PostRequestCallback& post_sync_action);
+
+public:
+
+    /// @brief Processes ha-sync command and returns a response.
+    ///
+    /// This method processes ha-sync command. It instructs the server
+    /// to disable the DHCP service on the HA peer, fetch all leases from
+    /// the peer and update the local lease database. Leases synchronization
+    /// is usually performed automatically by the server which starts up for
+    /// the first time or after a failure. However, the ha-sync command can
+    /// also be triggered manually by the server administrator to force
+    /// synchronization of the lease database in cases when manual recovery
+    /// is required. One of the possible cases is when the lease database has
+    /// to be recovered from the backup server, e.g. when both primary and
+    /// secondary (or standby) servers have crashed.
+    ///
+    /// @param server_name name of the server to fetch leases from.
+    /// @param max_period maximum number of seconds to disable DHCP service
+    /// of the peer. This value is used in dhcp-disable command issued to
+    /// the peer before the lease4-get-all command.
+    ///
+    /// @return Pointer to the response to the ha-sync command.
+    data::ConstElementPtr processSynchronize(const std::string& server_name,
+                                             const unsigned int max_period);
+
+protected:
+
+    /// @brief Synchronizes lease database with a partner.
+    ///
+    /// It instructs the server to disable the DHCP service on the HA peer,
+    /// fetch all leases from the peer and update the local lease database.
+    ///
+    /// @param [out] status_message status message in textual form.
+    /// @param server_name name of the server to fetch leases from.
+    /// @param max_period maximum number of seconds to disable DHCP service
+    /// of the peer. This value is used in dhcp-disable command issued to
+    /// the peer before the lease4-get-all command.
+    ///
+    /// @return Synchronization result according to the status codes returned
+    /// in responses to control commands.
+    int synchronize(std::string& status_message, const std::string& server_name,
+                    const unsigned int max_period);
+
+public:
+
+    /// @brief Processes ha-scopes command and returns a response.
+    ///
+    /// @param scopes vector of scope names to be enabled.
+    ///
+    /// @return Pointer to the response to the ha-scopes command.
+    data::ConstElementPtr processScopes(const std::vector<std::string>& scopes);
+
+protected:
+
+    /// @brief Checks if the response is valid or contains an error.
+    ///
+    /// The response must be non-null, must contain a JSON body and must
+    /// contain a success status code.
+    ///
+    /// @param response pointer to the received response.
+    /// @return Pointer to the response arguments.
+    /// @throw CtrlChannelError if response is invalid or contains an error.
+    data::ConstElementPtr verifyAsyncResponse(const http::HttpResponsePtr& response);
+
+    /// @brief Pointer to the IO service object shared between this hooks
+    /// library and the DHCP server.
+    asiolink::IOServicePtr io_service_;
+
+    /// @brief Pointer to the state of the DHCP service (enabled/disabled).
+    dhcp::NetworkStatePtr network_state_;
+
+    /// @brief Pointer to the HA hooks library configuration.
+    HAConfigPtr config_;
+
+    /// @brief DHCP server type.
+    HAServerType server_type_;
+
+    /// @brief HTTP client instance used to send lease updates.
+    http::HttpClient client_;
+
+    /// @brief Holds communication state with a peer.
+    CommunicationStatePtr communication_state_;
+
+    /// @brief Selects queries to be processed/dropped.
+    QueryFilter query_filter_;
+
+    /// @brief Map holding a number of scheduled requests for a given packet.
+    ///
+    /// A single callout may send multiple requests at the same time, e.g.
+    /// "leases4_committed" may provide multiple deleted leases and multiple
+    /// newly allocated leases. The parked packet may be unparked when all
+    /// requests have been delivered. Therefore, it is required to count
+    /// the number of responses received so far and unpark the packet when
+    /// all responses have been received. That's what this map is used for.
+    std::map<boost::shared_ptr<dhcp::Pkt>, int> pending_requests_;
+};
+
+/// @brief Pointer to the @c HAService class.
+typedef boost::shared_ptr<HAService> HAServicePtr;
+
+} // end of namespace isc::ha
+} // end of namespace isc
+
+#endif
diff --git a/src/hooks/dhcp/high_availability/ha_service_states.h b/src/hooks/dhcp/high_availability/ha_service_states.h
new file mode 100644 (file)
index 0000000..22d564f
--- /dev/null
@@ -0,0 +1,45 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#ifndef HA_SERVICE_STATES_H
+#define HA_SERVICE_STATES_H
+
+#include <util/state_model.h>
+
+namespace isc {
+namespace ha {
+
+/// Backup state.
+const int HA_BACKUP_ST = util::StateModel::SM_DERIVED_STATE_MIN + 1;
+
+/// Hot standby state.
+const int HA_HOT_STANDBY_ST = util::StateModel::SM_DERIVED_STATE_MIN + 2;
+
+/// Load balancing state.
+const int HA_LOAD_BALANCING_ST = util::StateModel::SM_DERIVED_STATE_MIN + 3;
+
+/// Partner down state.
+const int HA_PARTNER_DOWN_ST = util::StateModel::SM_DERIVED_STATE_MIN + 4;
+
+/// Server ready state, i.e. synchronized database, can enable DHCP service.
+const int HA_READY_ST = util::StateModel::SM_DERIVED_STATE_MIN + 5;
+
+/// Synchronizing database state.
+const int HA_SYNCING_ST = util::StateModel::SM_DERIVED_STATE_MIN + 6;
+
+/// HA service terminated state.
+const int HA_TERMINATED_ST = util::StateModel::SM_DERIVED_STATE_MIN + 7;
+
+/// Server waiting state, i.e. waiting for another server to be ready.
+const int HA_WAITING_ST = util::StateModel::SM_DERIVED_STATE_MIN + 8;
+
+/// Special state indicating that this server is unable to communicate with
+/// the partner.
+const int HA_UNAVAILABLE_ST = util::StateModel::SM_DERIVED_STATE_MIN + 1000;
+
+} // end of namespace isc::ha
+} // end of namespace isc
+
+#endif
diff --git a/src/hooks/dhcp/high_availability/query_filter.cc b/src/hooks/dhcp/high_availability/query_filter.cc
new file mode 100644 (file)
index 0000000..73baa0a
--- /dev/null
@@ -0,0 +1,319 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <config.h>
+
+#include <ha_log.h>
+#include <query_filter.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/option.h>
+#include <exceptions/exceptions.h>
+#include <array>
+#include <iostream>
+#include <sstream>
+
+using namespace isc::dhcp;
+using namespace isc::log;
+
+namespace {
+
+/// @brief A "mixing table" of 256 distinct values, in pseudo-random order.
+///
+/// The mixing table comes from section 6 of RFC3074.
+std::array<uint8_t, 256> loadb_mx_tbl = { {
+    251, 175, 119, 215, 81, 14, 79, 191, 103, 49, 181, 143, 186, 157,  0,
+    232, 31, 32, 55, 60, 152, 58, 17, 237, 174, 70, 160, 144, 220, 90, 57,
+    223, 59,  3, 18, 140, 111, 166, 203, 196, 134, 243, 124, 95, 222, 179,
+    197, 65, 180, 48, 36, 15, 107, 46, 233, 130, 165, 30, 123, 161, 209, 23,
+    97, 16, 40, 91, 219, 61, 100, 10, 210, 109, 250, 127, 22, 138, 29, 108,
+    244, 67, 207,  9, 178, 204, 74, 98, 126, 249, 167, 116, 34, 77, 193,
+    200, 121,  5, 20, 113, 71, 35, 128, 13, 182, 94, 25, 226, 227, 199, 75,
+    27, 41, 245, 230, 224, 43, 225, 177, 26, 155, 150, 212, 142, 218, 115,
+    241, 73, 88, 105, 39, 114, 62, 255, 192, 201, 145, 214, 168, 158, 221,
+    148, 154, 122, 12, 84, 82, 163, 44, 139, 228, 236, 205, 242, 217, 11,
+    187, 146, 159, 64, 86, 239, 195, 42, 106, 198, 118, 112, 184, 172, 87,
+    2, 173, 117, 176, 229, 247, 253, 137, 185, 99, 164, 102, 147, 45, 66,
+    231, 52, 141, 211, 194, 206, 246, 238, 56, 110, 78, 248, 63, 240, 189,
+    93, 92, 51, 53, 183, 19, 171, 72, 50, 33, 104, 101, 69, 8, 252, 83, 120,
+    76, 135, 85, 54, 202, 125, 188, 213, 96, 235, 136, 208, 162, 129, 190,
+    132, 156, 38, 47, 1, 7, 254, 24, 4, 216, 131, 89, 21, 28, 133, 37, 153,
+    149, 80, 170, 68, 6, 169, 234, 151 }
+};
+
+} // end of anonymous namespace
+
+namespace isc {
+namespace ha {
+
+QueryFilter::QueryFilter(const HAConfigPtr& config)
+    : config_(config), peers_(), scopes_(), active_servers_(0) {
+
+    // Make sure that the configuration is valid. We make certain
+    // assumptions about the availability of the servers' configurations
+    // in the config_ structure.
+    config_->validate();
+
+    HAConfig::PeerConfigMap peers_map = config->getAllServersConfig();
+    std::vector<HAConfig::PeerConfigPtr> backup_peers;
+
+    // The returned configurations are not ordered. Let's iterate over them
+    // and put them in the desired order.
+    for (auto peer_pair = peers_map.begin(); peer_pair != peers_map.end(); ++peer_pair) {
+        auto peer = peer_pair->second;
+        // The primary server is always first on the list.
+        if (peer->getRole() == HAConfig::PeerConfig::PRIMARY) {
+            peers_.insert(peers_.begin(), peer);
+            ++active_servers_;
+
+        // The secondary server is always behind the primary server.
+        } else if ((peer->getRole() == HAConfig::PeerConfig::SECONDARY) ||
+                   (peer->getRole() == HAConfig::PeerConfig::STANDBY)) {
+            peers_.push_back(peer);
+
+            // If this is a secondary server, we're in the load balancing
+            // mode, in which case we have two active servers.
+            if (peer->getRole() == HAConfig::PeerConfig::SECONDARY) {
+                ++active_servers_;
+            }
+
+        // If this is neither primary nor secondary/standby, it is a backup.
+        } else {
+            backup_peers.push_back(peer);
+        }
+    }
+
+    // Append backup servers to the list.
+    if (!backup_peers.empty()) {
+        peers_.insert(peers_.end(), backup_peers.begin(), backup_peers.end());
+    }
+
+    // The query filter is initially setup to serve default scopes, i.e. for the
+    // load balancing case the primary and secondary are responsible for their
+    // own scopes. The backup servers are not responding to any queries. In the
+    // hot standby mode, the primary server is responsible for the entire traffic.
+    // The standby server is not responding.
+    serveDefaultScopes();
+}
+
+void
+QueryFilter::serveScope(const std::string& scope_name) {
+    validateScopeName(scope_name);
+    scopes_[scope_name] = true;
+}
+
+void
+QueryFilter::serveScopeOnly(const std::string& scope_name) {
+    validateScopeName(scope_name);
+    serveNoScopes();
+    serveScope(scope_name);
+}
+
+void
+QueryFilter::serveScopes(const std::vector<std::string>& scopes) {
+    // Remember currently enabled scopes in case we fail to process
+    // the provided list of scopes.
+    auto current_scopes = scopes_;
+    try {
+        serveNoScopes();
+        for (size_t i = 0; i < scopes.size(); ++i) {
+            serveScope(scopes[i]);
+        }
+
+    } catch (...) {
+        // There was an error processing scopes list. Need to revert
+        // to the previous configuration.
+        scopes_ = current_scopes;
+        throw;
+    }
+}
+
+void
+QueryFilter::serveDefaultScopes() {
+    // Get this server instance configuration.
+    HAConfig::PeerConfigPtr my_config = config_->getThisServerConfig();
+    HAConfig::PeerConfig::Role my_role = my_config->getRole();
+
+    // Clear scopes.
+    serveNoScopes();
+
+    // If I am primary or secondary, then I am only responsible for my own
+    // scope.  If I am standby, I am not responsible for any scope.
+    if ((my_role == HAConfig::PeerConfig::PRIMARY) ||
+        (my_role == HAConfig::PeerConfig::SECONDARY)) {
+        serveScope(my_config->getName());
+    }
+}
+
+void
+QueryFilter::serveFailoverScopes() {
+    // Clear scopes.
+    serveNoScopes();
+
+    // Iterate over the roles of all servers to see which scope should
+    // be enabled.
+    for (auto peer = peers_.begin(); peer != peers_.end(); ++peer) {
+        // The scope of the primary server must always be served. If we're
+        // doing load balancing, the scope of the secondary server also
+        // has to be served. Regardless if I am primary or secondary,
+        // I will start serving queries from both scopes. If I am a
+        // standby server, I will start serving the scope of the primary
+        // server.
+        if (((*peer)->getRole() == HAConfig::PeerConfig::PRIMARY) ||
+            ((*peer)->getRole() == HAConfig::PeerConfig::SECONDARY)) {
+            serveScope((*peer)->getName());
+        }
+    }
+}
+
+void
+QueryFilter::serveNoScopes() {
+    scopes_.clear();
+
+    // Disable scope for each peer in the configuration.
+    for (auto peer = peers_.begin(); peer != peers_.end(); ++peer) {
+        scopes_[(*peer)->getName()] = false;
+    }
+}
+
+bool
+QueryFilter::amServingScope(const std::string& scope_name) const {
+    auto scope = scopes_.find(scope_name);
+    return ((scope == scopes_.end()) || (scope->second));
+}
+
+std::set<std::string>
+QueryFilter::getServedScopes() const {
+    std::set<std::string> scope_set;
+    for (auto scope : scopes_) {
+        if (scope.second) {
+            scope_set.insert(scope.first);
+        }
+    }
+    return (scope_set);
+}
+
+bool
+QueryFilter::inScope(const dhcp::Pkt4Ptr& query4, std::string& scope_class) const {
+    return (inScopeInternal(query4, scope_class));
+}
+
+bool
+QueryFilter::inScope(const dhcp::Pkt6Ptr& query6, std::string& scope_class) const {
+    return (inScopeInternal(query6, scope_class));
+}
+
+template<typename QueryPtrType>
+bool
+QueryFilter::inScopeInternal(const QueryPtrType& query,
+                             std::string& scope_class) const {
+    if (!query) {
+        isc_throw(BadValue, "query must not be null");
+    }
+
+    int candidate_server = 0;
+
+    // If we're doing load balancing we have to check if this query
+    // belongs to us or the partner. If it belongs to a partner but
+    // we're configured to serve this scope, we should accept it.
+    if (config_->getHAMode() == HAConfig::LOAD_BALANCING) {
+        candidate_server = loadBalance(query);
+        // Malformed query received.
+        if (candidate_server < 0) {
+            return (false);
+        }
+    }
+
+    auto scope = peers_[candidate_server]->getName();
+    scope_class = makeScopeClass(scope);
+    return ((candidate_server >= 0) && amServingScope(scope));
+}
+
+int
+QueryFilter::loadBalance(const dhcp::Pkt4Ptr& query4) const {
+    uint8_t lb_hash = 0;
+    // Try to compute the hash by client identifier if the client
+    // identifier has been specified.
+    OptionPtr opt_client_id = query4->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
+    if (opt_client_id && !opt_client_id->getData().empty()) {
+        const auto& client_id_key = opt_client_id->getData();
+        lb_hash = loadBalanceHash(&client_id_key[0], client_id_key.size());
+
+    } else {
+        // No client identifier available. Use the HW address instead.
+        HWAddrPtr hwaddr = query4->getHWAddr();
+        if (hwaddr && !hwaddr->hwaddr_.empty()) {
+            lb_hash = loadBalanceHash(&hwaddr->hwaddr_[0], hwaddr->hwaddr_.size());
+
+        } else {
+            // No client identifier and no HW address. Indicate an
+            // error.
+            std::stringstream xid;
+            xid << "0x" << std::hex << query4->getTransid() << std::dec;
+            LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_LOAD_BALANCING_IDENTIFIER_MISSING)
+                .arg(xid.str());
+            return (-1);
+        }
+    }
+
+    // The hash value modulo number of active servers gives an index
+    // of the server to process the packet.
+    return (active_servers_ > 0 ? static_cast<int>(lb_hash % active_servers_) : -1);
+}
+
+int
+QueryFilter::loadBalance(const dhcp::Pkt6Ptr& query6) const {
+    uint8_t lb_hash = 0;
+    // Compute the hash by DUID if the DUID.
+    OptionPtr opt_duid = query6->getOption(D6O_CLIENTID);
+    if (opt_duid && !opt_duid->getData().empty()) {
+        const auto& duid_key = opt_duid->getData();
+        lb_hash = loadBalanceHash(&duid_key[0], duid_key.size());
+
+    } else {
+        // No DUID. Indicate an error.
+        std::stringstream xid;
+        xid << "0x" << std::hex << query6->getTransid() << std::dec;
+        LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_LOAD_BALANCING_DUID_MISSING)
+            .arg(xid.str());
+        return (-1);
+    }
+
+    // The hash value modulo number of active servers gives an index
+    // of the server to process the packet.
+    return (active_servers_ > 0 ? static_cast<int>(lb_hash % active_servers_) : -1);
+}
+
+uint8_t
+QueryFilter::loadBalanceHash(const uint8_t* key, const size_t key_len) const {
+    uint8_t hash  = static_cast<uint8_t>(key_len);
+
+    for (auto i = key_len; i > 0;) {
+        hash = loadb_mx_tbl[hash ^ key[--i]];
+    }
+
+    return (hash);
+}
+
+void
+QueryFilter::validateScopeName(const std::string& scope_name) const {
+    try {
+        // If there is no such server, the scope name is invalid.
+        static_cast<void>(config_->getPeerConfig(scope_name));
+
+    } catch (...) {
+        isc_throw(BadValue, "invalid server name specified '" << scope_name
+                  << "' while enabling/disabling HA scopes");
+    }
+}
+
+std::string
+QueryFilter::makeScopeClass(const std::string& scope_name) const {
+    return (std::string("HA_") + scope_name);
+}
+
+
+} // end of namespace isc::ha
+} // end of namespace isc
diff --git a/src/hooks/dhcp/high_availability/query_filter.h b/src/hooks/dhcp/high_availability/query_filter.h
new file mode 100644 (file)
index 0000000..9817dc1
--- /dev/null
@@ -0,0 +1,265 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#ifndef QUERY_FILTER_H
+#define QUERY_FILTER_H
+
+#include <ha_config.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt6.h>
+#include <cstdint>
+#include <map>
+#include <vector>
+#include <set>
+#include <string>
+
+namespace isc {
+namespace ha {
+
+/// @brief DHCP query filtering class.
+///
+/// This class is a central point of information about distribution of the
+/// DHCP queries processed by the servers within HA setup. It also implements
+/// load balancing of the DHCP queries, when configured to do so.
+///
+/// The query filter uses a term "scope" to identify group of DHCP queries
+/// processed by a given server. Currently, we support load balanacing
+/// between two servers. Therefore, in this mode of operation, there are two
+/// scopes named after servers responsible for processing packets belonging
+/// to those scopes, e.g. "server1" and "server2".
+///
+/// In the hot-standby mode, there is only one server processing incoming
+/// DHCP queries. Thus, there is only one scope named after the primary
+/// server, e.g. "server1".
+///
+/// This class allows for assigning the server to process queries belonging
+/// to various scopes. For example: when a failure of the partner server
+/// is detected, the @c QueryFilter::serveFailoverScopes is called to
+/// indicate that this server instance should start handling queries
+/// belonging to the scope(s) of the server which have died. Converesly,
+/// a call to @c QueryFilter::serveDefaultScopes reverts to the default
+/// state.
+///
+/// When DHCP query is received, the @c QueryFilter class is used to determine
+/// whether this query should be processed by this server. The
+/// @c QueryFilter::inScope methods return boolean value indicating
+/// whether the query should be processed or not. If not, the query is
+/// dropped.
+///
+/// The server administrator may force the server to start processing queries
+/// from the selected scopes, e.g. the administrator may manually instruct the
+/// backup server to take over the traffic of the primary and secondary servers.
+/// The 'ha-scopes' command is sent in such case, which enables/disables
+/// scopes within this class instance.
+class QueryFilter {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// This constructor puts HA peers configurations in the following order:
+    /// - primary server configuration,
+    /// - secondary or standby server configuration,
+    /// - backup servers configurations
+    ///
+    /// It also sets the @c active_servers_ value to the number of active
+    /// servers (responding to DHCP queries) for a given HA mode. In our case,
+    /// this is 2 for the load balancing case and 1 for the hot-standby.
+    /// Such organization of the configurations makes it easier for the
+    /// load balancing algorithm to distribute queries between active servers.
+    /// In our simple case, the load balancing algorithm can produce a value
+    /// of 0 or 1, which points to a primary or secondary server in the
+    /// configuration vector.
+    ///
+    /// @param config pointer to the HA configuration.
+    /// @throw HAConfigValidationError if provided configuration is invalid.
+    explicit QueryFilter(const HAConfigPtr& config);
+
+    /// @brief Enable scope.
+    ///
+    /// Starts serving queries from the specified scope. It doesn't affect
+    /// other scopes.
+    ///
+    /// @param scope_name name of the scope/server to be enabled.
+    /// @throw BadValue if scope name doesn't match any of the server names.
+    void serveScope(const std::string& scope_name);
+
+    /// @brief Enable scope and disable all other scopes.
+    ///
+    /// Starts serving queries from the specified scope. Disable all other
+    /// scopes.
+    ///
+    /// @param scope_name name of the scope/server to be enabled.
+    /// @throw BadValue if scope name doesn't match any of the server names.
+    void serveScopeOnly(const std::string& scope_name);
+
+    /// @brief Enables selected scopes.
+    ///
+    /// All non listed scopes are disabled.
+    ///
+    /// @param scopes vector of scope names to be enabled.
+    void serveScopes(const std::vector<std::string>& scopes);
+
+    /// @brief Serve default scopes for the given HA mode.
+    ///
+    /// If this server is primary or secondary (load balancing), the scope
+    /// of this server is enabled. All other scopes are disabled.
+    void serveDefaultScopes();
+
+    /// @brief Enable scopes required in failover case.
+    ///
+    /// In the load balancing case, the scopes of the primary and secondary
+    /// servers are enabled (this server will handle the entire traffic).
+    /// In the hot standby case, the primary server's scope is enabled
+    /// (this server will handle the entire traffic normally processed by
+    /// the primary server).
+    void serveFailoverScopes();
+
+    /// @brief Disables all scopes.
+    void serveNoScopes();
+
+    /// @brief Checks if this server instance is configured to process traffic
+    /// belonging to a particular scope.
+    ///
+    /// @param scope_name name of the scope/server.
+    /// @return true if the scope is enabled.
+    bool amServingScope(const std::string& scope_name) const;
+
+    /// @brief Returns served scopes.
+    ///
+    /// This method is mostly useful for testing purposes.
+    std::set<std::string> getServedScopes() const;
+
+    /// @brief Checks if this server should process the DHCPv4 query.
+    ///
+    /// This method takes into account enabled scopes for this server and
+    /// HA mode to determine whether this query should be processed. It
+    /// triggers load balancing when load balancing mode is enabled.
+    ///
+    /// @param query4 pointer to the DHCPv4 query instance.
+    /// @param [out] scope_class name of the class which corresponds to the
+    /// name of the server which owns the packet. Those class names are used
+    /// in subnets, pools and network configurations to associate them with
+    /// different servers.
+    ///
+    /// @return true if the specified query should be processed by this
+    /// server, false otherwise.
+    bool inScope(const dhcp::Pkt4Ptr& query4, std::string& scope_class) const;
+
+    /// @brief Checks if this server should process the DHCPv6 query.
+    ///
+    /// This method takes into account enabled scopes for this server and
+    /// HA mode to determine whether this query should be processed. It
+    /// triggers load balancing when load balancing mode is enabled.
+    ///
+    /// @param query6 pointer to the DHCPv6 query instance.
+    /// @param [out] scope_class name of the class which corresponds to the
+    /// name of the server which owns the packet. Those class names are used
+    /// in subnets, pools and network configurations to associate them with
+    /// different servers.
+    ///
+    /// @return true if the specified query should be processed by this
+    /// server, false otherwise.
+    bool inScope(const dhcp::Pkt6Ptr& query6, std::string& scope_class) const;
+
+private:
+
+    /// @brief Generic implementation of the @c inScope function for DHCPv4
+    /// and DHCPv6 queries.
+    ///
+    /// @tparam QueryPtrType type of the query, i.e. DHCPv4 or DHCPv6 query.
+    /// @param query pointer to the DHCP query instance.
+    /// @param [out] scope_class name of the class which corresponds to the
+    /// name of the server which owns the packet. Those class names are used
+    /// in subnets, pools and network configurations to associate them with
+    /// different servers.
+    ///
+    /// @return true if the specified query should be processed by this
+    /// server, false otherwise.
+    template<typename QueryPtrType>
+    bool inScopeInternal(const QueryPtrType& query, std::string& scope_class) const;
+
+protected:
+
+    /// @brief Performs load balancing of the DHCPv4 queries.
+    ///
+    /// This method returns an index of the server configuration
+    /// held within @c peers_ vector. This points to a server
+    /// which should process the given query. Currently, we only
+    /// support load balancing between two servers, therefore this
+    /// value should be 0 or 1.
+    ///
+    /// @param query4 pointer to the DHCPv4 query instance.
+    /// @return Index of the server which should process the query. It
+    /// returns negative value if the query is malformed, i.e. contains
+    /// no HW address and no client identifier.
+    int loadBalance(const dhcp::Pkt4Ptr& query4) const;
+
+    /// @brief Performs load balancing of the DHCPv6 queries.
+    ///
+    /// This method returns an index of the server configuration
+    /// held within @c peers_ vector. This points to a server
+    /// which should process the given query. Currently, we only
+    /// support load balancing between two servers, therefore this
+    /// value should be 0 or 1.
+    ///
+    /// @param query6 pointer to the DHCPv6 query instance.
+    /// @return Index of the server which should process the query. It
+    /// returns negative value if the query is malformed, i.e. contains
+    /// no DUID.
+    int loadBalance(const dhcp::Pkt6Ptr& query6) const;
+
+    /// @brief Compute load balancing hash.
+    ///
+    /// The load balancing hash is computed according to section 6
+    /// if RFC3074.
+    ///
+    /// @param key identifier used to compute a hash, i.e. HW address
+    /// or client identifier.
+    /// @param key_len length of the key.
+    ///
+    /// @return Computed hash value.
+    uint8_t loadBalanceHash(const uint8_t* key, const size_t key_len) const;
+
+    /// @brief Checks if the scope name matches a name of any of the
+    /// configured servers.
+    ///
+    /// @param scope_name scope name to be tested.
+    /// @throw BadValue if no server is found for a given scope name.
+    void validateScopeName(const std::string& scope_name) const;
+
+    /// @brief Returns scope class name for the specified scope name.
+    ///
+    /// When the server is designated to process a received DHCP query, it
+    /// is often required to assign a class to this query which corresponds
+    /// to the particular server. This class name is associated with the
+    /// pools, subnets and/or networks which this server should hand out
+    /// leases from.
+    ///
+    /// This function converts scope name to the class name by prefixing
+    /// the scope name with "ha_" string.
+    ///
+    /// @param scope_name scope name to be converted to class name.
+    ///
+    /// @return Scope class name.
+    std::string makeScopeClass(const std::string& scope_name) const;
+
+    /// @brief Pointer to the HA configuration.
+    HAConfigPtr config_;
+
+    /// @brief Vector of HA peers configurations.
+    std::vector<HAConfig::PeerConfigPtr> peers_;
+
+    /// @brief Holds mapping of the scope names to the flag which indicates
+    /// if the scopes are enabled or disabled.
+    std::map<std::string, bool> scopes_;
+
+    /// @brief Number of the active servers in the given HA mode.
+    int active_servers_;
+};
+
+} // end of namespace isc::ha
+} // end of namespace isc
+
+#endif // QUERY_FILTER_H
diff --git a/src/hooks/dhcp/high_availability/tests/.gitignore b/src/hooks/dhcp/high_availability/tests/.gitignore
new file mode 100644 (file)
index 0000000..bf94471
--- /dev/null
@@ -0,0 +1,4 @@
+/ha_unittests
+/ha_unittests.log
+/ha_unittests.trs
+/test-suite.log
diff --git a/src/hooks/dhcp/high_availability/tests/Makefile.am b/src/hooks/dhcp/high_availability/tests/Makefile.am
new file mode 100644 (file)
index 0000000..40296c8
--- /dev/null
@@ -0,0 +1,64 @@
+SUBDIRS = .
+
+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += -I$(top_builddir)/src/hooks/dhcp/high_availability -I$(top_srcdir)/src/hooks/dhcp/high_availability
+AM_CPPFLAGS += $(BOOST_INCLUDES)
+AM_CPPFLAGS += -DLIBDHCP_HA_SO=\"$(abs_top_builddir)/src/hooks/dhcp/high_availability/.libs/libdhcp_ha.so\"
+AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
+
+AM_CXXFLAGS = $(KEA_CXXFLAGS)
+
+if USE_STATIC_LINK
+AM_LDFLAGS = -static
+endif
+
+# Unit test data files need to get installed.
+EXTRA_DIST =
+
+CLEANFILES = *.gcno *.gcda
+
+TESTS_ENVIRONMENT = \
+       $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
+
+TESTS =
+if HAVE_GTEST
+TESTS += ha_unittests
+
+ha_unittests_SOURCES  = command_creator_unittest.cc
+ha_unittests_SOURCES += communication_state_unittest.cc
+ha_unittests_SOURCES += ha_config_unittest.cc
+ha_unittests_SOURCES += ha_impl_unittest.cc
+ha_unittests_SOURCES += ha_service_unittest.cc
+ha_unittests_SOURCES += ha_test.cc ha_test.h
+ha_unittests_SOURCES += query_filter_unittest.cc
+ha_unittests_SOURCES += run_unittests.cc
+
+ha_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
+
+ha_unittests_LDFLAGS  = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS)
+
+ha_unittests_CXXFLAGS = $(AM_CXXFLAGS)
+
+ha_unittests_LDADD  = $(top_builddir)/src/hooks/dhcp/high_availability/libha.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/http/libkea-http.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la
+ha_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
+ha_unittests_LDADD += $(LOG4CPLUS_LIBS)
+ha_unittests_LDADD += $(CRYPTO_LIBS)
+ha_unittests_LDADD += $(BOOST_LIBS)
+ha_unittests_LDADD += $(GTEST_LDADD)
+endif
+noinst_PROGRAMS = $(TESTS)
diff --git a/src/hooks/dhcp/high_availability/tests/command_creator_unittest.cc b/src/hooks/dhcp/high_availability/tests/command_creator_unittest.cc
new file mode 100644 (file)
index 0000000..85c7036
--- /dev/null
@@ -0,0 +1,261 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <config.h>
+
+#include <ha_server_type.h>
+#include <command_creator.h>
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <dhcp/hwaddr.h>
+#include <dhcpsrv/lease.h>
+#include <boost/pointer_cast.hpp>
+#include <gtest/gtest.h>
+#include <vector>
+
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::ha;
+
+namespace {
+
+/// @brief Creates lease instance used in tests.
+///
+/// It creates a lease with an address of 192.1.2.3 and for the HW address
+/// of 0b:0b:0b:0b:0b:0b.
+///
+/// @return Pointer to the created lease.
+Lease4Ptr createLease4() {
+    HWAddrPtr hwaddr(new HWAddr(std::vector<uint8_t>(6, 11), HTYPE_ETHER));
+    Lease4Ptr lease4(new Lease4(IOAddress("192.1.2.3"), hwaddr,
+                                static_cast<const uint8_t*>(0), 0,
+                                60, 30, 40, 0, 1));
+    return (lease4);
+}
+
+/// @brief Creates IPv6 lease instance used in tests.
+///
+/// It creates a lease with an address of 2001:db8:1::cafe and for the
+/// DUID of 02:02:02:02:02:02:02:02.
+///
+/// @return Pointer to the created lease.
+Lease6Ptr createLease6() {
+    DuidPtr duid(new DUID(std::vector<uint8_t>(8, 02)));
+    Lease6Ptr lease6(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::cafe"),
+                                duid, 1234, 50, 60, 30, 40, 1));
+    return (lease6);
+}
+
+/// @brief Returns JSON representation of the lease.
+///
+/// @param lease_ptr Pointer to the lease.
+/// @return Pointer to the JSON representation of the lease.
+ElementPtr leaseAsJson(const LeasePtr& lease_ptr) {
+    ElementPtr lease = boost::const_pointer_cast<Element>(lease_ptr->toElement());
+    // Replace cltt with expiration time as this is what the lease_cmds hooks
+    // library expects.
+    int64_t cltt = lease->get("cltt")->intValue();
+    int64_t valid_lifetime = lease->get("valid-lft")->intValue();
+    int64_t expire = cltt + valid_lifetime;
+    lease->set("expire", Element::create(expire));
+    lease->remove("cltt");
+    return (lease);
+}
+
+/// @brief Performs basic checks on the command.
+///
+/// It tests whether the command name is correct, if it contains arguments
+/// and if the arguments are held in a map.
+///
+/// @param command Pointer to the command to be tested.
+/// @param expected_command Expected command name.
+/// @param expected_service Expected service name.
+/// @param [out] arguments Pointer to the arguments map extracted from the
+/// provided command.
+void
+testCommandBasics(const ConstElementPtr& command,
+                  const std::string& expected_command,
+                  const std::string& expected_service,
+                  ConstElementPtr& arguments) {
+    ASSERT_TRUE(command);
+    ASSERT_EQ(Element::map, command->getType());
+    ConstElementPtr command_name = command->get("command");
+    ASSERT_TRUE(command_name);
+
+    // Make sure the command is a string.
+    ASSERT_EQ(Element::string, command_name->getType());
+    // Verify the command name.
+    ASSERT_EQ(expected_command, command_name->stringValue());
+
+    // Make sure that the service is present and includes a single
+    // entry.
+    ConstElementPtr service = command->get("service");
+    ASSERT_TRUE(service);
+    ASSERT_EQ(Element::list, service->getType());
+    ASSERT_EQ(1, service->size());
+    ASSERT_EQ(Element::string, service->get(0)->getType());
+    EXPECT_EQ(expected_service, service->get(0)->stringValue());
+
+    // Make sure that arguments are present.
+    ConstElementPtr command_arguments = command->get("arguments");
+    ASSERT_TRUE(command_arguments);
+    // Make sure that arguments are held in a map.
+    ASSERT_EQ(Element::map, command_arguments->getType());
+
+    // Return extracted arguments.
+    arguments = command_arguments;
+}
+
+/// @brief Performs basic checks on the command.
+///
+/// This variant of the function expects no arguments to be provided.
+///
+/// @param command Pointer to the command to be tested.
+/// @param expected_command Expected command name.
+/// @param expected_service Expected service name.
+void
+testCommandBasics(const ConstElementPtr& command,
+                  const std::string& expected_command,
+                  const std::string& expected_service) {
+    ASSERT_TRUE(command);
+    ASSERT_EQ(Element::map, command->getType());
+    ConstElementPtr command_name = command->get("command");
+    ASSERT_TRUE(command_name);
+
+    // Make sure the command is a string.
+    ASSERT_EQ(Element::string, command_name->getType());
+    // Verify the command name.
+    ASSERT_EQ(expected_command, command_name->stringValue());
+
+    // Make sure that the service is present and includes a single
+    // entry.
+    ConstElementPtr service = command->get("service");
+    ASSERT_TRUE(service);
+    ASSERT_EQ(Element::list, service->getType());
+    ASSERT_EQ(1, service->size());
+    ASSERT_EQ(Element::string, service->get(0)->getType());
+    EXPECT_EQ(expected_service, service->get(0)->stringValue());
+
+    ConstElementPtr command_arguments = command->get("arguments");
+    ASSERT_FALSE(command_arguments);
+}
+
+// This test verifies that the dhcp-disable command is correct.
+TEST(CommandCreatorTest, createDHCPDisable4) {
+    // Create command with max-period value set to 20.
+    ConstElementPtr command = CommandCreator::createDHCPDisable(20, HAServerType::DHCPv4);
+    ConstElementPtr arguments;
+    ASSERT_NO_FATAL_FAILURE(testCommandBasics(command, "dhcp-disable", "dhcp4",
+                                              arguments));
+    ConstElementPtr max_period = arguments->get("max-period");
+    ASSERT_TRUE(max_period);
+    ASSERT_EQ(Element::integer, max_period->getType());
+    EXPECT_EQ(20, max_period->intValue());
+
+    // Repeat the test but this time the max-period is not specified.
+    command = CommandCreator::createDHCPDisable(0, HAServerType::DHCPv4);
+    ASSERT_NO_FATAL_FAILURE(testCommandBasics(command, "dhcp-disable", "dhcp4"));
+}
+
+// This test verifies that the dhcp-enable command is correct.
+TEST(CommandCreatorTest, createDHCPEnable4) {
+    ConstElementPtr command = CommandCreator::createDHCPEnable(HAServerType::DHCPv4);
+    ASSERT_NO_FATAL_FAILURE(testCommandBasics(command, "dhcp-enable", "dhcp4"));
+}
+
+// This test verifies that the ha-heartbeat command is correct.
+TEST(CommandCreatorTest, createHeartbeat4) {
+    ConstElementPtr command = CommandCreator::createHeartbeat(HAServerType::DHCPv4);
+    ASSERT_NO_FATAL_FAILURE(testCommandBasics(command, "ha-heartbeat", "dhcp4"));
+}
+
+// This test verifies that the command generated for the lease update
+// is correct.
+TEST(CommandCreatorTest, createLease4Update) {
+    ConstElementPtr command = CommandCreator::createLease4Update(*createLease4());
+    ConstElementPtr arguments;
+    ASSERT_NO_FATAL_FAILURE(testCommandBasics(command, "lease4-update", "dhcp4",
+                                              arguments));
+    ElementPtr lease_as_json = leaseAsJson(createLease4());
+    // The lease update must contain the "force-create" parameter indicating that
+    // the lease must be created if it doesn't exist.
+    lease_as_json->set("force-create", Element::create(true));
+    EXPECT_EQ(lease_as_json->str(), arguments->str());
+}
+
+// This test verifies that the command generated for the lease deletion
+// is correct.
+TEST(CommandCreatorTest, createLease4Delete) {
+    ConstElementPtr command = CommandCreator::createLease4Delete(*createLease4());
+    ConstElementPtr arguments;
+    ASSERT_NO_FATAL_FAILURE(testCommandBasics(command, "lease4-del", "dhcp4",
+                                              arguments));
+    ElementPtr lease_as_json = leaseAsJson(createLease4());
+    EXPECT_EQ(lease_as_json->str(), arguments->str());
+}
+
+// This test verifies that the lease4-get-all command is correct.
+TEST(CommandCreatorTest, createLease4GetAll) {
+    ConstElementPtr command = CommandCreator::createLease4GetAll();
+    ASSERT_NO_FATAL_FAILURE(testCommandBasics(command, "lease4-get-all", "dhcp4"));
+}
+
+// This test verifies that the dhcp-disable command (DHCPv6 case) is
+// correct.
+TEST(CommandCreatorTest, createDHCPDisable6) {
+    // Create command with max-period value set to 20.
+    ConstElementPtr command = CommandCreator::createDHCPDisable(20, HAServerType::DHCPv6);
+    ConstElementPtr arguments;
+    ASSERT_NO_FATAL_FAILURE(testCommandBasics(command, "dhcp-disable", "dhcp6",
+                                              arguments));
+    ConstElementPtr max_period = arguments->get("max-period");
+    ASSERT_TRUE(max_period);
+    ASSERT_EQ(Element::integer, max_period->getType());
+    EXPECT_EQ(20, max_period->intValue());
+
+    // Repeat the test but this time the max-period is not specified.
+    command = CommandCreator::createDHCPDisable(0, HAServerType::DHCPv6);
+    ASSERT_NO_FATAL_FAILURE(testCommandBasics(command, "dhcp-disable", "dhcp6"));
+}
+
+// This test verifies that the dhcp-enable command (DHCPv6 case) is
+// correct.
+TEST(CommandCreatorTest, createDHCPEnable6) {
+    ConstElementPtr command = CommandCreator::createDHCPEnable(HAServerType::DHCPv6);
+    ASSERT_NO_FATAL_FAILURE(testCommandBasics(command, "dhcp-enable", "dhcp6"));
+}
+
+TEST(CommandCreatorTest, createLease6Update) {
+    ConstElementPtr command = CommandCreator::createLease6Update(*createLease6());
+    ConstElementPtr arguments;
+    ASSERT_NO_FATAL_FAILURE(testCommandBasics(command, "lease6-update", "dhcp6",
+                                              arguments));
+    ElementPtr lease_as_json = leaseAsJson(createLease6());
+    // The lease update must contain the "force-create" parameter indicating that
+    // the lease must be created if it doesn't exist.
+    lease_as_json->set("force-create", Element::create(true));
+    EXPECT_EQ(lease_as_json->str(), arguments->str());
+}
+
+// This test verifies that the command generated for the lease deletion
+// is correct.
+TEST(CommandCreatorTest, createLease6Delete) {
+    ConstElementPtr command = CommandCreator::createLease6Delete(*createLease6());
+    ConstElementPtr arguments;
+    ASSERT_NO_FATAL_FAILURE(testCommandBasics(command, "lease6-del", "dhcp6",
+                                              arguments));
+    ElementPtr lease_as_json = leaseAsJson(createLease6());
+    EXPECT_EQ(lease_as_json->str(), arguments->str());
+}
+
+// This test verifies that the lease6-get-all command is correct.
+TEST(CommandCreatorTest, createLease6GetAll) {
+    ConstElementPtr command = CommandCreator::createLease6GetAll();
+    ASSERT_NO_FATAL_FAILURE(testCommandBasics(command, "lease6-get-all", "dhcp6"));
+}
+
+
+}
diff --git a/src/hooks/dhcp/high_availability/tests/communication_state_unittest.cc b/src/hooks/dhcp/high_availability/tests/communication_state_unittest.cc
new file mode 100644 (file)
index 0000000..5796131
--- /dev/null
@@ -0,0 +1,380 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <config.h>
+
+#include <ha_test.h>
+#include <asiolink/asio_wrapper.h>
+#include <communication_state.h>
+#include <asiolink/interval_timer.h>
+#include <asiolink/io_service.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <exceptions/exceptions.h>
+#include <http/date_time.h>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <boost/bind.hpp>
+#include <boost/function.hpp>
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::ha;
+using namespace isc::ha::test;
+using namespace isc::http;
+
+namespace {
+
+
+/// @brief Test fixture class for @c CommunicationState class.
+class CommunicationStateTest : public HATest {
+public:
+
+    /// @brief Constructor.
+    CommunicationStateTest()
+        : state_(io_service_, createValidConfiguration()),
+          state6_(io_service_, createValidConfiguration()) {
+    }
+
+    /// @brief Destructor.
+    ~CommunicationStateTest() {
+        io_service_->poll();
+    }
+
+    /// @brief Returns test heartbeat implementation.
+    ///
+    /// @return Pointer to heartbeat implementation function under test.
+    boost::function<void()> getHeartbeatImpl() {
+        return (boost::bind(&CommunicationStateTest::heartbeatImpl, this));
+    }
+
+    /// @brief Test heartbeat implementation.
+    ///
+    /// It simply pokes the communication state object. Note that the real
+    /// implementation would send an actual heartbeat command prior to
+    /// poking the state.
+    void heartbeatImpl() {
+        state_.poke();
+    }
+
+    /// @brief Communication state object used throughout the tests.
+    NakedCommunicationState4 state_;
+
+    /// @brief Communication state for IPv6 used throughout the tests.
+    NakedCommunicationState6 state6_;
+};
+
+// Verifies that the partner state is set and retrieved correctly.
+TEST_F(CommunicationStateTest, partnerState) {
+    // Initially the state is unknown.
+    EXPECT_LT(state_.getPartnerState(), 0);
+
+    state_.setPartnerState("hot-standby");
+    EXPECT_EQ(HA_HOT_STANDBY_ST, state_.getPartnerState());
+
+    state_.setPartnerState("load-balancing");
+    EXPECT_EQ(HA_LOAD_BALANCING_ST, state_.getPartnerState());
+
+    state_.setPartnerState("partner-down");
+    EXPECT_EQ(HA_PARTNER_DOWN_ST, state_.getPartnerState());
+
+    state_.setPartnerState("ready");
+    EXPECT_EQ(HA_READY_ST, state_.getPartnerState());
+
+    state_.setPartnerState("syncing");
+    EXPECT_EQ(HA_SYNCING_ST, state_.getPartnerState());
+
+    state_.setPartnerState("terminated");
+    EXPECT_EQ(HA_TERMINATED_ST, state_.getPartnerState());
+
+    state_.setPartnerState("waiting");
+    EXPECT_EQ(HA_WAITING_ST, state_.getPartnerState());
+
+    state_.setPartnerState("unavailable");
+    EXPECT_EQ(HA_UNAVAILABLE_ST, state_.getPartnerState());
+
+    // An attempt to set unsupported value should result in exception.
+    EXPECT_THROW(state_.setPartnerState("unsupported"), BadValue);
+
+}
+
+// Verifies that the object is poked right after construction.
+TEST_F(CommunicationStateTest, initialDuration) {
+    EXPECT_TRUE(state_.isPoked());
+}
+
+// Verifies that  poking the state updates the returned duration.
+TEST_F(CommunicationStateTest, poke) {
+    state_.modifyPokeTime(-30);
+    ASSERT_GE(state_.getDurationInMillisecs(), 30000);
+    ASSERT_TRUE(state_.isCommunicationInterrupted());
+    ASSERT_NO_THROW(state_.poke());
+    EXPECT_TRUE(state_.isPoked());
+    EXPECT_FALSE(state_.isCommunicationInterrupted());
+}
+
+// Test that heartbeat function is triggered.
+TEST_F(CommunicationStateTest, heartbeat) {
+    // Set poke time to the past and expect that the object is considered
+    // not poked.
+    state_.modifyPokeTime(-30);
+    EXPECT_FALSE(state_.isPoked());
+
+    // Run heartbeat every 1 second.
+    ASSERT_NO_THROW(state_.startHeartbeat(1, getHeartbeatImpl()));
+    runIOService(1200);
+
+    // After > than 1 second the state should have been poked.
+    EXPECT_TRUE(state_.isPoked());
+
+    // Repeat the test.
+    state_.modifyPokeTime(-30);
+    EXPECT_FALSE(state_.isPoked());
+    ASSERT_NO_THROW(state_.startHeartbeat(1, getHeartbeatImpl()));
+    runIOService(1200);
+    EXPECT_TRUE(state_.isPoked());
+}
+
+// Test that invalid values provided to startHeartbeat are rejected.
+TEST_F(CommunicationStateTest, startHeartbeatInvalidValues) {
+    EXPECT_THROW(state_.startHeartbeat(-1, getHeartbeatImpl()), BadValue);
+    EXPECT_THROW(state_.startHeartbeat(0, getHeartbeatImpl()), BadValue);
+    EXPECT_THROW(state_.startHeartbeat(1, 0), BadValue);
+}
+
+// Test that failure detection works properly for DHCPv4 case.
+TEST_F(CommunicationStateTest, detectFailureV4) {
+    // Initially, there should be no unacked clients recorded.
+    ASSERT_FALSE(state_.failureDetected());
+
+    // The maximum number of unacked clients is 10. Let's provide 10
+    // DHCPDISCOVER messages with the "secs" value of 15 which exceeds
+    // the threshold of 10. All these clients should be recorded as
+    // unacked.
+    for (uint8_t i = 0; i < 10; ++i) {
+        // Some of the requests have no client identifier to test that
+        // we don't fall over if the client identifier is null.
+        const uint8_t client_id_seed = (i < 5 ? i : 0);
+        ASSERT_NO_THROW(state_.analyzeMessage(createMessage4(DHCPDISCOVER, i,
+                                                             client_id_seed,
+                                                             15)));
+        // We don't exceed the maximum of number of unacked clients so the
+        // partner failure shouldn't be reported.
+        ASSERT_FALSE(state_.failureDetected())
+            << "failure detected for the request number "
+            << static_cast<int>(i);
+    }
+
+    // Let's provide similar set of requests but this time the "secs" field is
+    // below the threshold. They should not be counted as failures. Also,
+    // all of these requests have client identifier.
+    for (uint8_t i = 0; i < 10; ++i) {
+        ASSERT_NO_THROW(state_.analyzeMessage(createMessage4(DHCPDISCOVER, i, i,
+                                                             9)));
+        ASSERT_FALSE(state_.failureDetected())
+            << "failure detected for the request number "
+            << static_cast<int>(i);
+    }
+
+    // Let's create a message from a new (not recorded yet) client with the
+    // "secs" field value below the threshold. It should not be recorded.
+    ASSERT_NO_THROW(state_.analyzeMessage(createMessage4(DHCPDISCOVER, 10, 10, 6)));
+
+    // Still no failure.
+    ASSERT_FALSE(state_.failureDetected());
+
+    // Let's repeat one of the requests which already have been recorded as
+    // unacked but with a greater value of "secs" field. This should not
+    // be counted because only new clients count.
+    ASSERT_NO_THROW(state_.analyzeMessage(createMessage4(DHCPDISCOVER, 3, 3, 20)));
+    ASSERT_FALSE(state_.failureDetected());
+
+    // This time let's simulate a client with a MAC address already recorded but
+    // with a client identifier. This should be counted as a new unacked request.
+    ASSERT_NO_THROW(state_.analyzeMessage(createMessage4(DHCPDISCOVER, 7, 7, 15)));
+    ASSERT_TRUE(state_.failureDetected());
+
+    // Poking should cause all counters to reset as it is an indication that the
+    // control connection has been re-established.
+    ASSERT_NO_THROW(state_.poke());
+
+    // We're back to no failure state.
+    EXPECT_FALSE(state_.failureDetected());
+
+    // Send 11 DHCPDISCOVER messages with the "secs" field bytes swapped. Swapping
+    // bytes was reported for some misbehaving Windows clients. The server should
+    // detect bytes swapping when second byte is 0 and the first byte is non-zero.
+    // However, the first byte is equal to 5 which is below our threshold so none
+    // of the requests below should count as unacked.
+    for (uint8_t i = 0; i < 11; ++i) {
+        ASSERT_NO_THROW(state_.analyzeMessage(createMessage4(DHCPDISCOVER, i, i,
+                                                             0x0500)));
+        ASSERT_FALSE(state_.failureDetected())
+            << "failure detected for the request number "
+            << static_cast<int>(i)
+            << " when testing swapped secs field bytes";
+    }
+
+    // Repeat the same test, but this time either the first byte exceeds the
+    // secs threshold or the second byte is non-zero. All should be counted
+    // as unacked.
+    for (uint8_t i = 0; i < 10; ++i) {
+        uint16_t secs = (i % 2 == 0 ? 0x0F00 : 0x0501);
+        ASSERT_NO_THROW(state_.analyzeMessage(createMessage4(DHCPDISCOVER, i, i,
+                                                             secs)));
+        ASSERT_FALSE(state_.failureDetected())
+            << "failure detected for the request number "
+            << static_cast<int>(i)
+            << " when testing swapped secs field bytes";
+    }
+
+    // This last message should cause the failure state.
+    ASSERT_NO_THROW(state_.analyzeMessage(createMessage4(DHCPDISCOVER, 11, 11,
+                                                         0x30)));
+    EXPECT_TRUE(state_.failureDetected());
+}
+
+// This test verifies that it is possible to disable analysis of the DHCPv4
+// packets in which case the partner's failure is assumed when there is
+// no connection over the control channel.
+TEST_F(CommunicationStateTest, failureDetectionDisabled4) {
+    state_.config_->setMaxUnackedClients(0);
+    EXPECT_TRUE(state_.failureDetected());
+}
+
+// Test that failure detection works properly for DHCPv6 case.
+TEST_F(CommunicationStateTest, detectFailureV6) {
+    // Initially, there should be no unacked clients recorded.
+    ASSERT_FALSE(state6_.failureDetected());
+
+    // The maximum number of unacked clients is 10. Let's provide 10
+    // Solicit messages with the "elapsed time" value of 1500 which exceeds
+    // the threshold of 10000ms. Note that the elapsed time value is provided
+    // in 1/100s of 1 second. All these clients should be recorded as
+    // unacked.
+    for (uint8_t i = 0; i < 10; ++i) {
+        ASSERT_NO_THROW(state6_.analyzeMessage(createMessage6(DHCPV6_SOLICIT, i,
+                                                              1500)));
+        // We don't exceed the maximum number of unacked clients so the
+        // partner failure shouldn't be reported.
+        ASSERT_FALSE(state6_.failureDetected())
+            << "failure detected for the request number "
+            << static_cast<int>(i);
+    }
+
+    // Let's provide similar set of requests but this time the "elapsed time" is
+    // below the threshold. They should not be counted as failures. Also,
+    // all of these requests have client identifier.
+    for (uint8_t i = 0; i < 10; ++i) {
+        ASSERT_NO_THROW(state6_.analyzeMessage(createMessage6(DHCPV6_SOLICIT, i,
+                                                             900)));
+        ASSERT_FALSE(state6_.failureDetected())
+            << "failure detected for the request number "
+            << static_cast<int>(i);
+    }
+
+    // Let's create a message from a new (not recorded yet) client with the
+    // "elapsed time" value below the threshold. It should not be recorded.
+    ASSERT_NO_THROW(state6_.analyzeMessage(createMessage6(DHCPV6_SOLICIT, 10, 600)));
+
+    // Still no failure.
+    ASSERT_FALSE(state6_.failureDetected());
+
+    // Let's repeat one of the requests which already have been recorded as
+    // unacked but with a greater value of "elapsed time". This should not
+    // be counted because only new clients count.
+    ASSERT_NO_THROW(state6_.analyzeMessage(createMessage6(DHCPV6_SOLICIT, 3, 2000)));
+    ASSERT_FALSE(state6_.failureDetected());
+
+    // New unacked client should cause failure to the detected.
+    ASSERT_NO_THROW(state6_.analyzeMessage(createMessage6(DHCPV6_SOLICIT, 11, 1500)));
+    ASSERT_TRUE(state6_.failureDetected());
+
+    // Poking should cause all counters to reset as it is an indication that the
+    // control connection has been re-established.
+    ASSERT_NO_THROW(state6_.poke());
+
+    // We're back to no failure state.
+    EXPECT_FALSE(state6_.failureDetected());
+}
+
+// This test verifies that it is possible to disable analysis of the DHCPv6
+// packets in which case the partner's failure is assumed when there is
+// no connection over the control channel.
+TEST_F(CommunicationStateTest, failureDetectionDisabled6) {
+    state6_.config_->setMaxUnackedClients(0);
+    EXPECT_TRUE(state6_.failureDetected());
+}
+
+// This test verifies that the clock skew is checked properly by the
+// clockSkewShouldWarn and clockSkewShouldTerminate functions.
+TEST_F(CommunicationStateTest, clockSkew) {
+    // Default clock skew is 0.
+    EXPECT_FALSE(state_.clockSkewShouldWarn());
+    EXPECT_FALSE(state_.clockSkewShouldTerminate());
+    state_.setPartnerTime(HttpDateTime().rfc1123Format());
+
+    // Partner time is ahead by 15s (no warning).
+    state_.clock_skew_ += boost::posix_time::time_duration(0, 0, 15);
+    EXPECT_FALSE(state_.clockSkewShouldWarn());
+    EXPECT_FALSE(state_.clockSkewShouldTerminate());
+
+    // Partner time is behind by 15s (no warning).
+    state_.setPartnerTime(HttpDateTime().rfc1123Format());
+    state_.clock_skew_ -= boost::posix_time::time_duration(0, 0, 15);
+    EXPECT_FALSE(state_.clockSkewShouldWarn());
+    EXPECT_FALSE(state_.clockSkewShouldTerminate());
+
+    // Partner time is ahead by 35s (warning).
+    state_.setPartnerTime(HttpDateTime().rfc1123Format());
+    state_.clock_skew_ += boost::posix_time::time_duration(0, 0, 35);
+    EXPECT_TRUE(state_.clockSkewShouldWarn());
+    EXPECT_FALSE(state_.clockSkewShouldTerminate());
+
+    // Partner time is behind by 35s (warning).
+    state_.setPartnerTime(HttpDateTime().rfc1123Format());
+    state_.clock_skew_ -= boost::posix_time::time_duration(0, 0, 35);
+    state_.last_clock_skew_warn_ = boost::posix_time::ptime();
+    EXPECT_TRUE(state_.clockSkewShouldWarn());
+    EXPECT_FALSE(state_.clockSkewShouldTerminate());
+
+    // Due to the gating mechanism this should not return true the second
+    // time.
+    EXPECT_FALSE(state_.clockSkewShouldWarn());
+
+    // But should warn if the warning was issued more than 60 seconds ago.
+    state_.last_clock_skew_warn_ -= boost::posix_time::time_duration(0, 1, 30);
+    EXPECT_TRUE(state_.clockSkewShouldWarn());
+
+    // Partner time is ahead by 65s (warning and terminate).
+    state_.setPartnerTime(HttpDateTime().rfc1123Format());
+    state_.clock_skew_ += boost::posix_time::time_duration(0, 1, 5);
+    state_.last_clock_skew_warn_ = boost::posix_time::ptime();
+    EXPECT_TRUE(state_.clockSkewShouldWarn());
+    EXPECT_TRUE(state_.clockSkewShouldTerminate());
+
+    // Partner time is behind by 65s (warning and terminate).
+    state_.setPartnerTime(HttpDateTime().rfc1123Format());
+    state_.clock_skew_ -= boost::posix_time::time_duration(0, 1, 5);
+    state_.last_clock_skew_warn_ = boost::posix_time::ptime();
+    EXPECT_TRUE(state_.clockSkewShouldWarn());
+    EXPECT_TRUE(state_.clockSkewShouldTerminate());
+}
+
+// This test verifies that the clock skew value is formatted correctly
+// for logging.
+TEST_F(CommunicationStateTest, logFormatClockSkew) {
+    // Partner time is ahead by 15s.
+    state_.clock_skew_ += boost::posix_time::time_duration(0, 0, 15);
+    EXPECT_EQ("15s ahead", state_.logFormatClockSkew());
+
+    // Partner time is behind by 1m23s.
+    state_.setPartnerTime(HttpDateTime().rfc1123Format());
+    state_.clock_skew_ -= boost::posix_time::time_duration(0, 1, 23);
+    EXPECT_EQ("83s behind", state_.logFormatClockSkew());
+}
+
+}
diff --git a/src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc b/src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc
new file mode 100644 (file)
index 0000000..74ed152
--- /dev/null
@@ -0,0 +1,718 @@
+// Copyright (C) 2017-2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <config.h>
+
+#include <ha_impl.h>
+#include <ha_test.h>
+#include <cc/command_interpreter.h>
+#include <cc/data.h>
+#include <cc/dhcp_config_error.h>
+#include <config/command_mgr.h>
+#include <string>
+
+using namespace isc;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::ha;
+using namespace isc::hooks;
+using namespace isc::ha::test;
+
+namespace {
+
+/// @brief Test fixture class for testing HA hooks library
+/// configuration.
+class HAConfigTest : public HATest {
+public:
+
+    /// @brief Constructor.
+    HAConfigTest()
+        : HATest() {
+    }
+
+    /// @brief Verifies if an exception is thrown if provided HA
+    /// configuration is invalid.
+    ///
+    /// @param invalid_config Configuration to be tested.
+    /// @param expected_error Expected error message.
+    void testInvalidConfig(const std::string& invalid_config,
+                           const std::string& expected_error) {
+        HAImplPtr impl(new HAImpl());
+        try {
+            impl->configure(Element::fromJSON(invalid_config));
+
+        } catch (const ConfigError& ex) {
+            EXPECT_EQ(expected_error, std::string(ex.what()));
+
+        } catch (...) {
+            ADD_FAILURE() << "expected ConfigError exception, thrown different"
+                " exception type";
+        }
+    }
+};
+
+// Verifies that load balancing configuration is parsed correctly.
+TEST_F(HAConfigTest, configureLoadBalancing) {
+    const std::string ha_config =
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"load-balancing\","
+        "        \"send-lease-updates\": false,"
+        "        \"sync-leases\": false,"
+        "        \"heartbeat-delay\": 8,"
+        "        \"max-response-delay\": 11,"
+        "        \"max-ack-delay\": 5,"
+        "        \"max-unacked-clients\": 20,"
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8081/\","
+        "                \"role\": \"secondary\""
+        "            },"
+        "            {"
+        "                \"name\": \"server3\","
+        "                \"url\": \"http://127.0.0.1:8082/\","
+        "                \"role\": \"backup\","
+        "                \"auto-failover\": false"
+        "            }"
+        "        ]"
+        "    }"
+        "]";
+
+    HAImplPtr impl(new HAImpl());
+    ASSERT_NO_THROW(impl->configure(Element::fromJSON(ha_config)));
+    EXPECT_EQ("server1", impl->getConfig()->getThisServerName());
+    EXPECT_EQ(HAConfig::LOAD_BALANCING, impl->getConfig()->getHAMode());
+    EXPECT_FALSE(impl->getConfig()->amSendingLeaseUpdates());
+    EXPECT_FALSE(impl->getConfig()->amSyncingLeases());
+    EXPECT_EQ(8, impl->getConfig()->getHeartbeatDelay());
+    EXPECT_EQ(11, impl->getConfig()->getMaxResponseDelay());
+    EXPECT_EQ(5, impl->getConfig()->getMaxAckDelay());
+    EXPECT_EQ(20, impl->getConfig()->getMaxUnackedClients());
+
+    HAConfig::PeerConfigPtr cfg = impl->getConfig()->getThisServerConfig();
+    ASSERT_TRUE(cfg);
+    EXPECT_EQ("server1", cfg->getName());
+    EXPECT_EQ("http://127.0.0.1:8080/", cfg->getUrl().toText());
+    EXPECT_EQ(cfg->getLogLabel(), "server1 (http://127.0.0.1:8080/)");
+    EXPECT_EQ(HAConfig::PeerConfig::PRIMARY, cfg->getRole());
+    EXPECT_FALSE(cfg->isAutoFailover());
+
+    cfg = impl->getConfig()->getPeerConfig("server2");
+    ASSERT_TRUE(cfg);
+    EXPECT_EQ("server2", cfg->getName());
+    EXPECT_EQ("http://127.0.0.1:8081/", cfg->getUrl().toText());
+    EXPECT_EQ(cfg->getLogLabel(), "server2 (http://127.0.0.1:8081/)");
+    EXPECT_EQ(HAConfig::PeerConfig::SECONDARY, cfg->getRole());
+    EXPECT_TRUE(cfg->isAutoFailover());
+
+    cfg = impl->getConfig()->getPeerConfig("server3");
+    ASSERT_TRUE(cfg);
+    EXPECT_EQ("server3", cfg->getName());
+    EXPECT_EQ("http://127.0.0.1:8082/", cfg->getUrl().toText());
+    EXPECT_EQ(cfg->getLogLabel(), "server3 (http://127.0.0.1:8082/)");
+    EXPECT_EQ(HAConfig::PeerConfig::BACKUP, cfg->getRole());
+    EXPECT_FALSE(cfg->isAutoFailover());
+}
+
+// Verifies that load balancing configuration is parsed correctly.
+TEST_F(HAConfigTest, configureHotStandby) {
+    const std::string ha_config =
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"hot-standby\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8081/\","
+        "                \"role\": \"standby\","
+        "                \"auto-failover\": true"
+        "            },"
+        "            {"
+        "                \"name\": \"server3\","
+        "                \"url\": \"http://127.0.0.1:8082/\","
+        "                \"role\": \"backup\","
+        "                \"auto-failover\": false"
+        "            }"
+        "        ]"
+        "    }"
+        "]";
+
+    HAImplPtr impl(new HAImpl());
+    ASSERT_NO_THROW(impl->configure(Element::fromJSON(ha_config)));
+    EXPECT_EQ("server1", impl->getConfig()->getThisServerName());
+    EXPECT_EQ(HAConfig::HOT_STANDBY, impl->getConfig()->getHAMode());
+    EXPECT_TRUE(impl->getConfig()->amSendingLeaseUpdates());
+    EXPECT_TRUE(impl->getConfig()->amSyncingLeases());
+    EXPECT_EQ(10000, impl->getConfig()->getHeartbeatDelay());
+    EXPECT_EQ(10000, impl->getConfig()->getMaxAckDelay());
+    EXPECT_EQ(10, impl->getConfig()->getMaxUnackedClients());
+
+    HAConfig::PeerConfigPtr cfg = impl->getConfig()->getThisServerConfig();
+    ASSERT_TRUE(cfg);
+    EXPECT_EQ("server1", cfg->getName());
+    EXPECT_EQ("http://127.0.0.1:8080/", cfg->getUrl().toText());
+    EXPECT_EQ(HAConfig::PeerConfig::PRIMARY, cfg->getRole());
+    EXPECT_FALSE(cfg->isAutoFailover());
+
+    cfg = impl->getConfig()->getPeerConfig("server2");
+    ASSERT_TRUE(cfg);
+    EXPECT_EQ("server2", cfg->getName());
+    EXPECT_EQ("http://127.0.0.1:8081/", cfg->getUrl().toText());
+    EXPECT_EQ(HAConfig::PeerConfig::STANDBY, cfg->getRole());
+    EXPECT_TRUE(cfg->isAutoFailover());
+
+    cfg = impl->getConfig()->getPeerConfig("server3");
+    ASSERT_TRUE(cfg);
+    EXPECT_EQ("server3", cfg->getName());
+    EXPECT_EQ("http://127.0.0.1:8082/", cfg->getUrl().toText());
+    EXPECT_EQ(HAConfig::PeerConfig::BACKUP, cfg->getRole());
+    EXPECT_FALSE(cfg->isAutoFailover());
+}
+
+// This server name must not be empty.
+TEST_F(HAConfigTest, emptyServerName) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"\","
+        "        \"mode\": \"load-balancing\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"secondary\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "'this-server-name' value must not be empty");
+}
+
+// There must be a configuration provided for this server.
+TEST_F(HAConfigTest, nonMatchingServerName) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"foo\","
+        "        \"mode\": \"load-balancing\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"secondary\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "no peer configuration specified for the 'foo'");
+}
+
+// Error should be returned when mode is invalid.
+TEST_F(HAConfigTest, unsupportedMode) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"unsupported-mode\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"secondary\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "unsupported value 'unsupported-mode' for mode parameter");
+}
+
+// Error should be returned when heartbeat-delay is negative.
+TEST_F(HAConfigTest, negativeHeartbeatDelay) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"load-balancing\","
+        "        \"heartbeat-delay\": -1,"
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"secondary\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "'heartbeat-delay' must not be negative");
+}
+
+// Error should be returned when heartbeat-delay is too large.
+TEST_F(HAConfigTest, largeHeartbeatDelay) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"load-balancing\","
+        "        \"heartbeat-delay\": 65536,"
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"secondary\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "'heartbeat-delay' must not be greater than 65535");
+}
+
+// There must be at least two servers provided.
+TEST_F(HAConfigTest, singlePeer) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"load-balancing\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "peers configuration requires at least two peers to be specified");
+}
+
+// Server name must not be empty.
+TEST_F(HAConfigTest, emptyPeerName) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"load-balancing\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"secondary\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "peer name must not be empty");
+}
+
+// Can't have two servers with the same name.
+TEST_F(HAConfigTest, duplicatePeerName) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"load-balancing\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"secondary\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "peer with name 'server1' already specified");
+}
+
+// URL must be valid.
+TEST_F(HAConfigTest, invalidURL) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"load-balancing\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"://127.0.0.1:8080/\","
+        "                \"role\": \"secondary\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "invalid URL: url ://127.0.0.1:8080/ lacks http or"
+        " https scheme for server server2");
+}
+
+// Only certain roles are allowed.
+TEST_F(HAConfigTest, unsupportedRole) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"load-balancing\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"unsupported\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"secondary\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "unsupported value 'unsupported' for role parameter");
+}
+
+// There must be exactly one primary server.
+TEST_F(HAConfigTest, twoPrimary) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"load-balancing\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "multiple primary servers specified");
+}
+
+// There must be exactly one secondary server.
+TEST_F(HAConfigTest, twoSecondary) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"load-balancing\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"secondary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"secondary\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "multiple secondary servers specified");
+}
+
+// Only one standby server is allowed.
+TEST_F(HAConfigTest, twoStandby) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"hot-standby\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"standby\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"standby\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "multiple standby servers specified");
+}
+
+// Primary server is required for load balancing.
+TEST_F(HAConfigTest, loadBalancingNoPrimary) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"load-balancing\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"backup\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"secondary\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "primary server required in the load balancing configuration");
+}
+
+// Secondary server is required for load balancing.
+TEST_F(HAConfigTest, loadBalancingNoSecondary) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"load-balancing\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"backup\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "secondary server required in the load balancing configuration");
+}
+
+// Primary server is required for hot standby mode.
+TEST_F(HAConfigTest, hotStandbyNoPrimary) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"hot-standby\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"backup\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"standby\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "primary server required in the hot standby configuration");
+}
+
+// Standby server is required for hot standby mode.
+TEST_F(HAConfigTest, hotStandbyNoStandby) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"hot-standby\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"backup\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "standby server required in the hot standby configuration");
+}
+
+// Standby server must not be specified in the load balancing mode.
+TEST_F(HAConfigTest, loadBalancingStandby) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"load-balancing\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"standby\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "standby servers not allowed in the load balancing configuration");
+}
+
+// Secondary server must not be specified in the hot standby mode.
+TEST_F(HAConfigTest, hotStandbySecondary) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"hot-standby\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"secondary\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "secondary servers not allowed in the hot standby configuration");
+}
+
+// Test that conversion of the role names works correctly.
+TEST_F(HAConfigTest, stringToRole) {
+    EXPECT_EQ(HAConfig::PeerConfig::PRIMARY,
+              HAConfig::PeerConfig::stringToRole("primary"));
+    EXPECT_EQ(HAConfig::PeerConfig::SECONDARY,
+              HAConfig::PeerConfig::stringToRole("secondary"));
+    EXPECT_EQ(HAConfig::PeerConfig::STANDBY,
+              HAConfig::PeerConfig::stringToRole("standby"));
+    EXPECT_EQ(HAConfig::PeerConfig::BACKUP,
+              HAConfig::PeerConfig::stringToRole("backup"));
+    EXPECT_THROW(HAConfig::PeerConfig::stringToRole("unsupported"),
+                 BadValue);
+}
+
+// Test that role name is generated correctly.
+TEST_F(HAConfigTest, roleToString) {
+    EXPECT_EQ("primary",
+              HAConfig::PeerConfig::roleToString(HAConfig::PeerConfig::PRIMARY));
+    EXPECT_EQ("secondary",
+              HAConfig::PeerConfig::roleToString(HAConfig::PeerConfig::SECONDARY));
+    EXPECT_EQ("standby",
+              HAConfig::PeerConfig::roleToString(HAConfig::PeerConfig::STANDBY));
+    EXPECT_EQ("backup",
+              HAConfig::PeerConfig::roleToString(HAConfig::PeerConfig::BACKUP));
+}
+
+// Test that conversion of the HA mode names works correctly.
+TEST_F(HAConfigTest, stringToHAMode) {
+    EXPECT_EQ(HAConfig::LOAD_BALANCING, HAConfig::stringToHAMode("load-balancing"));
+    EXPECT_EQ(HAConfig::HOT_STANDBY, HAConfig::stringToHAMode("hot-standby"));
+}
+
+// Test that HA mode name is generated correctly.
+TEST_F(HAConfigTest, HAModeToString) {
+    EXPECT_EQ("load-balancing", HAConfig::HAModeToString(HAConfig::LOAD_BALANCING));
+    EXPECT_EQ("hot-standby", HAConfig::HAModeToString(HAConfig::HOT_STANDBY));
+}
+
+} // end of anonymous namespace
diff --git a/src/hooks/dhcp/high_availability/tests/ha_impl_unittest.cc b/src/hooks/dhcp/high_availability/tests/ha_impl_unittest.cc
new file mode 100644 (file)
index 0000000..94bc63e
--- /dev/null
@@ -0,0 +1,512 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <config.h>
+
+#include <ha_test.h>
+#include <ha_impl.h>
+#include <asiolink/io_address.h>
+#include <cc/command_interpreter.h>
+#include <cc/data.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/hwaddr.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/network_state.h>
+#include <hooks/hooks_manager.h>
+#include <boost/pointer_cast.hpp>
+#include <gtest/gtest.h>
+#include <string>
+
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::ha;
+using namespace isc::ha::test;
+using namespace isc::hooks;
+
+namespace {
+
+/// @brief Derivation of the @c HAImpl which provides access to protected
+/// methods and members.
+class TestHAImpl : public HAImpl {
+public:
+
+    using HAImpl::config_;
+    using HAImpl::service_;
+};
+
+/// @brief Test fixture class for @c HAImpl.
+class HAImplTest : public HATest {
+public:
+
+    /// @brief Tests handler of a ha-sync command.
+    ///
+    /// It always expects that the error result is returned. The expected
+    /// error text should be provided as function argument.
+    ///
+    /// @param ha_sync_command command provided as text.
+    /// @param expected_response expected text response.
+    void testSynchronizeHandler(const std::string& ha_sync_command,
+                                const std::string& expected_response) {
+        HAImpl ha_impl;
+        ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration()));
+
+        // Starting the service is required prior to running any callouts.
+        NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4));
+        ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state,
+                                             HAServerType::DHCPv4));
+
+        ConstElementPtr command = Element::fromJSON(ha_sync_command);
+
+        CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+        callout_handle->setArgument("command", command);
+
+        ASSERT_NO_THROW(ha_impl.synchronizeHandler(*callout_handle));
+
+        ConstElementPtr response;
+        callout_handle->getArgument("response", response);
+        ASSERT_TRUE(response);
+
+        checkAnswer(response, CONTROL_RESULT_ERROR, expected_response);
+    }
+};
+
+// Tests that HAService object is created for DHCPv4 service.
+TEST_F(HAImplTest, startService) {
+    // Valid configuration must be provided prior to starting the service.
+    TestHAImpl ha_impl;
+    ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration()));
+
+    // Network state is also required.
+    NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4));
+
+    // Start the service for DHCPv4 server.
+    ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state,
+                                         HAServerType::DHCPv4));
+
+    // Make sure that the HA service has been created for the requested
+    // server type.
+    ASSERT_TRUE(ha_impl.service_);
+    EXPECT_EQ(HAServerType::DHCPv4, ha_impl.service_->getServerType());
+}
+
+// Tests that HAService object is created for DHCPv6 service.
+TEST_F(HAImplTest, startService6) {
+    // Valid configuration must be provided prior to starting the service.
+    TestHAImpl ha_impl;
+    ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration()));
+
+    // Network state is also required.
+    NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv6));
+
+    // Start the service for DHCPv4 server.
+    ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state,
+                                         HAServerType::DHCPv6));
+
+    // Make sure that the HA service has been created for the requested
+    // server type.
+    ASSERT_TRUE(ha_impl.service_);
+    EXPECT_EQ(HAServerType::DHCPv6, ha_impl.service_->getServerType());
+}
+
+// Tests for buffer4_receive callout implementation.
+TEST_F(HAImplTest, buffer4Receive) {
+    // Use hot-standby mode to make sure that this server instance is selected
+    // to process each received query. This is going to give predictable results.
+    ConstElementPtr ha_config = createValidJsonConfiguration(HAConfig::HOT_STANDBY);
+
+    // Create implementation object and configure it.
+    TestHAImpl ha_impl;
+    ASSERT_NO_THROW(ha_impl.configure(ha_config));
+
+    // Starting the service is required prior to running any callouts.
+    NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4));
+    ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state,
+                                         HAServerType::DHCPv4));
+
+    // Initially the HA service is in the waiting state and serves no scopes.
+    // We need to explicitly enable the scope to be served.
+    ha_impl.service_->serveDefaultScopes();
+
+    // Create callout handle to be used for passing arguments to the
+    // callout.
+    CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+    ASSERT_TRUE(callout_handle);
+
+    // Create the BOOTP message. We can use it for testing message parsing
+    // failure case because BOOTP is not supported. We will later turn it
+    // into the DHCP message to test successful parsing.
+    std::vector<uint8_t> msg = {
+        1, // BOOTREQUEST
+        1, // ethernet
+        6, // HW address length = 6
+        0, // hops = 0
+        1, 2, 3, 4, // xid
+        0, 0, // secs = 0
+        0, 0, // flags
+        0, 0, 0, 0, // ciaddr = 0
+        0, 0, 0, 0, // yiaddr = 0
+        0, 0, 0, 0, // siaddr = 0
+        0, 0, 0, 0, // giaddr = 0
+        1, 2, 3, 4, 5, 6, // chaddr
+    };
+
+    // fill chaddr reminder, sname and file with zeros
+    msg.insert(msg.end(), 10 + 128 + 64, 0);
+
+    // Create DHCPv4 message object from the BOOTP message. This should be
+    // successful because the message is not parsed yet.
+    Pkt4Ptr query4(new Pkt4(&msg[0], msg.size()));
+
+    // Set buffer4_receive callout arguments.
+    callout_handle->setArgument("query4", query4);
+
+    // Invoke the buffer4_receive callout.
+    ASSERT_NO_THROW(ha_impl.buffer4Receive(*callout_handle));
+
+    // The BOOTP messages are not supported so trying to unpack the message
+    // should trigger an error. The callout should set the next step to
+    // DROP treating the message as malformed.
+    EXPECT_EQ(CalloutHandle::NEXT_STEP_DROP, callout_handle->getStatus());
+    // Malformed message should not be classified.
+    EXPECT_TRUE(query4->getClasses().empty());
+
+    // Turn this into the DHCP message by appending a magic cookie and the
+    // options.
+    std::vector<uint8_t> magic_cookie = {
+        99, 130, 83, 99
+    };
+
+    // Provide DHCP message type option, truncated vendor option and domain name.
+    // Parsing this message should be successful but domain name following the
+    // truncated vendor option should be skipped.
+    std::vector<uint8_t> options = {
+        53, 1, 1, // Message type = DHCPDISCOVER
+        125, 6, // vendor options
+            1, 2, 3, 4, // enterprise id
+            8, 1, // data len 8 but the actual length is 1 (truncated options)
+        15, 3, 'a', 'b', 'c' // Domain name = abc
+    };
+
+    // Append the magic cookie and the options to our BOOTP message.
+    msg.insert(msg.end(), magic_cookie.begin(), magic_cookie.end());
+    msg.insert(msg.end(), options.begin(), options.end());
+
+    // Create new query and pass it to the callout.
+    query4.reset(new Pkt4(&msg[0], msg.size()));
+    callout_handle->setArgument("query4", query4);
+
+    // Invoke the callout again.
+    ASSERT_NO_THROW(ha_impl.buffer4Receive(*callout_handle));
+
+    // This time the callout should set the next step to SKIP to indicate to
+    // the DHCP server that the message has been already parsed.
+    EXPECT_EQ(CalloutHandle::NEXT_STEP_SKIP, callout_handle->getStatus());
+
+    // The client class should be assigned to the message to indicate that the
+    // server1 should process this message.
+    ASSERT_EQ(1, query4->getClasses().size());
+    EXPECT_EQ("HA_server1", *(query4->getClasses().cbegin()));
+
+    // Check that the message has been parsed. The DHCP message type should
+    // be set in this case.
+    EXPECT_EQ(DHCPDISCOVER, static_cast<int>(query4->getType()));
+    // Domain name should be skipped because the vendor option was truncated.
+    EXPECT_FALSE(query4->getOption(DHO_DOMAIN_NAME));
+}
+
+// Tests for buffer6_receive callout implementation.
+TEST_F(HAImplTest, buffer6Receive) {
+    // Use hot-standby mode to make sure that this server instance is selected
+    // to process each received query. This is going to give predictable results.
+    ConstElementPtr ha_config = createValidJsonConfiguration(HAConfig::HOT_STANDBY);
+
+    // Create implementation object and configure it.
+    TestHAImpl ha_impl;
+    ASSERT_NO_THROW(ha_impl.configure(ha_config));
+
+    // Starting the service is required prior to running any callouts.
+    NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv6));
+    ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state,
+                                         HAServerType::DHCPv6));
+
+    // Initially the HA service is in the waiting state and serves no scopes.
+    // We need to explicitly enable the scope to be served.
+    ha_impl.service_->serveDefaultScopes();
+
+    // Create callout handle to be used for passing arguments to the
+    // callout.
+    CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+    ASSERT_TRUE(callout_handle);
+
+    // Create DHCPv6 message. It initially has no transaction id so should be
+    // considered malformed.
+    std::vector<uint8_t> msg = {
+        1, // Solicit
+    };
+
+    // Create DHCPv4 message object from the BOOTP message. This should be
+    // successful because the message is not parsed yet.
+    Pkt6Ptr query6(new Pkt6(&msg[0], msg.size()));
+
+    // Set buffer6_receive callout arguments.
+    callout_handle->setArgument("query6", query6);
+
+    // Invoke the buffer6_receive callout.
+    ASSERT_NO_THROW(ha_impl.buffer6Receive(*callout_handle));
+
+    // Our DHCP messages contains no transaction id so it should cause
+    // parsing error. The next step is set to DROP for malformed messages.
+    EXPECT_EQ(CalloutHandle::NEXT_STEP_DROP, callout_handle->getStatus());
+    // Malformed message should not be classified.
+    EXPECT_TRUE(query6->getClasses().empty());
+
+    // Append transaction id (3 bytes, each set to 1).
+    msg.insert(msg.end(), 3, 1);
+
+    // Include 3 options in the DHCPv6 message: ORO, truncated vendor option
+    // and the NIS Domain Name option. This should be parsed correctly but the
+    // last option should be skipped because of the preceding option being
+    // truncated.
+    std::vector<uint8_t> options = {
+        0, 6, 0, 2, 0, 29, // option ORO requesting option 29
+        0, 17, // vendor options
+        0, 9, // option length = 10
+            1, 2, 3, 4, // enterprise id
+            0, 1, 0, 10, // code 1, (invalid) length = 10
+            1, // ONLY 1 byte of data (truncated)
+        0, 29, 0, 3, 'a', 'b', 'c' // NIS Domain Name = abc
+    };
+
+    msg.insert(msg.end(), options.begin(), options.end());
+
+    // Create new query and pass it to the callout.
+    query6.reset(new Pkt6(&msg[0], msg.size()));
+    callout_handle->setArgument("query6", query6);
+
+    // Invoke the callout again.
+    ASSERT_NO_THROW(ha_impl.buffer6Receive(*callout_handle));
+
+    // This time the callout should set the next step to SKIP to indicate to
+    // the DHCP server that the message has been already parsed.
+    EXPECT_EQ(CalloutHandle::NEXT_STEP_SKIP, callout_handle->getStatus());
+
+    // The client class should be assigned to the message to indicate that the
+    // server1 should process this message.
+    ASSERT_EQ(1, query6->getClasses().size());
+    EXPECT_EQ("HA_server1", *(query6->getClasses().cbegin()));
+
+    // Check that the message has been parsed. The DHCP message type should
+    // be set in this case.
+    EXPECT_EQ(DHCPV6_SOLICIT, static_cast<int>(query6->getType()));
+    // Domain name should be skipped because the vendor option was truncated.
+    EXPECT_FALSE(query6->getOption(D6O_NIS_DOMAIN_NAME));
+}
+
+// Tests leases4_committed callout implementation.
+TEST_F(HAImplTest, leases4Committed) {
+    // Create implementation object and configure it.
+    TestHAImpl ha_impl;
+    ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration()));
+
+    // Starting the service is required prior to running any callouts.
+    NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4));
+    ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state,
+                                         HAServerType::DHCPv4));
+
+    // Create callout handle to be used for passing arguments to the
+    // callout.
+    CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+    ASSERT_TRUE(callout_handle);
+
+    // query4
+    Pkt4Ptr query4 = createMessage4(DHCPREQUEST, 1, 0, 0);
+    callout_handle->setArgument("query4", query4);
+
+    // leases4
+    Lease4CollectionPtr leases4(new Lease4Collection());
+    callout_handle->setArgument("leases4", leases4);
+
+    // deleted_leases4
+    Lease4CollectionPtr deleted_leases4(new Lease4Collection());
+    callout_handle->setArgument("deleted_leases4", deleted_leases4);
+
+    // Set initial status.
+    callout_handle->setStatus(CalloutHandle::NEXT_STEP_CONTINUE);
+
+    // There are no leases so the callout should return.
+    ASSERT_NO_THROW(ha_impl.leases4Committed(*callout_handle));
+
+    // No updates are generated so the default status should not be modified.
+    EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, callout_handle->getStatus());
+    EXPECT_FALSE(callout_handle->getParkingLotHandlePtr()->drop(query4));
+
+    // Create a lease and pass it to the callout, but temporarily disable lease
+    // updates.
+    HWAddrPtr hwaddr(new HWAddr(std::vector<uint8_t>(6, 1), HTYPE_ETHER));
+    Lease4Ptr lease4(new Lease4(IOAddress("192.1.2.3"), hwaddr,
+                                static_cast<const uint8_t*>(0), 0,
+                                60, 30, 40, 0, 1));
+    leases4->push_back(lease4);
+    callout_handle->setArgument("leases4", leases4);
+
+    ha_impl.config_->setSendLeaseUpdates(false);
+
+    // Run the callout again.
+    ASSERT_NO_THROW(ha_impl.leases4Committed(*callout_handle));
+
+    // No updates are generated so the default status should not be modified.
+    EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, callout_handle->getStatus());
+    EXPECT_FALSE(callout_handle->getParkingLotHandlePtr()->drop(query4));
+
+    // Enable updates and retry.
+    ha_impl.config_->setSendLeaseUpdates(true);
+    callout_handle->setArgument("leases4", leases4);
+    ASSERT_NO_THROW(ha_impl.leases4Committed(*callout_handle));
+
+    // This time the lease update should be generated and the status should
+    // be set to "park".
+    EXPECT_EQ(CalloutHandle::NEXT_STEP_PARK, callout_handle->getStatus());
+    EXPECT_TRUE(callout_handle->getParkingLotHandlePtr()->drop(query4));
+}
+
+// Tests leases6_committed callout implementation.
+TEST_F(HAImplTest, leases6Committed) {
+    // Create implementation object and configure it.
+    TestHAImpl ha_impl;
+    ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration()));
+
+    // Starting the service is required prior to running any callouts.
+    NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv6));
+    ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state,
+                                         HAServerType::DHCPv6));
+
+    // Create callout handle to be used for passing arguments to the
+    // callout.
+    CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+    ASSERT_TRUE(callout_handle);
+
+    // query6
+    Pkt6Ptr query6 = createMessage6(DHCPV6_REQUEST, 1, 0);
+    callout_handle->setArgument("query6", query6);
+
+    // leases6
+    Lease6CollectionPtr leases6(new Lease6Collection());
+    callout_handle->setArgument("leases6", leases6);
+
+    // deleted_leases6
+    Lease6CollectionPtr deleted_leases6(new Lease6Collection());
+    callout_handle->setArgument("deleted_leases6", deleted_leases6);
+
+    // Set initial status.
+    callout_handle->setStatus(CalloutHandle::NEXT_STEP_CONTINUE);
+
+    // There are no leases so the callout should return.
+    ASSERT_NO_THROW(ha_impl.leases6Committed(*callout_handle));
+
+    // No updates are generated so the default status should not be modified.
+    EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, callout_handle->getStatus());
+    EXPECT_FALSE(callout_handle->getParkingLotHandlePtr()->drop(query6));
+
+    // Create a lease and pass it to the callout, but temporarily disable lease
+    // updates.
+    DuidPtr duid(new DUID(std::vector<uint8_t>(8, 2)));
+    Lease6Ptr lease6(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::cafe"), duid,
+                                1234, 50, 60, 30, 40, 1));
+    leases6->push_back(lease6);
+    callout_handle->setArgument("leases6", leases6);
+
+    ha_impl.config_->setSendLeaseUpdates(false);
+
+    // Run the callout again.
+    ASSERT_NO_THROW(ha_impl.leases6Committed(*callout_handle));
+
+    // No updates are generated so the default status should not be modified.
+    EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, callout_handle->getStatus());
+    EXPECT_FALSE(callout_handle->getParkingLotHandlePtr()->drop(query6));
+
+    // Enable updates and retry.
+    ha_impl.config_->setSendLeaseUpdates(true);
+    callout_handle->setArgument("leases6", leases6);
+    ASSERT_NO_THROW(ha_impl.leases6Committed(*callout_handle));
+
+    // This time the lease update should be generated and the status should
+    // be set to "park".
+    EXPECT_EQ(CalloutHandle::NEXT_STEP_PARK, callout_handle->getStatus());
+    EXPECT_TRUE(callout_handle->getParkingLotHandlePtr()->drop(query6));
+}
+
+// Tests ha-sync command handler with correct and incorrect arguments.
+TEST_F(HAImplTest, synchronizeHandler) {
+    {
+        // This syntax is correct. The error returned is simply a result of
+        // trying to connect to the server which is offline, which should
+        // result in connection refused error.
+        SCOPED_TRACE("Correct syntax");
+        testSynchronizeHandler("{"
+                               "    \"command\": \"ha-sync\","
+                               "    \"arguments\": {"
+                               "        \"server-name\": \"server2\""
+                               "    }"
+                               "}", "Connection refused");
+    }
+
+    {
+        SCOPED_TRACE("No arguments");
+        testSynchronizeHandler("{"
+                               "    \"command\": \"ha-sync\""
+                               "}", "arguments not found in the 'ha-sync' command");
+    }
+
+    {
+        SCOPED_TRACE("No server name");
+        testSynchronizeHandler("{"
+                               "    \"command\": \"ha-sync\","
+                               "    \"arguments\": {"
+                               "        \"max-period\": 20"
+                               "    }"
+                               "}", "'server-name' is mandatory for the 'ha-sync' command");
+    }
+
+    {
+        SCOPED_TRACE("Server name is not a string");
+        testSynchronizeHandler("{"
+                               "    \"command\": \"ha-sync\","
+                               "    \"arguments\": {"
+                               "        \"server-name\": 20"
+                               "    }"
+                               "}", "'server-name' must be a string in the 'ha-sync' command");
+    }
+
+    {
+        SCOPED_TRACE("Max period is not a number");
+        testSynchronizeHandler("{"
+                               "    \"command\": \"ha-sync\","
+                               "    \"arguments\": {"
+                               "        \"server-name\": \"server2\","
+                               "        \"max-period\": \"20\""
+                               "    }"
+                               "}", "'max-period' must be a positive integer in the 'ha-sync'"
+                               " command");
+    }
+
+    {
+        SCOPED_TRACE("Max period must be positive");
+        testSynchronizeHandler("{"
+                               "    \"command\": \"ha-sync\","
+                               "    \"arguments\": {"
+                               "        \"server-name\": \"server2\","
+                               "        \"max-period\": \"20\""
+                               "    }"
+                               "}", "'max-period' must be a positive integer in the 'ha-sync'"
+                               " command");
+    }
+
+}
+
+
+}
diff --git a/src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc b/src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc
new file mode 100644 (file)
index 0000000..c6d895b
--- /dev/null
@@ -0,0 +1,4034 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <asiolink/asio_wrapper.h>
+#include <ha_test.h>
+#include <ha_config.h>
+#include <ha_service.h>
+#include <ha_service_states.h>
+#include <asiolink/interval_timer.h>
+#include <asiolink/io_address.h>
+#include <asiolink/io_service.h>
+#include <cc/command_interpreter.h>
+#include <cc/data.h>
+#include <dhcp/classify.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
+#include <dhcp/pkt4.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/network_state.h>
+#include <dhcpsrv/subnet_id.h>
+#include <hooks/parking_lots.h>
+#include <http/date_time.h>
+#include <http/http_types.h>
+#include <http/listener.h>
+#include <http/post_request_json.h>
+#include <http/response_creator.h>
+#include <http/response_creator_factory.h>
+#include <http/response_json.h>
+#include <boost/bind.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <boost/pointer_cast.hpp>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+#include <functional>
+#include <sstream>
+#include <string>
+#include <vector>
+
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::ha;
+using namespace isc::ha::test;
+using namespace isc::hooks;
+using namespace isc::http;
+
+namespace {
+
+/// @brief IP address to which HTTP service is bound.
+const std::string SERVER_ADDRESS = "127.0.0.1";
+
+/// @brief Port number to which HTTP service is bound.
+const unsigned short SERVER_PORT = 18123;
+
+/// @brief Request Timeout used in most of the tests (ms).
+const long REQUEST_TIMEOUT = 10000;
+
+/// @brief Persistent connection idle timeout used in most of the tests (ms).
+const long IDLE_TIMEOUT = 10000;
+
+/// @brief Test timeout (ms).
+const long TEST_TIMEOUT = 10000;
+
+/// @brief Generates IPv4 leases to be used by the tests.
+///
+/// @param [out] leases reference to the container where leases are stored.
+void generateTestLeases(std::vector<Lease4Ptr>& leases) {
+    for (uint8_t i = 1; i <= 10; ++i) {
+        uint32_t lease_address = 0xC0000201 + 256 * i;
+        std::vector<uint8_t> hwaddr(6, i);
+        Lease4Ptr lease(new Lease4(IOAddress(lease_address),
+                                   HWAddrPtr(new HWAddr(hwaddr, HTYPE_ETHER)),
+                                   ClientIdPtr(),
+                                   60, 30, 40,
+                                   static_cast<time_t>(1000 + i),
+                                   SubnetID(i)));
+        leases.push_back(lease);
+    }
+}
+
+/// @brief Generates IPv6 leases to be used by the tests.
+///
+/// @param [out] leases reference to the container where leases are stored.
+void generateTestLeases(std::vector<Lease6Ptr>& leases) {
+    std::vector<uint8_t> address_bytes = IOAddress("2001:db8:1::1").toBytes();
+    for (uint8_t i = 1; i <= 10; ++i) {
+        DuidPtr duid(new DUID(std::vector<uint8_t>(10, i)));
+        address_bytes[6] += i;
+        Lease6Ptr lease(new Lease6(Lease::TYPE_NA,
+                                   IOAddress::fromBytes(AF_INET6, &address_bytes[0]),
+                                   duid, 1, 50, 60, 30, 40, SubnetID(i)));
+        leases.push_back(lease);
+    }
+}
+
+/// @brief Returns generated leases in JSON format.
+///
+/// @tparam LeasesVec vector of IPv4 or IPv6 lease pointers.
+/// @param leases reference to the container holding leases to be
+/// converted to JSON format.
+template<typename LeasesVec>
+ConstElementPtr getLeasesAsJson(const LeasesVec& leases) {
+    ElementPtr leases_json = Element::createList();
+    for (auto l = leases.begin(); l != leases.end(); ++l) {
+        leases_json->add((*l)->toElement());
+    }
+    return (leases_json);
+}
+
+/// @brief Derivation of the @c HAService which provides access to
+/// protected methods and members.
+class TestHAService : public HAService {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param io_service Pointer to the IO service used by the DHCP server.
+    /// @param network_state Objec holding state of the DHCP service
+    /// (enabled/disabled).
+    /// @param config Parsed HA hook library configuration.
+    /// @param server_type Server type, i.e. DHCPv4 or DHCPv6 server.
+    TestHAService(const IOServicePtr& io_service,
+                  const NetworkStatePtr& network_state,
+                  const HAConfigPtr& config,
+                  const HAServerType& server_type = HAServerType::DHCPv4)
+        : HAService(io_service, network_state, config, server_type) {
+    }
+
+    /// @brief Test version of the @c HAService::runModel.
+    ///
+    /// The original implementation of this method returns control when
+    /// @c NOP_EVT is found. This implementation runs a
+    /// single handler to allow the tests to verify if the state machine
+    /// transitions to an expected state before it is run again.
+    virtual void runModel(unsigned int event) {
+        try {
+            postNextEvent(event);
+            getState(getCurrState())->run();
+
+        } catch (const std::exception& ex) {
+            abortModel(ex.what());
+        }
+    }
+
+    /// @brief Schedules asynchronous "dhcp-disable" command to the specified
+    /// server.
+    ///
+    /// This variant of the method uses default HTTP client for communication.
+    ///
+    /// @param server_name name of the server to which the command should be
+    /// sent.
+    /// @param max_period maximum number of seconds for which the DHCP service
+    /// should be disabled.
+    /// @param post_request_action pointer to the function to be executed when
+    /// the request is completed.
+    void asyncDisable(const std::string& server_name,
+                      const unsigned int max_period,
+                      const PostRequestCallback& post_request_action) {
+        HAService::asyncDisable(client_, server_name, max_period,
+                                post_request_action);
+    }
+
+    /// @brief Schedules asynchronous "dhcp-enable" command to the specified
+    /// server.
+    ///
+    /// This variant of the method uses default HTTP client for communication.
+    ///
+    /// @param server_name name of the server to which the command should be
+    /// sent.
+    /// @param post_request_action pointer to the function to be executed when
+    /// the request is completed.
+    void asyncEnable(const std::string& server_name,
+                     const PostRequestCallback& post_request_action) {
+        HAService::asyncEnable(client_, server_name, post_request_action);
+    }
+
+    using HAService::asyncSendHeartbeat;
+    using HAService::asyncSyncLeases;
+    using HAService::postNextEvent;
+    using HAService::transition;
+    using HAService::verboseTransition;
+    using HAService::shouldSendLeaseUpdates;
+    using HAService::network_state_;
+    using HAService::config_;
+    using HAService::communication_state_;
+    using HAService::query_filter_;
+    using HAService::pending_requests_;
+};
+
+/// @brief Pointer to the @c TestHAService.
+typedef boost::shared_ptr<TestHAService> TestHAServicePtr;
+
+/// @brief Test HTTP response creator.
+///
+/// It records received requests and allows the tests to retrieve them
+/// to verify that they include expected values.
+class TestHttpResponseCreator : public HttpResponseCreator {
+public:
+
+    /// @brief Constructor.
+    TestHttpResponseCreator() :
+        requests_(), control_result_(CONTROL_RESULT_SUCCESS),
+        arguments_(), per_request_control_result_(),
+        per_request_arguments_() {
+    }
+
+    /// @brief Removes all received requests.
+    void clearReceivedRequests() {
+        requests_.clear();
+    }
+
+    /// @brief Returns a vector of received requests.
+    std::vector<ConstPostHttpRequestJsonPtr> getReceivedRequests() {
+        return (requests_);
+    }
+
+    /// @brief Finds a received request which includes two strings.
+    ///
+    /// @param str1 First string which must be included in the request.
+    /// @param str2 Second string which must be included in the request.
+    ///
+    /// @return Pointer to the request found, or null pointer if there is
+    /// no such request.
+    ConstPostHttpRequestJsonPtr
+    findRequest(const std::string& str1, const std::string& str2) {
+        for (auto r = requests_.begin(); r < requests_.end(); ++r) {
+            std::string request_as_string = (*r)->toString();
+            if (request_as_string.find(str1) != std::string::npos) {
+                if (request_as_string.find(str2) != std::string::npos) {
+                    return (*r);
+                }
+            }
+        }
+
+        // Request not found.
+        return (ConstPostHttpRequestJsonPtr());
+    }
+
+    /// @brief Sets control result  to be included in the responses.
+    ///
+    /// @param control_result new control result value.
+    void setControlResult(const int control_result) {
+        control_result_ = control_result;
+    }
+
+    /// @brief Sets control result to be returned for the particular command.
+    ///
+    /// @param command_name command name.
+    /// @param control_result new control result value.
+    void setControlResult(const std::string& command_name,
+                          const int control_result) {
+        per_request_control_result_[command_name] = control_result;
+    }
+
+    /// @brief Sets arguments to be included in the responses.
+    ///
+    /// @param arguments pointer to the arguments.
+    void setArguments(const ElementPtr& arguments) {
+        arguments_ = arguments;
+    }
+
+    /// @brief Sets arguments to be included in the response to a particular
+    /// command.
+    ///
+    /// @param command_name command name.
+    /// @param arguments pointer to the arguments.
+    void setArguments(const std::string& command_name,
+                      const ElementPtr& arguments) {
+        per_request_arguments_[command_name] = arguments;
+    }
+
+    /// @brief Create a new request.
+    ///
+    /// @return Pointer to the new instance of the @ref HttpRequest.
+    virtual HttpRequestPtr
+    createNewHttpRequest() const {
+        return (HttpRequestPtr(new PostHttpRequestJson()));
+    }
+
+private:
+
+    /// @brief Creates HTTP response.
+    ///
+    /// @param request Pointer to the HTTP request.
+    /// @return Pointer to the generated HTTP response.
+    virtual HttpResponsePtr
+    createStockHttpResponse(const ConstHttpRequestPtr& request,
+                            const HttpStatusCode& status_code) const {
+        // The request hasn't been finalized so the request object
+        // doesn't contain any information about the HTTP version number
+        // used. But, the context should have this data (assuming the
+        // HTTP version is parsed ok).
+        HttpVersion http_version(request->context()->http_version_major_,
+                                 request->context()->http_version_minor_);
+        // This will generate the response holding JSON content.
+        HttpResponseJsonPtr response(new HttpResponseJson(http_version, status_code));
+        response->finalize();
+        return (response);
+    }
+
+    /// @brief Creates HTTP response.
+    ///
+    /// It records received request so it may be later validated by the tests.
+    /// The returned status code and arguments are set using @c setControlResult
+    /// and @c setArguments methods. The per-request control result and arguments
+    /// take precedence over global values.
+    ///
+    /// @param request Pointer to the HTTP request.
+    /// @return Pointer to the generated HTTP OK response.
+    virtual HttpResponsePtr
+    createDynamicHttpResponse(const ConstHttpRequestPtr& request) {
+        // Request must always be JSON.
+        ConstPostHttpRequestJsonPtr request_json =
+            boost::dynamic_pointer_cast<const PostHttpRequestJson>(request);
+
+        // Remember the request received.
+        requests_.push_back(request_json);
+
+        int control_result = -1;
+        ElementPtr arguments;
+
+        // First, check if the request contains a body with a command.
+        // If so, we may include a specific error code and arguments in the
+        // response based on the command name.
+        ConstElementPtr body = request_json->getBodyAsJson();
+        if (body && (body->getType() == Element::map)) {
+            ConstElementPtr command = body->get("command");
+            if (command && (command->getType() == Element::string)) {
+                std::string command_name = command->stringValue();
+
+                // Check if there is specific error code to be returned for this
+                // command.
+                if (per_request_control_result_.count(command_name) > 0) {
+                    control_result = per_request_control_result_[command_name];
+                }
+
+                // Check if there are specific arguments to be returned for this
+                // command.
+                if (per_request_arguments_.count(command_name) > 0) {
+                    arguments = per_request_arguments_[command_name];
+                }
+            }
+        }
+
+        HttpResponseJsonPtr response(new HttpResponseJson(request->getHttpVersion(),
+                                                          HttpStatusCode::OK));
+        // Body is a list of responses from multiple servers listed in "service"
+        // argument of the request.
+        ElementPtr response_body = Element::createList();
+
+        // No per-command control result specified, so include the global result.
+        if (control_result < 0) {
+            control_result = control_result_;
+        }
+
+        // No per-command arguments specified, so include the global arguments.
+        if (!arguments) {
+            arguments = arguments_;
+        }
+
+        // Insert current date-time if not statically provided.
+        if (arguments && !arguments->contains("date-time")) {
+            arguments->set("date-time", Element::create(HttpDateTime().rfc1123Format()));
+        }
+
+        response_body->add(boost::const_pointer_cast<Element>
+                           (createAnswer(control_result, "response returned",
+                                         arguments)));
+        response->setBodyAsJson(response_body);
+        response->finalize();
+        return (response);
+    }
+
+    /// @brief Holds received HTTP requests.
+    std::vector<ConstPostHttpRequestJsonPtr> requests_;
+
+    /// @brief Control result to be returned in the server responses.
+    int control_result_;
+
+    /// @brief Arguments to be included in the responses.
+    ElementPtr arguments_;
+
+    /// @brief Command specific control results.
+    std::map<std::string, int> per_request_control_result_;
+
+    /// @brief Command specific response arguments.
+    std::map<std::string, ElementPtr> per_request_arguments_;
+};
+
+/// @brief Shared pointer to the @c TestHttpResponseCreator.
+typedef boost::shared_ptr<TestHttpResponseCreator> TestHttpResponseCreatorPtr;
+
+/// @brief Implementation of the test @ref HttpResponseCreatorFactory.
+///
+/// This factory class creates @ref TestHttpResponseCreator instances.
+class TestHttpResponseCreatorFactory : public HttpResponseCreatorFactory {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Initializes common HTTP response creator instance.
+    TestHttpResponseCreatorFactory()
+        : creator_(new TestHttpResponseCreator()) {
+    }
+
+    /// @brief Creates @ref TestHttpResponseCreator instance.
+    virtual HttpResponseCreatorPtr create() const {
+        return (creator_);
+    }
+
+    /// @brief Returns instance of the response creator constructed by this
+    /// factory.
+    TestHttpResponseCreatorPtr getResponseCreator() const {
+        return (boost::dynamic_pointer_cast<TestHttpResponseCreator>(creator_));
+    }
+
+private:
+
+    /// @brief Pointer to the common HTTP response creator.
+    HttpResponseCreatorPtr creator_;
+};
+
+/// @brief Pointer to the @c TestHttpResponseCreatorFactory.
+typedef boost::shared_ptr<TestHttpResponseCreatorFactory>
+TestHttpResponseCreatorFactoryPtr;
+
+/// @brief Test fixture class for @c HAService.
+///
+/// It creates 3 HTTP listeners (servers) which are used in the unit tests.
+class HAServiceTest : public HATest {
+public:
+
+    struct MyState {
+        explicit MyState(const int state)
+            : state_(state) {
+        }
+        int state_;
+    };
+
+    struct PartnerState {
+        explicit PartnerState(const int state)
+            : state_(state) {
+        }
+        int state_;
+    };
+
+    struct FinalState {
+        explicit FinalState(const int state)
+            : state_(state) {
+        }
+        int state_;
+    };
+
+    /// @brief Constructor.
+    HAServiceTest()
+        : HATest(),
+          factory_(new TestHttpResponseCreatorFactory()),
+          factory2_(new TestHttpResponseCreatorFactory()),
+          factory3_(new TestHttpResponseCreatorFactory()),
+          listener_(new HttpListener(*io_service_, IOAddress(SERVER_ADDRESS),
+                                     SERVER_PORT, factory_,
+                                     HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+                                     HttpListener::IdleTimeout(IDLE_TIMEOUT))),
+          listener2_(new HttpListener(*io_service_, IOAddress(SERVER_ADDRESS),
+                                      SERVER_PORT + 1, factory2_,
+                                      HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+                                      HttpListener::IdleTimeout(IDLE_TIMEOUT))),
+          listener3_(new HttpListener(*io_service_, IOAddress(SERVER_ADDRESS),
+                                      SERVER_PORT + 2, factory3_,
+                                      HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+                                      HttpListener::IdleTimeout(IDLE_TIMEOUT))),
+          leases4_(),
+          leases6_() {
+    }
+
+    /// @brief Destructor.
+    ///
+    /// Stops all test servers.
+    ~HAServiceTest() {
+        listener_->stop();
+        listener2_->stop();
+        listener3_->stop();
+        io_service_->get_io_service().reset();
+        io_service_->poll();
+    }
+
+    /// @brief Callback function invoke upon test timeout.
+    ///
+    /// It stops the IO service and reports test timeout.
+    ///
+    /// @param fail_on_timeout Specifies if test failure should be reported.
+    void timeoutHandler(const bool fail_on_timeout) {
+        if (fail_on_timeout) {
+            ADD_FAILURE() << "Timeout occurred while running the test!";
+        }
+        io_service_->stop();
+    }
+
+    /// @brief Generates IPv4 leases to be used by the tests.
+    void generateTestLeases4() {
+        generateTestLeases(leases4_);
+    }
+
+    /// @brief Returns generated IPv4 leases in JSON format.
+    ConstElementPtr getTestLeases4AsJson() const {
+        return (getLeasesAsJson(leases4_));
+    }
+
+    /// @brief Generates IPv6 leases to be used by the tests.
+    void generateTestLeases6() {
+        generateTestLeases(leases6_);
+    }
+
+    /// @brief Returns generated IPv6 leases in JSON format.
+    ConstElementPtr getTestLeases6AsJson() const {
+        return (getLeasesAsJson(leases6_));
+    }
+
+    /// @brief Tests scenarios when lease updates are sent to a partner while
+    /// the partner is online or offline.
+    ///
+    /// @param unpark_handler a function called when packet is unparked.
+    /// @param should_pass indicates if the update is expected to be successful.
+    /// @param num_updates expected number of servers to which lease updates are
+    /// sent.
+    /// @param my_state state of the server while lease updates are sent.
+    void testSendLeaseUpdates(std::function<void()> unpark_handler,
+                              const bool should_pass,
+                              const size_t num_updates,
+                              const MyState& my_state = MyState(HA_LOAD_BALANCING_ST)) {
+        // Create HA configuration for 3 servers. This server is
+        // server 1.
+        HAConfigPtr config_storage = createValidConfiguration();
+
+        // Create parking lot where query is going to be parked and unparked.
+        ParkingLotPtr parking_lot(new ParkingLot());
+        ParkingLotHandlePtr parking_lot_handle(new ParkingLotHandle(parking_lot));
+
+        // Create query.
+        Pkt4Ptr query(new Pkt4(DHCPREQUEST, 1234));
+
+        // Create leases collection and put the lease there.
+        Lease4CollectionPtr leases4(new Lease4Collection());
+        HWAddrPtr hwaddr(new HWAddr(std::vector<uint8_t>(6, 1), HTYPE_ETHER));
+        Lease4Ptr lease4(new Lease4(IOAddress("192.1.2.3"), hwaddr,
+                                    static_cast<const uint8_t*>(0), 0,
+                                    60, 30, 40, 0, 1));
+        leases4->push_back(lease4);
+
+        // Create deleted leases collection and put the lease there too.
+        Lease4CollectionPtr deleted_leases4(new Lease4Collection());
+        Lease4Ptr deleted_lease4(new Lease4(IOAddress("192.2.3.4"), hwaddr,
+                                            static_cast<const uint8_t*>(0), 0,
+                                            60, 30, 40, 0, 1));
+        deleted_leases4->push_back(deleted_lease4);
+
+        // The communication state is the member of the HAServce object. We have to
+        // replace this object with our own implementation to have an ability to
+        // modify its poke time.
+        NakedCommunicationState4Ptr state(new NakedCommunicationState4(io_service_,
+                                                                       config_storage));
+        // Set poke time 30s in the past. If the state is poked it will be reset
+        // to the current time. This allows for testing whether the object has been
+        // poked by the HA service.
+        state->modifyPokeTime(-30);
+
+        // Create HA service and schedule lease updates.
+        TestHAService service(io_service_, network_state_, config_storage);
+        service.communication_state_ = state;
+
+        service.transition(my_state.state_, HAService::NOP_EVT);
+
+        EXPECT_EQ(num_updates,
+                  service.asyncSendLeaseUpdates(query, leases4, deleted_leases4,
+                                                parking_lot_handle));
+
+        EXPECT_FALSE(state->isPoked());
+
+        ASSERT_NO_THROW(parking_lot->reference(query));
+
+        // Let's park the packet and associate it with the callback function which
+        // simply records the fact that it has been called. We expect that it wasn't
+        // because the parked packet should be dropped as a result of lease updates
+        // failures.
+        ASSERT_NO_THROW(parking_lot->park(query, unpark_handler));
+
+        // Actually perform the lease updates.
+        ASSERT_NO_THROW(runIOService(TEST_TIMEOUT, [&service]() {
+            // Finish running IO service when there are no more pending requests.
+            return (service.pending_requests_.empty());
+        }));
+
+        // Try to drop the packet. We expect that the packet has been already
+        // dropped so this should return false.
+        EXPECT_FALSE(parking_lot_handle->drop(query));
+
+        // The updates should not be sent to this server.
+        EXPECT_TRUE(factory_->getResponseCreator()->getReceivedRequests().empty());
+
+        if (should_pass) {
+            EXPECT_TRUE(state->isPoked());
+        } else {
+            EXPECT_FALSE(state->isPoked());
+        }
+    }
+
+    /// @brief Tests scenarios when IPv6 lease updates are sent to a partner while
+    /// the partner is online or offline.
+    ///
+    /// @param unpark_handler a function called when packet is unparked.
+    /// @param should_pass indicates if the update is expected to be successful.
+    /// @param num_updates expected number of servers to which lease updates are
+    /// sent.
+    /// @param my_state state of the server while lease updates are sent.
+    void testSendLeaseUpdates6(std::function<void()> unpark_handler,
+                               const bool should_pass,
+                               const size_t num_updates,
+                               const MyState& my_state = MyState(HA_LOAD_BALANCING_ST)) {
+        // Create HA configuration for 3 servers. This server is
+        // server 1.
+        HAConfigPtr config_storage = createValidConfiguration();
+
+        // Create parking lot where query is going to be parked and unparked.
+        ParkingLotPtr parking_lot(new ParkingLot());
+        ParkingLotHandlePtr parking_lot_handle(new ParkingLotHandle(parking_lot));
+
+        // Create query.
+        Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+
+        // Create leases collection and put the lease there.
+        Lease6CollectionPtr leases6(new Lease6Collection());
+        DuidPtr duid(new DUID(std::vector<uint8_t>(8, 2)));
+        Lease6Ptr lease6(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::cafe"), duid,
+                                    1234, 50, 60, 30, 40, 1));
+        leases6->push_back(lease6);
+
+        // Create deleted leases collection and put the lease there too.
+        Lease6CollectionPtr deleted_leases6(new Lease6Collection());
+        Lease6Ptr deleted_lease6(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::efac"),
+                                            duid, 1234, 50, 60, 30, 40, 1));
+        deleted_leases6->push_back(deleted_lease6);
+
+        // The communication state is the member of the HAServce object. We have to
+        // replace this object with our own implementation to have an ability to
+        // modify its poke time.
+        NakedCommunicationState6Ptr state(new NakedCommunicationState6(io_service_,
+                                                                       config_storage));
+        // Set poke time 30s in the past. If the state is poked it will be reset
+        // to the current time. This allows for testing whether the object has been
+        // poked by the HA service.
+        state->modifyPokeTime(-30);
+
+        // Create HA service and schedule lease updates.
+        TestHAService service(io_service_, network_state_, config_storage);
+        service.communication_state_ = state;
+
+        service.transition(my_state.state_, HAService::NOP_EVT);
+
+        EXPECT_EQ(num_updates,
+                  service.asyncSendLeaseUpdates(query, leases6, deleted_leases6,
+                                                parking_lot_handle));
+
+        EXPECT_FALSE(state->isPoked());
+
+        ASSERT_NO_THROW(parking_lot->reference(query));
+
+        // Let's park the packet and associate it with the callback function which
+        // simply records the fact that it has been called. We expect that it wasn't
+        // because the parked packet should be dropped as a result of lease updates
+        // failures.
+        ASSERT_NO_THROW(parking_lot->park(query, unpark_handler));
+
+        // Actually perform the lease updates.
+        ASSERT_NO_THROW(runIOService(TEST_TIMEOUT, [&service]() {
+            // Finish running IO service when there are no more pending requests.
+            return (service.pending_requests_.empty());
+        }));
+
+        // Try to drop the packet. We expect that the packet has been already
+        // dropped so this should return false.
+        EXPECT_FALSE(parking_lot_handle->drop(query));
+
+        // The updates should not be sent to this server.
+        EXPECT_TRUE(factory_->getResponseCreator()->getReceivedRequests().empty());
+
+        if (should_pass) {
+            EXPECT_TRUE(state->isPoked());
+        } else {
+            EXPECT_FALSE(state->isPoked());
+        }
+    }
+
+    /// @brief Tests scenarios when recurring heartbeat has been enabled
+    /// and the partner is online or offline.
+    ///
+    /// @param control_result control result that the servers should return.
+    /// @param should_pass boolean value indicating if the heartbeat should
+    /// be successful or not.
+    void testRecurringHeartbeat(const int control_result,
+                                const bool should_pass) {
+        // Create HA configuration for 3 servers. This server is
+        // server 1.
+        HAConfigPtr config_storage = createValidConfiguration();
+        config_storage->setHeartbeatDelay(1000);
+
+        // Create a valid static response to the heartbeat command.
+        ElementPtr response_arguments = Element::createMap();
+        response_arguments->set("state", Element::create(std::string("load-balancing")));
+
+        // Both server 2 and server 3 are configured to send this response.
+        factory2_->getResponseCreator()->setArguments(response_arguments);
+        factory3_->getResponseCreator()->setArguments(response_arguments);
+
+        // Configure server 2 and server 3 to send a specified control result.
+        factory2_->getResponseCreator()->setControlResult(control_result);
+        factory3_->getResponseCreator()->setControlResult(control_result);
+
+        // The communication state is the member of the HAServce object. We have to
+        // replace this object with our own implementation to have an ability to
+        // modify its poke time.
+        NakedCommunicationState4Ptr state(new NakedCommunicationState4(io_service_,
+                                                                       config_storage));
+        // Set poke time 30s in the past. If the state is poked it will be reset
+        // to the current time. This allows for testing whether the object has been
+        // poked by the HA service.
+        state->modifyPokeTime(-30);
+
+        // Create the service and replace the default communication state object.
+        TestHAService service(io_service_, network_state_, config_storage);
+        service.communication_state_ = state;
+
+        EXPECT_FALSE(state->isPoked());
+
+        // Let's explicitly transition the state machine to the load balancing state
+        // in which the periodic heartbeats should be generated.
+        ASSERT_NO_THROW(service.verboseTransition(HA_LOAD_BALANCING_ST));
+        ASSERT_NO_THROW(service.runModel(HAService::NOP_EVT));
+
+        // Run the IO service to allow the heartbeat interval timers to execute.
+        ASSERT_NO_THROW(runIOService(2000));
+
+        // Server 1 and server 3 must never receive heartbeats because the former
+        // is the one that generates them and the latter is a backup server.
+        EXPECT_TRUE(factory_->getResponseCreator()->getReceivedRequests().empty());
+        EXPECT_TRUE(factory3_->getResponseCreator()->getReceivedRequests().empty());
+
+        // If should pass, the communication state should be poked.
+        if (should_pass) {
+            EXPECT_TRUE(state->isPoked());
+        } else {
+            EXPECT_FALSE(state->isPoked());
+        }
+    }
+
+    /// @brief Runs HAService::processSynchronize for the DHCPv4 server and
+    /// returns a response.
+    ///
+    /// The HAService::processSynchronize is synchronous. Therefore, the IO service
+    /// for HTTP servers is run in a thread. The unit test is responsible for setting
+    /// up the status codes to be returned by the servers, verifying a response and
+    /// leases in the lease database.
+    ///
+    /// @param [out] rsp pointer to the object where response will be stored.
+    void runProcessSynchronize4(ConstElementPtr& rsp) {
+        // Create lease manager.
+        ASSERT_NO_THROW(LeaseMgrFactory::create("universe=4 type=memfile persist=false"));
+
+        // Create IPv4 leases which will be fetched from the other server.
+        ASSERT_NO_THROW(generateTestLeases4());
+
+        // Create HA configuration for 3 servers. This server is
+        // server 1.
+        HAConfigPtr config_storage = createValidConfiguration();
+
+        // Convert leases to the JSON format, the same as used by the lease_cmds
+        // hook library. Configure our test HTTP servers to return those
+        // leases in this format.
+        ElementPtr response_arguments = Element::createMap();
+        response_arguments->set("leases", getTestLeases4AsJson());
+
+        factory2_->getResponseCreator()->setArguments("lease4-get-all", response_arguments);
+        factory3_->getResponseCreator()->setArguments("lease4-get-all", response_arguments);
+
+        // Start the servers.
+        ASSERT_NO_THROW({
+            listener_->start();
+            listener2_->start();
+            listener3_->start();
+        });
+
+        HAService service(io_service_, network_state_, config_storage);
+
+        // The tested function is synchronous, so we need to run server side IO service
+        // in bakckground to not block the main thread.
+        auto thread = runIOServiceInThread();
+
+        // Process ha-sync command.
+        ASSERT_NO_THROW(rsp = service.processSynchronize("server2", 20));
+
+        // Stop the IO service. This should cause the thread to terminate.
+        io_service_->stop();
+        thread->wait();
+        io_service_->get_io_service().reset();
+        io_service_->poll();
+    }
+
+    /// @brief Runs HAService::processSynchronize for the DHCPv6 server
+    /// and returns a response.
+    ///
+    /// The HAService::processSynchronize is synchronous. Therefore, the IO service
+    /// for HTTP servers is run in a thread. The unit test is responsible for setting
+    /// up the status codes to be returned by the servers, verifying a response and
+    /// leases in the lease database.
+    ///
+    /// @param [out] rsp pointer to the object where response will be stored.
+    void runProcessSynchronize6(ConstElementPtr& rsp) {
+        // Create lease manager.
+        ASSERT_NO_THROW(LeaseMgrFactory::create("universe=6 type=memfile persist=false"));
+
+        // Create IPv4 leases which will be fetched from the other server.
+        ASSERT_NO_THROW(generateTestLeases6());
+
+        // Create HA configuration for 3 servers. This server is
+        // server 1.
+        HAConfigPtr config_storage = createValidConfiguration();
+
+        // Convert leases to the JSON format, the same as used by the lease_cmds
+        // hook library. Configure our test HTTP servers to return those
+        // leases in this format.
+        ElementPtr response_arguments = Element::createMap();
+        response_arguments->set("leases", getTestLeases6AsJson());
+
+        factory2_->getResponseCreator()->setArguments("lease6-get-all", response_arguments);
+        factory3_->getResponseCreator()->setArguments("lease6-get-all", response_arguments);
+
+        // Start the servers.
+        ASSERT_NO_THROW({
+            listener_->start();
+            listener2_->start();
+            listener3_->start();
+        });
+
+        HAService service(io_service_, network_state_, config_storage,
+                          HAServerType::DHCPv6);
+
+        // The tested function is synchronous, so we need to run server side IO service
+        // in bakckground to not block the main thread.
+        auto thread = runIOServiceInThread();
+
+        // Process ha-sync command.
+        ASSERT_NO_THROW(rsp = service.processSynchronize("server2", 20));
+
+        // Stop the IO service. This should cause the thread to terminate.
+        io_service_->stop();
+        thread->wait();
+        io_service_->get_io_service().reset();
+        io_service_->poll();
+    }
+
+    /// @brief HTTP response factory for server 1.
+    TestHttpResponseCreatorFactoryPtr factory_;
+
+    /// @brief HTTP response factory for server 2.
+    TestHttpResponseCreatorFactoryPtr factory2_;
+
+    /// @brief HTTP response factory for server 3.
+    TestHttpResponseCreatorFactoryPtr factory3_;
+
+    /// @brief Test HTTP server 1.
+    HttpListenerPtr listener_;
+
+    /// @brief Test HTTP server 2.
+    HttpListenerPtr listener2_;
+
+    /// @brief Test HTTP server 2.
+    HttpListenerPtr listener3_;
+
+    /// @brief IPv4 leases to be used in the tests.
+    std::vector<Lease4Ptr> leases4_;
+
+    /// @brief IPv6 leases to be used in the tests.
+    std::vector<Lease6Ptr> leases6_;
+};
+
+// Test that server performs load balancing and assigns appropriate classes
+// to the queries.
+TEST_F(HAServiceTest, loadBalancingScopeSelection) {
+    // Create HA configuration for load balancing.
+    HAConfigPtr config_storage = createValidConfiguration();
+    // ... and HA service using this configuration.
+    TestHAService service(io_service_, network_state_, config_storage);
+    service.verboseTransition(HA_LOAD_BALANCING_ST);
+    service.runModel(HAService::NOP_EVT);
+
+    // Count number of in scope packets.
+    unsigned in_scope = 0;
+    // Set the test size - 65535 queries.
+    const unsigned queries_num = 65535;
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random HW address.
+        Pkt4Ptr query4 = createQuery4(randomKey(HWAddr::ETHERNET_HWADDR_LEN));
+
+        // Some queries should be processed by this server, some not.
+        if (service.inScope(query4)) {
+            // If the query is to be processed by this server the query
+            // should be assigned to the "HA_server1" class but not to
+            // the "HA_server2" class.
+            ASSERT_TRUE(query4->inClass(ClientClass("HA_server1")));
+            ASSERT_FALSE(query4->inClass(ClientClass("HA_server2")));
+            ++in_scope;
+
+        } else {
+            // If the query is to be processed by another server, the
+            // "HA_server2" class should be assigned instead.
+            ASSERT_FALSE(query4->inClass(ClientClass("HA_server1")));
+            ASSERT_TRUE(query4->inClass(ClientClass("HA_server2")));
+        }
+    }
+
+    // We should have roughly 50/50 split of in scope and out of scope queries.
+    // However, we don't know exactly how many. To be safe we simply assume that
+    // we got more than 25% of in scope and more than 25% out of scope queries.
+    EXPECT_GT(in_scope, static_cast<unsigned>(queries_num / 4));
+    EXPECT_GT(queries_num - in_scope, static_cast<unsigned>(queries_num / 4));
+}
+
+// Test that primary server in hot standby configuration processes all queries.
+TEST_F(HAServiceTest, hotStandbyScopeSelectionThisPrimary) {
+    // Create HA configuration for load balancing.
+    HAConfigPtr config_storage = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    config_storage->setHAMode("hot-standby");
+    config_storage->getPeerConfig("server2")->setRole("standby");
+
+    // ... and HA service using this configuration.
+    TestHAService service(io_service_, network_state_, config_storage);
+    service.verboseTransition(HA_HOT_STANDBY_ST);
+    service.runModel(HAService::NOP_EVT);
+
+    // Set the test size - 65535 queries.
+    const unsigned queries_num = 65535;
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random HW address.
+        Pkt4Ptr query4 = createQuery4(randomKey(HWAddr::ETHERNET_HWADDR_LEN));
+
+        // All queries should be processed by this server.
+        ASSERT_TRUE(service.inScope(query4));
+        // The query should be assigned to the "HA_server1" class but not to
+        // the "HA_server2" class.
+        ASSERT_TRUE(query4->inClass(ClientClass("HA_server1")));
+        ASSERT_FALSE(query4->inClass(ClientClass("HA_server2")));
+    }
+}
+
+// Test that secondary server in hot standby configuration processes no queries.
+TEST_F(HAServiceTest, hotStandbyScopeSelectionThisStandby) {
+    // Create HA configuration for load balancing.
+    HAConfigPtr config_storage = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    config_storage->setHAMode("hot-standby");
+    config_storage->getPeerConfig("server2")->setRole("standby");
+    config_storage->setThisServerName("server2");
+
+    // ... and HA service using this configuration.
+    TestHAService service(io_service_, network_state_, config_storage);
+
+    // Set the test size - 65535 queries.
+    const unsigned queries_num = 65535;
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random HW address.
+        Pkt4Ptr query4 = createQuery4(randomKey(HWAddr::ETHERNET_HWADDR_LEN));
+
+        // No queries should be processed by this server.
+        ASSERT_FALSE(service.inScope(query4));
+        // The query should be assigned to the "HA_server1" class but not to
+        // the "HA_server2" class.
+        ASSERT_TRUE(query4->inClass(ClientClass("HA_server1")));
+        ASSERT_FALSE(query4->inClass(ClientClass("HA_server2")));
+    }
+}
+
+// Test scenario when all lease updates are sent successfully.
+TEST_F(HAServiceTest, sendSuccessfulUpdates) {
+    // Start HTTP servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    // This flag will be set to true if unpark is called.
+    bool unpark_called = false;
+    testSendLeaseUpdates([&unpark_called] {
+        unpark_called = true;
+    }, true, 2);
+
+    // Expecting that the packet was unparked because lease updates are expected
+    // to be successful.
+    EXPECT_TRUE(unpark_called);
+
+    // The server 2 should have received two commands.
+    EXPECT_EQ(2, factory2_->getResponseCreator()->getReceivedRequests().size());
+
+    // Check that the server 2 has received lease4-update command.
+    auto update_request2 = factory2_->getResponseCreator()->findRequest("lease4-update",
+                                                                        "192.1.2.3");
+    EXPECT_TRUE(update_request2);
+
+    // Check that the server 2 has received lease4-del command.
+    auto delete_request2 = factory2_->getResponseCreator()->findRequest("lease4-del",
+                                                                        "192.2.3.4");
+    EXPECT_TRUE(delete_request2);
+
+    // Lease updates should be successfully sent to server3.
+    EXPECT_EQ(2, factory3_->getResponseCreator()->getReceivedRequests().size());
+
+    // Check that the server 3 has received lease4-update command.
+    auto update_request3 = factory3_->getResponseCreator()->findRequest("lease4-update",
+                                                                        "192.1.2.3");
+    EXPECT_TRUE(update_request3);
+
+    // Check that the server 3 has received lease4-del command.
+    auto delete_request3 = factory3_->getResponseCreator()->findRequest("lease4-del",
+                                                                        "192.2.3.4");
+    EXPECT_TRUE(delete_request3);
+}
+
+// Test scenario when lease updates are sent successfully to the backup server
+// and not sent to the failover peer when this server is in patrtner-down state.
+TEST_F(HAServiceTest, sendUpdatesPartnerDown) {
+    // Start HTTP servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    // This flag will be set to true if unpark is called.
+    bool unpark_called = false;
+    testSendLeaseUpdates([&unpark_called] {
+        unpark_called = true;
+    }, false, 1, MyState(HA_PARTNER_DOWN_ST));
+
+    // Expecting that the packet was unparked because lease updates are expected
+    // to be successful.
+    EXPECT_TRUE(unpark_called);
+
+    // Server 2 should not receive lease4-update.
+    auto update_request2 = factory2_->getResponseCreator()->findRequest("lease4-update",
+                                                                        "192.1.2.3");
+    EXPECT_FALSE(update_request2);
+
+    // Server 2 should not receive lease4-del.
+    auto delete_request2 = factory2_->getResponseCreator()->findRequest("lease4-del",
+                                                                        "192.2.3.4");
+    EXPECT_FALSE(delete_request2);
+
+    // Lease updates should be successfully sent to server3.
+    EXPECT_EQ(2, factory3_->getResponseCreator()->getReceivedRequests().size());
+
+    // Check that the server 3 has received lease4-update command.
+    auto update_request3 = factory3_->getResponseCreator()->findRequest("lease4-update",
+                                                                        "192.1.2.3");
+    EXPECT_TRUE(update_request3);
+
+    // Check that the server 3 has received lease4-del command.
+    auto delete_request3 = factory3_->getResponseCreator()->findRequest("lease4-del",
+                                                                        "192.2.3.4");
+    EXPECT_TRUE(delete_request3);
+}
+
+// Test scenario when one of the servers to which updates are sent is offline.
+TEST_F(HAServiceTest, sendUpdatesActiveServerOffline) {
+    // Start only two servers out of three. The server 3 is not running.
+    ASSERT_NO_THROW({
+            listener_->start();
+            listener3_->start();
+    });
+
+    testSendLeaseUpdates([] {
+        ADD_FAILURE() << "unpark function called but expected that the packet"
+            " is dropped";
+    }, false, 2);
+
+    // Server 2 should not receive lease4-update.
+    auto update_request2 = factory2_->getResponseCreator()->findRequest("lease4-update",
+                                                                        "192.1.2.3");
+    EXPECT_FALSE(update_request2);
+
+    // Server 2 should not receive lease4-del.
+    auto delete_request2 = factory2_->getResponseCreator()->findRequest("lease4-del",
+                                                                        "192.2.3.4");
+    EXPECT_FALSE(delete_request2);
+
+    // Lease updates should be successfully sent to server3.
+    EXPECT_EQ(2, factory3_->getResponseCreator()->getReceivedRequests().size());
+
+    // Check that the server 3 has received lease4-update command.
+    auto update_request3 = factory3_->getResponseCreator()->findRequest("lease4-update",
+                                                                        "192.1.2.3");
+    EXPECT_TRUE(update_request3);
+
+    // Check that the server 3 has received lease4-del command.
+    auto delete_request3 = factory3_->getResponseCreator()->findRequest("lease4-del",
+                                                                        "192.2.3.4");
+    EXPECT_TRUE(delete_request3);
+}
+
+// Test scenario when one of the servers to which updates are sent is offline.
+TEST_F(HAServiceTest, sendUpdatesBackupServerOffline) {
+    // Start only two servers out of three. The server 2 is not running.
+    ASSERT_NO_THROW({
+            listener_->start();
+            listener2_->start();
+    });
+
+    bool unpark_called = false;
+    testSendLeaseUpdates([&unpark_called] {
+        unpark_called = true;
+    }, true, 2);
+
+    EXPECT_TRUE(unpark_called);
+
+    // The server 2 should have received two commands.
+    EXPECT_EQ(2, factory2_->getResponseCreator()->getReceivedRequests().size());
+
+    // Check that the server 2 has received lease4-update command.
+    auto update_request2 = factory2_->getResponseCreator()->findRequest("lease4-update",
+                                                                        "192.1.2.3");
+    EXPECT_TRUE(update_request2);
+
+    // Check that the server 2 has received lease4-del command.
+    auto delete_request2 = factory2_->getResponseCreator()->findRequest("lease4-del",
+                                                                        "192.2.3.4");
+    EXPECT_TRUE(delete_request2);
+
+    // Server 3 should not receive lease4-update.
+    auto update_request3 = factory3_->getResponseCreator()->findRequest("lease4-update",
+                                                                        "192.1.2.3");
+    EXPECT_FALSE(update_request3);
+
+    // Server 3 should not receive lease4-del.
+    auto delete_request3 = factory3_->getResponseCreator()->findRequest("lease4-del",
+                                                                        "192.2.3.4");
+    EXPECT_FALSE(delete_request3);
+}
+
+// Test scenario when one of the servers to which a lease update is sent
+// returns an error.
+TEST_F(HAServiceTest, sendUpdatesControlResultError) {
+    // Instruct the server 2 to return an error as a result of receiving a command.
+    factory2_->getResponseCreator()->setControlResult(CONTROL_RESULT_ERROR);
+
+    // Start only two servers out of three. The server 3 is not running.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    testSendLeaseUpdates([] {
+        ADD_FAILURE() << "unpark function called but expected that the packet"
+            " is dropped";
+    }, false, 2);
+
+    // The updates should be sent to server 2 and this server should return error code.
+    EXPECT_EQ(2, factory2_->getResponseCreator()->getReceivedRequests().size());
+
+    // Server 2 should receive lease4-update.
+    auto update_request2 = factory2_->getResponseCreator()->findRequest("lease4-update",
+                                                                        "192.1.2.3");
+    EXPECT_TRUE(update_request2);
+
+    // Server 2 should receive lease4-del.
+    auto delete_request2 = factory2_->getResponseCreator()->findRequest("lease4-del",
+                                                                        "192.2.3.4");
+    EXPECT_TRUE(delete_request2);
+
+    // Lease updates should be successfully sent to server3.
+    EXPECT_EQ(2, factory3_->getResponseCreator()->getReceivedRequests().size());
+
+    // Check that the server 3 has received lease4-update command.
+    auto update_request3 = factory3_->getResponseCreator()->findRequest("lease4-update",
+                                                                        "192.1.2.3");
+    EXPECT_TRUE(update_request3);
+
+    // Check that the server 3 has received lease4-del command.
+    auto delete_request3 = factory3_->getResponseCreator()->findRequest("lease4-del",
+                                                                        "192.2.3.4");
+    EXPECT_TRUE(delete_request3);
+}
+
+// Test scenario when all lease updates are sent successfully.
+TEST_F(HAServiceTest, sendSuccessfulUpdates6) {
+    // Start HTTP servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    // This flag will be set to true if unpark is called.
+    bool unpark_called = false;
+    testSendLeaseUpdates6([&unpark_called] {
+        unpark_called = true;
+    }, true, 2);
+
+    // Expecting that the packet was unparked because lease updates are expected
+    // to be successful.
+    EXPECT_TRUE(unpark_called);
+
+    // The server 2 should have received two commands.
+    EXPECT_EQ(2, factory2_->getResponseCreator()->getReceivedRequests().size());
+
+    // Check that the server 2 has received lease6-update command.
+    auto update_request2 = factory2_->getResponseCreator()->findRequest("lease6-update",
+                                                                        "2001:db8:1::cafe");
+    EXPECT_TRUE(update_request2);
+
+    // Check that the server 2 has received lease6-del command.
+    auto delete_request2 = factory2_->getResponseCreator()->findRequest("lease6-del",
+                                                                        "2001:db8:1::efac");
+    EXPECT_TRUE(delete_request2);
+
+    // Lease updates should be successfully sent to server3.
+    EXPECT_EQ(2, factory3_->getResponseCreator()->getReceivedRequests().size());
+
+    // Check that the server 3 has received lease6-update command.
+    auto update_request3 = factory3_->getResponseCreator()->findRequest("lease6-update",
+                                                                        "2001:db8:1::cafe");
+    EXPECT_TRUE(update_request3);
+
+    // Check that the server 3 has received lease6-del command.
+    auto delete_request3 = factory3_->getResponseCreator()->findRequest("lease6-del",
+                                                                        "2001:db8:1::efac");
+    EXPECT_TRUE(delete_request3);
+}
+
+// Test scenario when lease updates are sent successfully to the backup server
+// and not sent to the failover peer when this server is in patrtner-down state.
+TEST_F(HAServiceTest, sendUpdatesPartnerDown6) {
+    // Start HTTP servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    // This flag will be set to true if unpark is called.
+    bool unpark_called = false;
+    testSendLeaseUpdates6([&unpark_called] {
+        unpark_called = true;
+    }, false, 1, MyState(HA_PARTNER_DOWN_ST));
+
+    // Expecting that the packet was unparked because lease updates are expected
+    // to be successful.
+    EXPECT_TRUE(unpark_called);
+
+    // Server 2 should not receive lease6-update.
+    auto update_request2 = factory2_->getResponseCreator()->findRequest("lease6-update",
+                                                                        "2001:db8:1::cafe");
+    EXPECT_FALSE(update_request2);
+
+    // Server 2 should not receive lease6-del.
+    auto delete_request2 = factory2_->getResponseCreator()->findRequest("lease6-del",
+                                                                        "2001:db8:1::efac");
+    EXPECT_FALSE(delete_request2);
+
+    // Lease updates should be successfully sent to server3.
+    EXPECT_EQ(2, factory3_->getResponseCreator()->getReceivedRequests().size());
+
+    // Check that the server 3 has received lease6-update command.
+    auto update_request3 = factory3_->getResponseCreator()->findRequest("lease6-update",
+                                                                        "2001:db8:1::cafe");
+    EXPECT_TRUE(update_request3);
+
+    // Check that the server 3 has received lease6-del command.
+    auto delete_request3 = factory3_->getResponseCreator()->findRequest("lease6-del",
+                                                                        "2001:db8:1::efac");
+    EXPECT_TRUE(delete_request3);
+}
+
+// Test scenario when one of the servers to which updates are sent is offline.
+TEST_F(HAServiceTest, sendUpdatesActiveServerOffline6) {
+    // Start only two servers out of three. The server 3 is not running.
+    ASSERT_NO_THROW({
+            listener_->start();
+            listener3_->start();
+    });
+
+    testSendLeaseUpdates6([] {
+        ADD_FAILURE() << "unpark function called but expected that the packet"
+            " is dropped";
+    }, false, 2);
+
+    // Server 2 should not receive lease6-update.
+    auto update_request2 = factory2_->getResponseCreator()->findRequest("lease6-update",
+                                                                        "2001:db8:1::cafe");
+    EXPECT_FALSE(update_request2);
+
+    // Server 2 should not receive lease6-del.
+    auto delete_request2 = factory2_->getResponseCreator()->findRequest("lease6-del",
+                                                                        "2001:db8:1::efac");
+    EXPECT_FALSE(delete_request2);
+
+    // Lease updates should be successfully sent to server3.
+    EXPECT_EQ(2, factory3_->getResponseCreator()->getReceivedRequests().size());
+
+    // Check that the server 3 has received lease6-update command.
+    auto update_request3 = factory3_->getResponseCreator()->findRequest("lease6-update",
+                                                                        "2001:db8:1::cafe");
+    EXPECT_TRUE(update_request3);
+
+    // Check that the server 3 has received lease6-del command.
+    auto delete_request3 = factory3_->getResponseCreator()->findRequest("lease6-del",
+                                                                        "2001:db8:1::efac");
+    EXPECT_TRUE(delete_request3);
+}
+
+// Test scenario when one of the servers to which updates are sent is offline.
+TEST_F(HAServiceTest, sendUpdatesBackupServerOffline6) {
+    // Start only two servers out of three. The server 2 is not running.
+    ASSERT_NO_THROW({
+            listener_->start();
+            listener2_->start();
+    });
+
+    bool unpark_called = false;
+    testSendLeaseUpdates6([&unpark_called] {
+        unpark_called = true;
+    }, true, 2);
+
+    EXPECT_TRUE(unpark_called);
+
+    // The server 2 should have received two commands.
+    EXPECT_EQ(2, factory2_->getResponseCreator()->getReceivedRequests().size());
+
+    // Check that the server 2 has received lease6-update command.
+    auto update_request2 = factory2_->getResponseCreator()->findRequest("lease6-update",
+                                                                        "2001:db8:1::cafe");
+    EXPECT_TRUE(update_request2);
+
+    // Check that the server 2 has received lease6-del command.
+    auto delete_request2 = factory2_->getResponseCreator()->findRequest("lease6-del",
+                                                                        "2001:db8:1::efac");
+    EXPECT_TRUE(delete_request2);
+
+    // Server 3 should not receive lease6-update.
+    auto update_request3 = factory3_->getResponseCreator()->findRequest("lease6-update",
+                                                                        "2001:db8:1::cafe");
+    EXPECT_FALSE(update_request3);
+
+    // Server 3 should not receive lease6-del.
+    auto delete_request3 = factory3_->getResponseCreator()->findRequest("lease6-del",
+                                                                        "2001:db8:1::efac");
+    EXPECT_FALSE(delete_request3);
+}
+
+// Test scenario when one of the servers to which a lease update is sent
+// returns an error.
+TEST_F(HAServiceTest, sendUpdatesControlResultError6) {
+    // Instruct the server 2 to return an error as a result of receiving a command.
+    factory2_->getResponseCreator()->setControlResult(CONTROL_RESULT_ERROR);
+
+    // Start only two servers out of three. The server 3 is not running.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    testSendLeaseUpdates6([] {
+        ADD_FAILURE() << "unpark function called but expected that the packet"
+            " is dropped";
+    }, false, 2);
+
+    // The updates should be sent to server 2 and this server should return error code.
+    EXPECT_EQ(2, factory2_->getResponseCreator()->getReceivedRequests().size());
+
+    // Server 2 should receive lease6-update.
+    auto update_request2 = factory2_->getResponseCreator()->findRequest("lease6-update",
+                                                                        "2001:db8:1::cafe");
+    EXPECT_TRUE(update_request2);
+
+    // Server 2 should receive lease6-del.
+    auto delete_request2 = factory2_->getResponseCreator()->findRequest("lease6-del",
+                                                                        "2001:db8:1::efac");
+    EXPECT_TRUE(delete_request2);
+
+    // Lease updates should be successfully sent to server3.
+    EXPECT_EQ(2, factory3_->getResponseCreator()->getReceivedRequests().size());
+
+    // Check that the server 3 has received lease6-update command.
+    auto update_request3 = factory3_->getResponseCreator()->findRequest("lease6-update",
+                                                                        "2001:db8:1::cafe");
+    EXPECT_TRUE(update_request3);
+
+    // Check that the server 3 has received lease6-del command.
+    auto delete_request3 = factory3_->getResponseCreator()->findRequest("lease6-del",
+                                                                        "2001:db8:1::efac");
+    EXPECT_TRUE(delete_request3);
+}
+
+// This test verifies that the heartbeat command is processed successfully.
+TEST_F(HAServiceTest, processHeartbeat) {
+    // Create HA configuration for 3 servers. This server is
+    // server 1.
+    std::string config_text =
+        "["
+        "     {"
+        "         \"this-server-name\": \"server1\","
+        "         \"mode\": \"load-balancing\","
+        "         \"peers\": ["
+        "             {"
+        "                 \"name\": \"server1\","
+        "                 \"url\": \"http://127.0.0.1:18123/\","
+        "                 \"role\": \"primary\","
+        "                 \"auto-failover\": true"
+        "             },"
+        "             {"
+        "                 \"name\": \"server2\","
+        "                 \"url\": \"http://127.0.0.1:18124/\","
+        "                 \"role\": \"secondary\","
+        "                 \"auto-failover\": true"
+        "             },"
+        "             {"
+        "                 \"name\": \"server3\","
+        "                 \"url\": \"http://127.0.0.1:18125/\","
+        "                 \"role\": \"backup\","
+        "                 \"auto-failover\": false"
+        "             }"
+        "         ]"
+        "     }"
+        "]";
+
+    // Parse the HA configuration.
+    HAConfigPtr config_storage(new HAConfig());
+    HAConfigParser parser;
+    ASSERT_NO_THROW(parser.parse(config_storage, Element::fromJSON(config_text)));
+
+    HAService service(io_service_,  network_state_, config_storage);
+
+    // Process heartbeat command.
+    ConstElementPtr rsp;
+    ASSERT_NO_THROW(rsp = service.processHeartbeat());
+
+    ASSERT_TRUE(rsp);
+    checkAnswer(rsp, CONTROL_RESULT_SUCCESS, "HA peer status returned.");
+    // Response must include arguments.
+    ConstElementPtr args = rsp->get("arguments");
+    ASSERT_TRUE(args);
+
+    // Response must include server state as string.
+    ConstElementPtr state = args->get("state");
+    ASSERT_TRUE(state);
+    EXPECT_EQ(Element::string, state->getType());
+    EXPECT_EQ("waiting", state->stringValue());
+
+    // Response must include timestamp when the response was generated.
+    ConstElementPtr date_time = args->get("date-time");
+    ASSERT_TRUE(date_time);
+    EXPECT_EQ(Element::string, date_time->getType());
+
+    // The response should contain the timestamp in the format specified
+    // in RFC1123. We use the HttpDateTime method to parse this timestamp.
+    HttpDateTime t;
+    ASSERT_NO_THROW(t = HttpDateTime::fromRfc1123(date_time->stringValue()));
+
+    // Let's test if the timestamp is accurate. We do it by checking current
+    // time and comparing with the received timestamp.
+    HttpDateTime now;
+    boost::posix_time::time_duration td = now.getPtime() - t.getPtime();
+
+    // Let's allow the response propagation time of 5 seconds to make
+    // sure this test doesn't fail on slow systems.
+    EXPECT_LT(td.seconds(), 5);
+}
+
+// This test verifies that the correct value of the heartbeat-delay is used.
+TEST_F(HAServiceTest, recurringHeartbeatDelay) {
+    HAConfigPtr config_storage = createValidConfiguration();
+
+    // Set the heartbeat delay to 6 seconds.
+    config_storage->setHeartbeatDelay(6000);
+
+    // The communication state is the member of the HAServce object. We have to
+    // replace this object with our own implementation to have an ability to
+    // test the setup of the interval timer.
+    NakedCommunicationState4Ptr state(new NakedCommunicationState4(io_service_,
+                                                                   config_storage));
+
+    TestHAService service(io_service_, network_state_, config_storage);
+    service.communication_state_ = state;
+
+    // Let's explicitly transition the state machine to the load balancing state
+    // in which the periodic heartbeats should be generated.
+    ASSERT_NO_THROW(service.verboseTransition(HA_LOAD_BALANCING_ST));
+    ASSERT_NO_THROW(service.runModel(HAService::NOP_EVT));
+
+    ASSERT_TRUE(state->timer_);
+    EXPECT_EQ(6000, state->timer_->getInterval());
+}
+
+// This test verifies that the heartbeat is periodically sent to the
+// other server.
+TEST_F(HAServiceTest, recurringHeartbeat) {
+    // Start the servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    // All servers are configured to return success and all servers are online.
+    // The heartbeat should be successful (as indicated by the 'true' argument).
+    ASSERT_NO_FATAL_FAILURE(testRecurringHeartbeat(CONTROL_RESULT_SUCCESS, true));
+
+    // Server 2 should have received the heartbeat
+    EXPECT_GE(factory2_->getResponseCreator()->getReceivedRequests().size(), 0);
+}
+
+// This test verifies that the heartbeat is considered being unsuccessful if the
+// partner is offline.
+TEST_F(HAServiceTest, recurringHeartbeatServerOffline) {
+    // Start the servers but do not start server 2.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener3_->start();
+    });
+
+    // The servers are configured to return success but the server 2 is offline
+    // so the heartbeat should be unsuccessul.
+    ASSERT_NO_FATAL_FAILURE(testRecurringHeartbeat(CONTROL_RESULT_SUCCESS, false));
+
+    // Server 2 is offline so it would be very weird if it received any command.
+    EXPECT_TRUE(factory2_->getResponseCreator()->getReceivedRequests().empty());
+}
+
+// This test verifies that the heartbeat is considered being unsuccessful if the
+// partner returns error control result.
+TEST_F(HAServiceTest, recurringHeartbeatControlResultError) {
+    // Start the servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    // Run the actual test. The servers return a control error and it is expected
+    // that the state is not poked.
+    ASSERT_NO_FATAL_FAILURE(testRecurringHeartbeat(CONTROL_RESULT_ERROR, false));
+
+    // Server 2 should have received the heartbeat.
+    EXPECT_EQ(1, factory2_->getResponseCreator()->getReceivedRequests().size());
+}
+
+// This test verifies that IPv4 leases can be fetched from the peer and inserted
+// or updated in the local lease database.
+TEST_F(HAServiceTest, asyncSyncLeases) {
+    // Create lease manager.
+    ASSERT_NO_THROW(LeaseMgrFactory::create("universe=4 type=memfile persist=false"));
+
+    // Create IPv4 leases which will be fetched from the other server.
+    ASSERT_NO_THROW(generateTestLeases4());
+
+    for (size_t i = 0; i < leases4_.size(); ++i) {
+        // For every even lease index we add this lease to the database to exercise
+        // the scenario when a lease is already in the database and may be updated
+        // by the lease synchronization procedure.
+        if ((i % 2) == 0) {
+            // Add a copy of the lease to make sure that by modifying the lease
+            // contents we don't affect the lease in the database.
+            Lease4Ptr lease_to_add(new Lease4(*leases4_[i]));
+            // Modify valid lifetime of the lease in the database so we can
+            // later use this value to verify if the lease has been updated.
+            --lease_to_add->valid_lft_;
+            LeaseMgrFactory::instance().addLease(lease_to_add);
+        }
+    }
+
+    // Modify cltt of the first lease. This lease should be updated as a result
+    // of synchrnonization process because cltt is checked and the lease is
+    // updated if the cltt of the fetched lease is later than the cltt of the
+    // existing lease.
+    ++leases4_[0]->cltt_;
+
+    // For the second lease, set the wrong subnet identifier. This should be
+    // rejected and this lease shouldn't be inserted into the database.
+    // Other leases should be inserted/updated just fine.
+    ++leases4_[1]->subnet_id_ = 0;
+
+    // Modify the partner's lease cltt so it is earlier than the local lease.
+    // Therfore, this lease update should be rejected.
+    --leases4_[2]->cltt_;
+
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidConfiguration();
+
+    // Convert leases to the JSON format, the same as used by the lease_cmds
+    // hook library. Configure our test HTTP servers to return those
+    // leases in this format.
+    ElementPtr response_arguments = Element::createMap();
+    response_arguments->set("leases", getTestLeases4AsJson());
+
+    factory2_->getResponseCreator()->setArguments(response_arguments);
+    factory3_->getResponseCreator()->setArguments(response_arguments);
+
+    // Start the servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    TestHAService service(io_service_, network_state_, config_storage);
+    // Setting the heartbeat delay to 0 disables the recurring heartbeat.
+    // We just want to synchronize leases and not send the heartbeat.
+    config_storage->setHeartbeatDelay(0);
+
+    // Start fetching leases asynchronously.
+    ASSERT_NO_THROW(service.asyncSyncLeases());
+
+    // Run IO service to actually perform the transaction.
+    ASSERT_NO_THROW(runIOService(TEST_TIMEOUT, []() {
+        // Stop running the IO service if we see a lease in the lease
+        // database which is expected to be inserted as a result of lease
+        // syncing.
+        return (!LeaseMgrFactory::instance().getLeases4(SubnetID(4)).empty());
+    }));
+
+    // Check if all leases have been stored in the local database.
+    for (size_t i = 0; i < leases4_.size(); ++i) {
+        if (i == 1) {
+            // This lease was purposely malformed and thus shouldn't be
+            // inserted into the database.
+            EXPECT_FALSE(LeaseMgrFactory::instance().getLease4(leases4_[i]->addr_))
+                << "lease " << leases4_[i]->addr_.toText()
+                << " was inserted into the database, but it shouldn't";
+
+        } else {
+            // All other leases should be in the database.
+            Lease4Ptr existing_lease = LeaseMgrFactory::instance().getLease4(leases4_[i]->addr_);
+            ASSERT_TRUE(existing_lease) << "lease " << leases4_[i]->addr_.toText()
+                                        << " not in the lease database";
+            // The lease with #2 returned by the partner is older than its local instance.
+            // The local server should reject this lease.
+            if (i == 2) {
+                // The existing lease should have unmodified timestamp because the
+                // update is expected to be rejected. Same for valid lifetime.
+                EXPECT_LT(leases4_[i]->cltt_, existing_lease->cltt_);
+                EXPECT_NE(leases4_[i]->valid_lft_, existing_lease->valid_lft_);
+
+            } else {
+                // All other leases should have the same cltt.
+                EXPECT_EQ(leases4_[i]->cltt_, existing_lease->cltt_);
+
+                // Leases with even indexes were added to the database with modified
+                // valid lifetime. Thus the local copy of each such lease should have
+                // this modified valid lifetime. The lease #0 should be updated from
+                // the partner because of the partner's cltt was set to later time.
+                if ((i != 0) && (i % 2) == 0) {
+                    EXPECT_EQ(leases4_[i]->valid_lft_ - 1, existing_lease->valid_lft_);
+
+                } else {
+                    // All other leases should have been fetched from the partner and
+                    // inserted with no change.
+                    EXPECT_EQ(leases4_[i]->valid_lft_, existing_lease->valid_lft_);
+                }
+            }
+        }
+    }
+}
+
+// Test that there is no exception thrown during leases synchronization
+// when server returns a wrong answer.
+TEST_F(HAServiceTest, asyncSyncLeasesWrongAnswer) {
+    // Create lease manager.
+    ASSERT_NO_THROW(LeaseMgrFactory::create("universe=4 type=memfile persist=false"));
+
+    // Create IPv4 leases which will be fetched from the other server.
+    ASSERT_NO_THROW(generateTestLeases4());
+
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidConfiguration();
+    // Setting the heartbeat delay to 0 disables the recurring heartbeat.
+    // We just want to synchronize leases and not send the heartbeat.
+    config_storage->setHeartbeatDelay(0);
+
+    // Set empty response. This should cause the HA service to log an
+    // error but not crash.
+    ElementPtr response_arguments = Element::createMap();
+
+    factory2_->getResponseCreator()->setArguments(response_arguments);
+    factory3_->getResponseCreator()->setArguments(response_arguments);
+
+    // Start the servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    TestHAService service(io_service_, network_state_, config_storage);
+
+    // Start fetching leases asynchronously.
+    ASSERT_NO_THROW(service.asyncSyncLeases());
+
+    // Run IO service to actually perform the transaction.
+    ASSERT_NO_THROW(runIOService(1000));
+}
+
+// Test that there is no exception thrown during leases synchronization
+// when servers are offline.
+TEST_F(HAServiceTest, asyncSyncLeasesServerOffline) {
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidConfiguration();
+    // Setting the heartbeat delay to 0 disables the recurring heartbeat.
+    // We just want to synchronize leases and not send the heartbeat.
+    config_storage->setHeartbeatDelay(0);
+
+    TestHAService service(io_service_, network_state_, config_storage);
+
+    // Start fetching leases asynchronously.
+    ASSERT_NO_THROW(service.asyncSyncLeases());
+
+    // Run IO service for 1 second.
+    ASSERT_NO_THROW(runIOService(1000));
+}
+
+// This test verifies that IPv6 leases can be fetched from the peer and inserted
+// or updated in the local lease database.
+TEST_F(HAServiceTest, asyncSyncLeases6) {
+    // Create lease manager.
+    ASSERT_NO_THROW(LeaseMgrFactory::create("universe=6 type=memfile persist=false"));
+
+    // Create IPv6 leases which will be fetched from the other server.
+    ASSERT_NO_THROW(generateTestLeases6());
+
+    for (size_t i = 0; i < leases6_.size(); ++i) {
+        // For every even lease index we add this lease to the database to exercise
+        // the scenario when a lease is already in the database and may be updated
+        // by the lease synchronization procedure.
+        if ((i % 2) == 0) {
+            // Add a copy of the lease to make sure that by modifying the lease
+            // contents we don't affect the lease in the database.
+            Lease6Ptr lease_to_add(new Lease6(*leases6_[i]));
+            // Modify valid lifetime of the lease in the database so we can
+            // later use this value to verify if the lease has been updated.
+            --lease_to_add->valid_lft_;
+            LeaseMgrFactory::instance().addLease(lease_to_add);
+        }
+    }
+
+    // Modify cltt of the first lease. This lease should be updated as a result
+    // of synchrnonization process because cltt is checked and the lease is
+    // updated if the cltt of the fetched lease is later than the cltt of the
+    // existing lease.
+    ++leases6_[0]->cltt_;
+
+    // For the second lease, set the wrong subnet identifier. This should be
+    // rejected and this lease shouldn't be inserted into the database.
+    // Other leases should be inserted/updated just fine.
+    ++leases6_[1]->subnet_id_ = 0;
+
+    // Modify the partner's lease cltt so it is earlier than the local lease.
+    // Therfore, this lease update should be rejected.
+    --leases6_[2]->cltt_;
+
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidConfiguration();
+
+    // Convert leases to the JSON format, the same as used by the lease_cmds
+    // hook library. Configure our test HTTP servers to return those
+    // leases in this format.
+    ElementPtr response_arguments = Element::createMap();
+    response_arguments->set("leases", getTestLeases6AsJson());
+
+    factory2_->getResponseCreator()->setArguments(response_arguments);
+    factory3_->getResponseCreator()->setArguments(response_arguments);
+
+    // Start the servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    TestHAService service(io_service_, network_state_, config_storage,
+                          HAServerType::DHCPv6);
+    // Setting the heartbeat delay to 0 disables the recurring heartbeat.
+    // We just want to synchronize leases and not send the heartbeat.
+    config_storage->setHeartbeatDelay(0);
+
+    // Start fetching leases asynchronously.
+    ASSERT_NO_THROW(service.asyncSyncLeases());
+
+    // Run IO service to actually perform the transaction.
+    ASSERT_NO_THROW(runIOService(TEST_TIMEOUT, []() {
+        // Stop running the IO service if we see a lease in the lease
+        // database which is expected to be inserted as a result of lease
+        // syncing.
+        return (!LeaseMgrFactory::instance().getLeases6(SubnetID(4)).empty());
+    }));
+
+    // Check if all leases have been stored in the local database.
+    for (size_t i = 0; i < leases6_.size(); ++i) {
+        if (i == 1) {
+            // This lease was purposely malformed and thus shouldn't be
+            // inserted into the database.
+            EXPECT_FALSE(LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+                                                               leases6_[i]->addr_))
+                << "lease " << leases6_[i]->addr_.toText()
+                << " was inserted into the database, but it shouldn't";
+        } else {
+            // Other leases should be inserted/updated.
+            Lease6Ptr existing_lease = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+                                                                             leases6_[i]->addr_);
+            ASSERT_TRUE(existing_lease) << "lease " << leases6_[i]->addr_.toText()
+                                        << " not in the lease database";
+
+            if (i == 2) {
+                // The existing lease should have unmodified timestamp because the
+                // update is expected to be rejected. Same for valid lifetime.
+                EXPECT_LT(leases6_[i]->cltt_, existing_lease->cltt_);
+                EXPECT_NE(leases6_[i]->valid_lft_, existing_lease->valid_lft_);
+
+            } else {
+                // All other leases should have the same cltt.
+                EXPECT_EQ(leases6_[i]->cltt_, existing_lease->cltt_);
+
+                // Leases with even indexes were added to the database with modified
+                // valid lifetime. Thus the local copy of each such lease should have
+                // this modified valid lifetime. The lease #0 should be updated from
+                // the partner because of the partner's cltt was set to later time.
+                if ((i != 0) && (i % 2) == 0) {
+                    EXPECT_EQ(leases6_[i]->valid_lft_ - 1, existing_lease->valid_lft_);
+
+                } else {
+                    // All other leases should have been fetched from the partner and
+                    // inserted with no change.
+                    EXPECT_EQ(leases6_[i]->valid_lft_, existing_lease->valid_lft_);
+                }
+            }
+        }
+    }
+}
+
+// Test that there is no exception thrown during IPv6 leases synchronization
+// when server returns a wrong answer.
+TEST_F(HAServiceTest, asyncSyncLeases6WrongAnswer) {
+    // Create lease manager.
+    ASSERT_NO_THROW(LeaseMgrFactory::create("universe=6 type=memfile persist=false"));
+
+    // Create IPv6 leases which will be fetched from the other server.
+    ASSERT_NO_THROW(generateTestLeases6());
+
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidConfiguration();
+    // Setting the heartbeat delay to 0 disables the recurring heartbeat.
+    // We just want to synchronize leases and not send the heartbeat.
+    config_storage->setHeartbeatDelay(0);
+
+    // Set empty response. This should cause the HA service to log an
+    // error but not crash.
+    ElementPtr response_arguments = Element::createMap();
+
+    factory2_->getResponseCreator()->setArguments(response_arguments);
+    factory3_->getResponseCreator()->setArguments(response_arguments);
+
+    // Start the servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    TestHAService service(io_service_, network_state_, config_storage,
+                          HAServerType::DHCPv6);
+
+    // Start fetching leases asynchronously.
+    ASSERT_NO_THROW(service.asyncSyncLeases());
+
+    // Run IO service to actually perform the transaction.
+    ASSERT_NO_THROW(runIOService(1000));
+}
+
+// Test that there is no exception thrown during IPv6 leases synchronization
+// when servers are offline.
+TEST_F(HAServiceTest, asyncSyncLeases6ServerOffline) {
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidConfiguration();
+    // Setting the heartbeat delay to 0 disables the recurring heartbeat.
+    // We just want to synchronize leases and not send the heartbeat.
+    config_storage->setHeartbeatDelay(0);
+
+    TestHAService service(io_service_, network_state_, config_storage,
+                          HAServerType::DHCPv6);
+
+    // Start fetching leases asynchronously.
+    ASSERT_NO_THROW(service.asyncSyncLeases());
+
+    // Run IO service for 1 second.
+    ASSERT_NO_THROW(runIOService(1000));
+}
+
+// This test verifies that the ha-sync command is processed successfully for the
+// DHCPv4 server.
+TEST_F(HAServiceTest, processSynchronize4) {
+
+    // Run HAService::processSynchronize and gather a response.
+    ConstElementPtr rsp;
+    runProcessSynchronize4(rsp);
+
+    // The response should indicate success.
+    ASSERT_TRUE(rsp);
+    checkAnswer(rsp, CONTROL_RESULT_SUCCESS, "Lease database synchronization"
+                " complete.");
+
+    // All leases should have been inserted into the database.
+    for (size_t i = 0; i < leases4_.size(); ++i) {
+        Lease4Ptr existing_lease = LeaseMgrFactory::instance().getLease4(leases4_[i]->addr_);
+        ASSERT_TRUE(existing_lease) << "lease " << leases4_[i]->addr_.toText()
+                                    << " not in the lease database";
+    }
+
+    // The following commands should have been sent to the server2: dhcp-disable,
+    // lease4-get-all and dhcp-enable.
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-disable","20"));
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("lease4-get-all",""));
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-enable",""));
+}
+
+// This test verifies that an error is reported when sending a dhcp-disable
+// command causes an error.
+TEST_F(HAServiceTest, processSynchronizeDisableError) {
+    // Setup the server2 to return an error to dhcp-disable commands.
+    factory2_->getResponseCreator()->setControlResult("dhcp-disable",
+                                                      CONTROL_RESULT_ERROR);
+
+    // Run HAService::processSynchronize and gather a response.
+    ConstElementPtr rsp;
+    runProcessSynchronize4(rsp);
+
+    // The response should indicate an error
+    ASSERT_TRUE(rsp);
+    checkAnswer(rsp, CONTROL_RESULT_ERROR);
+
+    // The server2 should only receive dhcp-disable commands. Remaining two should
+    // not be sent.
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-disable","20"));
+    EXPECT_FALSE(factory2_->getResponseCreator()->findRequest("lease4-get-all",""));
+    EXPECT_FALSE(factory2_->getResponseCreator()->findRequest("dhcp-enable",""));
+}
+
+// This test verifies that an error is reported when sending a lease4-get-all
+// command causes an error.
+TEST_F(HAServiceTest, processSynchronizeLease4GetAllError) {
+    // Setup the server2 to return an error to dhcp-disable commands.
+    factory2_->getResponseCreator()->setControlResult("lease4-get-all",
+                                                      CONTROL_RESULT_ERROR);
+
+    // Run HAService::processSynchronize and gather a response.
+    ConstElementPtr rsp;
+    runProcessSynchronize4(rsp);
+
+    // The response should indicate an error
+    ASSERT_TRUE(rsp);
+    checkAnswer(rsp, CONTROL_RESULT_ERROR);
+
+    // The server2 should receive all commands. The dhcp-disable was successful, so
+    // the dhcp-enable command must be sent to re-enable the service after failure.
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-disable","20"));
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("lease4-get-all",""));
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-enable",""));
+}
+
+// This test verifies that an error is reported when sending a dhcp-enable
+// command causes an error.
+TEST_F(HAServiceTest, processSynchronizeEnableError) {
+    // Setup the server2 to return an error to dhcp-disable commands.
+    factory2_->getResponseCreator()->setControlResult("dhcp-enable",
+                                                      CONTROL_RESULT_ERROR);
+
+    // Run HAService::processSynchronize and gather a response.
+    ConstElementPtr rsp;
+    runProcessSynchronize4(rsp);
+
+    // The response should indicate an error
+    ASSERT_TRUE(rsp);
+    checkAnswer(rsp, CONTROL_RESULT_ERROR);
+
+    // The server2 should receive all commands.
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-disable","20"));
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("lease4-get-all",""));
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-enable",""));
+}
+
+// This test verifies that the ha-sync command is processed successfully for the
+// DHCPv6 server.
+TEST_F(HAServiceTest, processSynchronize6) {
+
+    // Run HAService::processSynchronize and gather a response.
+    ConstElementPtr rsp;
+    runProcessSynchronize6(rsp);
+
+    // The response should indicate success.
+    ASSERT_TRUE(rsp);
+    checkAnswer(rsp, CONTROL_RESULT_SUCCESS, "Lease database synchronization"
+                " complete.");
+
+    // All leases should have been inserted into the database.
+    for (size_t i = 0; i < leases6_.size(); ++i) {
+        Lease6Ptr existing_lease = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+                                                                         leases6_[i]->addr_);
+        ASSERT_TRUE(existing_lease) << "lease " << leases6_[i]->addr_.toText()
+                                    << " not in the lease database";
+    }
+
+    // The following commands should have been sent to the server2: dhcp-disable,
+    // lease6-get-all and dhcp-enable.
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-disable","20"));
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("lease6-get-all",""));
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-enable",""));
+}
+
+// This test verifies that an error is reported when sending a dhcp-disable
+// command causes an error.
+TEST_F(HAServiceTest, processSynchronize6DisableError) {
+    // Setup the server2 to return an error to dhcp-disable commands.
+    factory2_->getResponseCreator()->setControlResult("dhcp-disable",
+                                                      CONTROL_RESULT_ERROR);
+
+    // Run HAService::processSynchronize and gather a response.
+    ConstElementPtr rsp;
+    runProcessSynchronize6(rsp);
+
+    // The response should indicate an error
+    ASSERT_TRUE(rsp);
+    checkAnswer(rsp, CONTROL_RESULT_ERROR);
+
+    // The server2 should only receive dhcp-disable commands. Remaining two should
+    // not be sent.
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-disable","20"));
+    EXPECT_FALSE(factory2_->getResponseCreator()->findRequest("lease6-get-all",""));
+    EXPECT_FALSE(factory2_->getResponseCreator()->findRequest("dhcp-enable",""));
+}
+
+// This test verifies that an error is reported when sending a lease6-get-all
+// command causes an error.
+TEST_F(HAServiceTest, processSynchronizeLease6GetAllError) {
+    // Setup the server2 to return an error to dhcp-disable commands.
+    factory2_->getResponseCreator()->setControlResult("lease6-get-all",
+                                                      CONTROL_RESULT_ERROR);
+
+    // Run HAService::processSynchronize and gather a response.
+    ConstElementPtr rsp;
+    runProcessSynchronize6(rsp);
+
+    // The response should indicate an error
+    ASSERT_TRUE(rsp);
+    checkAnswer(rsp, CONTROL_RESULT_ERROR);
+
+    // The server2 should receive all commands. The dhcp-disable was successful, so
+    // the dhcp-enable command must be sent to re-enable the service after failure.
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-disable","20"));
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("lease6-get-all",""));
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-enable",""));
+}
+
+// This test verifies that an error is reported when sending a dhcp-enable
+// command causes an error.
+TEST_F(HAServiceTest, processSynchronize6EnableError) {
+    // Setup the server2 to return an error to dhcp-disable commands.
+    factory2_->getResponseCreator()->setControlResult("dhcp-enable",
+                                                      CONTROL_RESULT_ERROR);
+
+    // Run HAService::processSynchronize and gather a response.
+    ConstElementPtr rsp;
+    runProcessSynchronize6(rsp);
+
+    // The response should indicate an error
+    ASSERT_TRUE(rsp);
+    checkAnswer(rsp, CONTROL_RESULT_ERROR);
+
+    // The server2 should receive all commands.
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-disable","20"));
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("lease6-get-all",""));
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-enable",""));
+}
+
+// This test verifies that the DHCPv4 service can be disabled on the remote server.
+TEST_F(HAServiceTest, asyncDisable4) {
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidConfiguration();
+
+    // Start the servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    TestHAService service(io_service_, network_state_, config_storage);
+
+    // Send dhcp-disable command with max-period of 10 seconds.
+    // When the transaction is finished, the IO service gets stopped.
+    ASSERT_NO_THROW(service.asyncDisable("server3", 10,
+                                         [this](const bool success,
+                                                const std::string& error_message) {
+        EXPECT_TRUE(success);
+        EXPECT_TRUE(error_message.empty());
+        io_service_->stop();
+    }));
+
+    // Run IO service to actually perform the transaction.
+    ASSERT_NO_THROW(runIOService(TEST_TIMEOUT));
+
+    // The second server should not receive the command.
+    EXPECT_FALSE(factory2_->getResponseCreator()->findRequest("dhcp-disable","10"));
+    // The third server should receive the dhcp-disable command with the max-period
+    // value of 10.
+    EXPECT_TRUE(factory3_->getResponseCreator()->findRequest("dhcp-disable","10"));
+}
+
+// This test verifies that there is no exception thrown as a result of dhcp-disable
+// command when the server is offline.
+TEST_F(HAServiceTest, asyncDisable4ServerOffline) {
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidConfiguration();
+
+    TestHAService service(io_service_, network_state_, config_storage);
+
+    // Send dhcp-disable command with max-period of 10 seconds.
+    // When the transaction is finished, the IO service gets stopped.
+    ASSERT_NO_THROW(service.asyncDisable("server2", 10,
+                                         [this](const bool success,
+                                                const std::string& error_message) {
+        EXPECT_FALSE(success);
+        EXPECT_FALSE(error_message.empty());
+        io_service_->stop();
+    }));
+
+    // Run IO service to actually perform the transaction.
+    ASSERT_NO_THROW(runIOService(TEST_TIMEOUT));
+}
+
+// This test verifies that an error is returned when the remote server
+// returns control status error.
+TEST_F(HAServiceTest, asyncDisable4ControlResultError) {
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidConfiguration();
+
+    // Set the servers to return error code in response to the dhcp-enable
+    // command.
+    factory2_->getResponseCreator()->setControlResult(CONTROL_RESULT_ERROR);
+    factory3_->getResponseCreator()->setControlResult(CONTROL_RESULT_ERROR);
+
+    // Start the servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    TestHAService service(io_service_, network_state_, config_storage);
+
+    // Send dhcp-disable command with max-period of 10 seconds.
+    // When the transaction is finished, the IO service gets stopped.
+    ASSERT_NO_THROW(service.asyncDisable("server3", 10,
+                                         [this](const bool success,
+                                                const std::string& error_message) {
+        EXPECT_FALSE(success);
+        EXPECT_FALSE(error_message.empty());
+        io_service_->stop();
+    }));
+
+    // Run IO service to actually perform the transaction.
+    ASSERT_NO_THROW(runIOService(TEST_TIMEOUT));
+}
+
+// This test verifies that the DHCPv4 service can be enabled on the remote server.
+TEST_F(HAServiceTest, asyncEnable4) {
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidConfiguration();
+
+    // Start the servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    TestHAService service(io_service_, network_state_, config_storage);
+
+    // Send dhcp-enable command. When the transaction is finished,
+    // the IO service gets stopped.
+    ASSERT_NO_THROW(service.asyncEnable("server2", [this](const bool success,
+                                                          const std::string& error_message) {
+        EXPECT_TRUE(success);
+        EXPECT_TRUE(error_message.empty());
+        io_service_->stop();
+    }));
+
+    // Run IO service to actually perform the transaction.
+    ASSERT_NO_THROW(runIOService(TEST_TIMEOUT));
+
+    // The second server should receive the dhcp-enable.
+    EXPECT_TRUE(factory2_->getResponseCreator()->findRequest("dhcp-enable",""));
+    // The third server should not receive the command.
+    EXPECT_FALSE(factory3_->getResponseCreator()->findRequest("dhcp-enable",""));
+}
+
+// This test verifies that there is no exception thrown as a result of dhcp-enable
+// command when the server is offline.
+TEST_F(HAServiceTest, asyncEnable4ServerOffline) {
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidConfiguration();
+
+    TestHAService service(io_service_, network_state_, config_storage);
+
+    // Send dhcp-enable command. When the transaction is finished,
+    // the IO service gets stopped.
+    ASSERT_NO_THROW(service.asyncEnable("server2", [this](const bool success,
+                                                          const std::string& error_message) {
+        EXPECT_FALSE(success);
+        EXPECT_FALSE(error_message.empty());
+        io_service_->stop();
+    }));
+
+    // Run IO service to actually perform the transaction.
+    ASSERT_NO_THROW(runIOService(TEST_TIMEOUT));
+}
+
+// This test verifies that an error is returned when the remote server
+// returns control status error.
+TEST_F(HAServiceTest, asyncEnable4ControlResultError) {
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidConfiguration();
+
+    // Set the servers to return error code in response to the dhcp-enable
+    // command.
+    factory2_->getResponseCreator()->setControlResult(CONTROL_RESULT_ERROR);
+    factory3_->getResponseCreator()->setControlResult(CONTROL_RESULT_ERROR);
+
+    // Start the servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+        listener3_->start();
+    });
+
+    TestHAService service(io_service_, network_state_, config_storage);
+
+    // Send dhcp-enable command. When the transaction is finished,
+    // the IO service gets stopped.
+    ASSERT_NO_THROW(service.asyncEnable("server2", [this](const bool success,
+                                                          const std::string& error_message) {
+        EXPECT_FALSE(success);
+        EXPECT_FALSE(error_message.empty());
+        io_service_->stop();
+    }));
+
+    // Run IO service to actually perform the transaction.
+    ASSERT_NO_THROW(runIOService(TEST_TIMEOUT));
+}
+
+// This test verifies that the "ha-scopes" command is processed correctly.
+TEST_F(HAServiceTest, processScopes) {
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidConfiguration();
+
+    // Create HA service using this configuration.
+    TestHAService service(io_service_, network_state_, config_storage);
+
+    // Enable "server1" and "server2" scopes.
+    ConstElementPtr rsp;
+    ASSERT_NO_THROW(rsp = service.processScopes({ "server1", "server2" }));
+
+    ASSERT_TRUE(rsp);
+    checkAnswer(rsp, CONTROL_RESULT_SUCCESS, "New HA scopes configured.");
+
+    // Verify that "server1" and "server2" scopes are enabled.
+    EXPECT_TRUE(service.query_filter_.amServingScope("server1"));
+    EXPECT_TRUE(service.query_filter_.amServingScope("server2"));
+    EXPECT_FALSE(service.query_filter_.amServingScope("server3"));
+
+    // Enable "server2" scope only.
+    ASSERT_NO_THROW(rsp = service.processScopes({ "server2" }));
+    checkAnswer(rsp, CONTROL_RESULT_SUCCESS, "New HA scopes configured.");
+    EXPECT_FALSE(service.query_filter_.amServingScope("server1"));
+    EXPECT_TRUE(service.query_filter_.amServingScope("server2"));
+    EXPECT_FALSE(service.query_filter_.amServingScope("server3"));
+
+    // Clear scopes.
+    ASSERT_NO_THROW(rsp = service.processScopes({ }));
+    checkAnswer(rsp, CONTROL_RESULT_SUCCESS, "New HA scopes configured.");
+    EXPECT_FALSE(service.query_filter_.amServingScope("server1"));
+    EXPECT_FALSE(service.query_filter_.amServingScope("server2"));
+    EXPECT_FALSE(service.query_filter_.amServingScope("server3"));
+
+    // Unsupported scope.
+    ASSERT_NO_THROW(rsp = service.processScopes({ "server1", "unsupported", "server3" }));
+    checkAnswer(rsp, CONTROL_RESULT_ERROR, "invalid server name specified 'unsupported'"
+                " while enabling/disabling HA scopes");
+    // Even though the "server1" is a valid scope name, it should not be
+    // enabled because we expect scopes enabling to be atomic operation,
+    // i.e. all or nothing.
+    EXPECT_FALSE(service.query_filter_.amServingScope("server1"));
+    EXPECT_FALSE(service.query_filter_.amServingScope("server2"));
+    EXPECT_FALSE(service.query_filter_.amServingScope("server3"));
+}
+
+/// @brief HA partner to the server under test.
+///
+/// This is a wrapper class around @c HttpListener which simulates a
+/// partner server. It provides convenient methods to start, stop the
+/// parter (its listener) and to transition the partner between various
+/// HA states. Depending on the state and whether the partner is started
+/// or stopped, different answers are returned in response to the
+/// ha-heartbeat commands.
+class HAPartner {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Creates the partner instance from a listner and the corresponding
+    /// response factory. It automatically transitions the partner to the
+    /// "waiting" state unless otherwise specified with the third parameter.
+    ///
+    /// @param listner pointer to the listner to be used.
+    /// @param factory pointer to the response factory to be used. This
+    /// must be the same factory that the listener is using.
+    /// @param initial_state initial state for the partner. Default is to
+    /// transition the partner to the "waiting" state which is the default
+    /// state for each starting server.
+    HAPartner(const HttpListenerPtr& listener,
+              const TestHttpResponseCreatorFactoryPtr& factory,
+              const std::string& initial_state = "waiting")
+        : listener_(listener), factory_(factory), running_(false),
+          static_date_time_() {
+        transition(initial_state);
+    }
+
+    /// @brief Sets control result to be returned as a result of the
+    /// communication with the partner.
+    ///
+    /// @param control_result new control result value.
+    void setControlResult(const int control_result) {
+        factory_->getResponseCreator()->setControlResult(control_result);
+    }
+
+    /// @brief Sets static date-time value to be used in responses.
+    ///
+    /// @param static_date_time fixed date-time value.
+    void setDateTime(const std::string& static_date_time) {
+        static_date_time_ = static_date_time;
+    }
+
+    /// @brief Enable response to commands required for leases synchronization.
+    ///
+    /// Enables dhcp-disable, dhcp-enable and lease4-get-all commands. The last
+    /// of them returns a bunch of test leases.
+    void enableRespondLeaseFetching() {
+        // Create IPv4 leases which will be fetched from the other server.
+        std::vector<Lease4Ptr> leases4;
+        ASSERT_NO_THROW(generateTestLeases(leases4));
+
+        // Convert leases to the JSON format, the same as used by the lease_cmds
+        // hook library. Configure our test HTTP servers to return those
+        // leases in this format.
+        ElementPtr response_arguments = Element::createMap();
+        response_arguments->set("leases", getLeasesAsJson(leases4));
+
+        factory_->getResponseCreator()->setArguments("lease4-get-all", response_arguments);
+    }
+
+    /// @brief Starts up the partner.
+    void startup() {
+        if (!running_) {
+            listener_->start();
+            running_ = true;
+        }
+    }
+
+    /// @brief Shuts down the partner.
+    ///
+    /// It may be used to simulate partner's crash as well as graceful
+    /// shutdown.
+    void shutdown() {
+        if (running_) {
+            listener_->stop();
+            running_ = false;
+        }
+    }
+
+    /// @brief Transitions the partner to the specified state.
+    ///
+    /// The state is provided in the textual form and the function doesn't
+    /// validate whether it is correct or not.
+    ///
+    /// @param state new partner state.
+    void transition(const std::string& state) {
+        ElementPtr response_arguments = Element::createMap();
+        response_arguments->set("state", Element::create(state));
+        if (!static_date_time_.empty()) {
+            response_arguments->set("date-time", Element::create(static_date_time_));
+        }
+        factory_->getResponseCreator()->setArguments(response_arguments);
+    }
+
+
+private:
+
+    /// @brief Instance of the listener wrapped by this class.
+    HttpListenerPtr listener_;
+    /// @brief Instance of the response factory used by the listener.
+    TestHttpResponseCreatorFactoryPtr factory_;
+
+    /// @brief IPv4 leases to be used in the tests.
+    std::vector<Lease4Ptr> leases4_;
+
+    /// @brief Boolean flag indicating if the partner is running.
+    bool running_;
+
+    /// @brief Static date-time value to be returned.
+    std::string static_date_time_;
+};
+
+/// @brief Shared pointer to a partner.
+typedef boost::shared_ptr<HAPartner> HAPartnerPtr;
+
+/// @brief Test fixture class for the HA service state machine.
+class HAServiceStateMachineTest : public HAServiceTest {
+public:
+    /// @brief Constructor.
+    HAServiceStateMachineTest()
+        : HAServiceTest(), service_(), state_(),
+          partner_(new HAPartner(listener2_, factory2_)) {
+    }
+
+    /// @brief Creates common HA service instance from the provided configuration.
+    ///
+    /// The custom @c state_ object is created and it replaces the default
+    /// @c state_ object of the HA service.
+    ///
+    /// @param config pointer to the configuration to be used by the service.
+    /// @param server_type server type, i.e. DHCPv4 or DHCPv6.
+    void startService(const HAConfigPtr& config,
+                      const HAServerType& server_type = HAServerType::DHCPv4) {
+        config->setHeartbeatDelay(1);
+        service_.reset(new TestHAService(io_service_, network_state_, config,
+                                         server_type));
+        // Replace default communication state with custom state which exposes
+        // protected members and methods.
+        state_.reset(new NakedCommunicationState4(io_service_, config));
+        service_->communication_state_ = state_;
+        // Move the state machine from initial state to "waiting" state.
+        service_->runModel(HAService::NOP_EVT);
+    }
+
+    /// @brief Runs IO service until specified event occurs.
+    ///
+    /// This method runs IO service until state machine is run as a result
+    /// of receiving a response to an IO operation. IO operations such as
+    /// lease updates, heartbeats etc. trigger state machine changes.
+    /// We can capture certain events to detect when a response to the heartbeat
+    /// or other control commands  is received. This is useful to return control
+    /// to a test to verify that the state machine remains in the expected state
+    /// after receiving such response.
+    ///
+    /// @param event an event which should trigger IO service to stop.
+    void waitForEvent(const int event) {
+        ASSERT_NE(event, HAService::NOP_EVT);
+
+        service_->postNextEvent(HAService::NOP_EVT);
+
+        // Run IO service until the event occurs.
+        runIOService(TEST_TIMEOUT, [this, event]() {
+            return (service_->getLastEvent() == event);
+        });
+
+        service_->postNextEvent(HAService::NOP_EVT);
+    }
+
+    /// @brief Convenience method checking if HA service is currently running
+    /// recurring heartbeat.
+    ///
+    /// @return true if the heartbeat is run.
+    bool isDoingHeartbeat() {
+        return (state_->isHeartbeatRunning());
+    }
+
+    /// @brief Convenience method checking if HA service has detected communications
+    /// interrupted condition.
+    ///
+    /// @return true if the communications interrupted condition deemed, false
+    /// otherwise.
+    bool isCommunicationInterrupted() {
+        return (state_->isCommunicationInterrupted());
+    }
+
+    /// @brief Convenience method checking if communication failure has been
+    /// detected by the HA service based on the analysis of the DHCP traffic.
+    ///
+    /// @return true if the communication failure is deemed, false otherwise.
+    bool isFailureDetected() {
+        return (state_->failureDetected());
+    }
+
+    /// @brief Simulates a case when communication with the partner has failed
+    /// for a time long enough to assume communications interrupted condition.
+    ///
+    /// This case is simulated by modifying the last poking time far into the
+    /// past.
+    void simulateNoCommunication() {
+        state_->modifyPokeTime(-1000);
+    }
+
+    /// @brief Simulates reception of unanswered DHCP queries by the partner.
+    ///
+    /// This case is simulated by creating a large number of queries with
+    /// secs field set to high value.
+    void simulateDroppedQueries() {
+        // Create 100 packets. Around 50% of them should be assigned to the
+        // partner if load balancing is performed.
+        const unsigned queries_num = 100;
+        for (unsigned i = 0; i < queries_num; ++i) {
+            // Create query with random HW address.
+            Pkt4Ptr query4 = createQuery4(randomKey(HWAddr::ETHERNET_HWADDR_LEN));
+            // Set large secs field value.
+            query4->setSecs(0x00EF);
+            // This function, besides checking if the query is in scope,
+            // updates unanswered message counters. If the counters exceed
+            // a configured value the communication failure is assumed.
+            static_cast<void>(service_->inScope(query4));
+        }
+        // The state machine needs to react to the dropped queries. Therefore
+        // we run the machine now.
+        service_->runModel(HAService::NOP_EVT);
+    }
+
+    /// @brief Checks transitions dependent on the partner's state.
+    ///
+    /// This method uses @c partner_ object to control the state of the partner.
+    /// This method must not be used to test transitions from the syncing state
+    /// because this state includes synchronous IO operations. There is a
+    /// separate test for the transitions from the syncing state.
+    ///
+    /// @param my_state initial state of this server.
+    /// @param partner_state state of the partner.
+    /// @param final_state expected state to transition to.
+    void testTransition(const MyState& my_state, const PartnerState& partner_state,
+                        const FinalState& final_state) {
+        // We need to shutdown the partner only if the partner is to be in the
+        // 'unavailable state'.
+        if (partner_state.state_ != HA_UNAVAILABLE_ST) {
+            // This function is not meant for testing transitions from the syncing
+            // state when partner is available.
+            ASSERT_NE(my_state.state_, HA_SYNCING_ST);
+            partner_->setControlResult(CONTROL_RESULT_SUCCESS);
+
+        } else {
+            partner_->setControlResult(CONTROL_RESULT_ERROR);
+        }
+
+        // Transition this server to the desired initial state.
+        service_->transition(my_state.state_, HAService::NOP_EVT);
+        // Transition the partner to the desired state.
+        partner_->transition(service_->getStateLabel(partner_state.state_));
+        // Run the heartbeat.
+        waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT);
+        // Make sure that this server ended up in the expected state.
+        EXPECT_EQ(final_state.state_, service_->getCurrState())
+            << "expected transition to the '"
+            << service_->getStateLabel(final_state.state_)
+            << "' state for the partner state '" << service_->getStateLabel(partner_state.state_)
+            << "', but transitioned to the '"
+            << service_->getStateLabel(service_->getCurrState())
+            << "' state";
+
+        // If the partner is unavailable we also have to verify the case when
+        // we detect that the partner is considered offline (after running the
+        // whole failure detection algorithm).
+        if (partner_state.state_ == HA_UNAVAILABLE_ST) {
+            // Transition this server back to the initial state.
+            service_->transition(my_state.state_, HAService::NOP_EVT);
+            // Simulate lack of communication between the servers.
+            simulateNoCommunication();
+            // Send the heartbeat again.
+            waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT);
+
+            // The load balancing server or the standby server is monitoring the stream
+            // of packets directed to the partner to detect delays in responses. The
+            // primary server in the hot standby configuration doesn't do it, because
+            // the partner is not meant to process any queries until it detects that
+            // the primary server is down. This is only done in states in which the
+            // DHCP service is enabled. Otherwise, the server doesn't receive DHCP
+            // queries it could analyze.
+            if (service_->network_state_->isServiceEnabled() &&
+                ((service_->config_->getHAMode() == HAConfig::LOAD_BALANCING) ||
+                 service_->config_->getThisServerConfig()->getRole() == HAConfig::PeerConfig::STANDBY)) {
+                // The server should remain in its current state until we also detect
+                // that the partner is not answering the queries.
+                ASSERT_EQ(final_state.state_, service_->getCurrState())
+                    << "expected transition to the '"
+                    << service_->getStateLabel(final_state.state_)
+                    << "' state for the partner state '" << service_->getStateLabel(partner_state.state_)
+                    << "', but transitioned to the '"
+                    << service_->getStateLabel(service_->getCurrState())
+                    << "' state";
+
+                // Back to the original state again.
+                service_->transition(my_state.state_, HAService::NOP_EVT);
+                // This time simulate no responses from the partner to the DHCP clients'
+                // requests. This should cause the server to transition to the partner
+                // down state regardless of the initial state.
+                simulateDroppedQueries();
+                waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT);
+                EXPECT_EQ(HA_PARTNER_DOWN_ST, service_->getCurrState())
+                    << "expected transition to the 'partner-down' state, but transitioned"
+                    " to the '" << service_->getStateLabel(service_->getCurrState())
+                    << "' state";
+
+            // The primary server in the hot-standby configuration should transition to
+            // the partner-down state when there is no communication with the partner
+            // over the control channel.
+            } else {
+                EXPECT_EQ(HA_PARTNER_DOWN_ST, service_->getCurrState())
+                    << "expected transition to the 'partner-down' state, but transitioned"
+                    " to the '" << service_->getStateLabel(service_->getCurrState())
+                    << "' state";
+            }
+        }
+    }
+
+    /// @brief Checks transitions from the syncing state.
+    ///
+    /// This method uses @c partner_ object to control the state of the partner.
+    ///
+    /// @param final_state expected final server state.
+    void testSyncingTransition(const FinalState& final_state) {
+        // Transition to the syncing state.
+        service_->transition(HA_SYNCING_ST, HAService::NOP_EVT);
+        partner_->transition("ready");
+        state_->stopHeartbeat();
+
+        testSynchronousCommands([this]() {
+            service_->runModel(HAService::NOP_EVT);
+        });
+
+        state_->stopHeartbeat();
+
+        EXPECT_EQ(final_state.state_, service_->getCurrState())
+            << "expected transition to the '"
+            << service_->getStateLabel(final_state.state_)
+            << "' state" << ", but transitioned to the '"
+            << service_->getStateLabel(service_->getCurrState())
+            << "' state";
+    }
+
+    /// @brief Tests transition from any state to "terminated".
+    ///
+    /// @pasram my_state initial server state.
+    void testTerminateTransition(const MyState& my_state) {
+        // Set the partner's time way in the past so as the clock skew gets high.
+        partner_->setDateTime("Sun, 06 Nov 1994 08:49:37 GMT");
+        partner_->transition("ready");
+        // Transition this server to the desired initial state.
+        service_->transition(my_state.state_, HAService::NOP_EVT);
+        // Run the heartbeat.
+        waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT);
+        // The server should get into terminated state because of the high
+        // clock skew.
+        EXPECT_EQ(HA_TERMINATED_ST, service_->getCurrState())
+            << "expected transition to the 'terminated' state"
+            << "', but transitioned to the '"
+            << service_->getStateLabel(service_->getCurrState())
+            << "' state";
+    }
+
+    /// @brief Test that the server is serving expected scopes while being in a
+    /// certain state.
+    ///
+    /// @param my_state state of the server.
+    /// @param scopes vector of scopes which the server is expected to handle in this
+    /// state.
+    /// @param dhcp_enabled Indicates whether DHCP service is expected to be enabled
+    /// or disabled in the given state.
+    void expectScopes(const MyState& my_state, const std::vector<std::string>& scopes,
+                      const bool dhcp_enabled) {
+
+        // If expecting no scopes, let's enable some scope to make sure that the
+        // code changes this setting.
+        if (scopes.empty()) {
+            service_->query_filter_.serveScope("server1");
+
+        } else {
+            // If expecting some scopes, let's initially clear the scopes to make
+            // sure that the code sets it.
+            service_->query_filter_.serveNoScopes();
+        }
+
+        // Also, let's preset the DHCP server state to the opposite of the expected
+        // state.
+        if (dhcp_enabled) {
+            service_->network_state_->disableService();
+
+        } else {
+            service_->network_state_->enableService();
+        }
+
+        // Transition to the desired state.
+        service_->verboseTransition(my_state.state_);
+        // Run the handler.
+        service_->runModel(TestHAService::NOP_EVT);
+        // First, check that the number of handlded scope is equal to the number of
+        // scopes specified as an argument.
+        ASSERT_EQ(scopes.size(), service_->query_filter_.getServedScopes().size())
+                << "test failed for state '" << service_->getStateLabel(my_state.state_)
+                << "'";
+
+        // Now, verify that each specified scope is handled.
+        for(auto scope : scopes) {
+            EXPECT_TRUE(service_->query_filter_.amServingScope(scope))
+                << "test failed for state '" << service_->getStateLabel(my_state.state_)
+                << "'";
+        }
+        // Verify if the DHCP service is enabled or disabled.
+        EXPECT_EQ(dhcp_enabled, service_->network_state_->isServiceEnabled());
+    }
+
+    /// @brief Transitions the server to the specified state and checks if the
+    /// HA service would send lease updates in this state.
+    ///
+    /// @param my_state this server's state
+    /// @param peer_config configuration of the server to which lease updates are
+    /// to be sent.
+    /// @return true if the lease updates would be sent, false otherwise.
+    bool expectLeaseUpdates(const MyState& my_state,
+                            const HAConfig::PeerConfigPtr& peer_config) {
+        service_->verboseTransition(my_state.state_);
+        return (service_->shouldSendLeaseUpdates(peer_config));
+    }
+
+    /// @brief Transitions the server to the specified state and checks if the
+    /// HA service is sending heartbeat in this state.
+    ///
+    /// @param my_state this server's state
+    /// @return true if the heartbeat is sent in this state, false otherwise.
+    bool expectHeartbeat(const MyState& my_state) {
+        service_->verboseTransition(my_state.state_);
+        service_->runModel(TestHAService::NOP_EVT);
+        return (isDoingHeartbeat());
+    }
+
+    /// @brief Pointer to the HA service under test.
+    TestHAServicePtr service_;
+    /// @brief Pointer to the communication state used in the tests.
+    NakedCommunicationState4Ptr state_;
+    /// @brief Pointer to the partner used in some tests.
+    HAPartnerPtr partner_;
+};
+
+
+// Test the following scenario:
+// 1. I show up in waiting state and look around
+// 2. My partner doesn't respond over control channel
+// 3. I start analyzing partner's packets and see that
+//    it doesn't respond.
+// 4. I transition to partner down state.
+// 5. Partner finally shows up and eventually transitions to the ready state.
+// 6. I see the partner being ready, so I fall back to load balancing.
+// 7. Next, the partner crashes again.
+// 8. I detect partner's crash and transition back to partner down.
+// 9. While being in the partner down state, we find that the partner
+//    is available and it is doing load balancing.
+// 10. Our server transitions to the waiting state to synchronize the
+//    database and then transition to the load balancing state.
+TEST_F(HAServiceStateMachineTest, waitingParterDownLoadBalancingPartnerDown) {
+    // Start the server: offline ---> WAITING state.
+    startService(createValidConfiguration());
+    EXPECT_EQ(HA_WAITING_ST, service_->getCurrState());
+
+    // WAITING state: no heartbeat reponse for a long period of time.
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    simulateNoCommunication();
+    ASSERT_TRUE(isDoingHeartbeat());
+
+    // WAITING state: communication interrupted. In this state we don't analyze
+    // packets ('secs' field) because the DHCP service is disabled.
+    // WAITING ---> PARTNER DOWN
+    EXPECT_EQ(HA_WAITING_ST, service_->getCurrState());
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_PARTNER_DOWN_ST, service_->getCurrState());
+    ASSERT_TRUE(isDoingHeartbeat());
+
+    // PARTNER DOWN state: still no response from the partner.
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_PARTNER_DOWN_ST, service_->getCurrState());
+
+    // Partner shows up and (eventually) transitions to READY state.
+    HAPartner partner(listener2_, factory2_, "ready");
+    partner.startup();
+
+    // PARTNER DOWN state: receive a response from the partner indicating that
+    // the partner is in READY state.
+    // PARTNER DOWN ---> LOAD BALANCING
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_LOAD_BALANCING_ST, service_->getCurrState());
+    ASSERT_TRUE(isDoingHeartbeat());
+    ASSERT_FALSE(isCommunicationInterrupted());
+    ASSERT_FALSE(isFailureDetected());
+
+    // Crash the partner and see whether our server can return to the partner
+    // down state.
+    partner.setControlResult(CONTROL_RESULT_ERROR);
+
+    // LOAD BALANCING state: wait for the next heartbeat to occur and make
+    // sure that a single heartbeat loss is not yet causing us to assume
+    // partner down condition.
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_LOAD_BALANCING_ST, service_->getCurrState());
+    ASSERT_TRUE(isDoingHeartbeat());
+    ASSERT_FALSE(isCommunicationInterrupted());
+    ASSERT_FALSE(isFailureDetected());
+
+    // LOAD BALANCING state: simulate lack of communication for a longer
+    // period of time. We should still be in the load balancing state
+    // because we still need to wait for unanswered DHCP traffic.
+    simulateNoCommunication();
+    EXPECT_EQ(HA_LOAD_BALANCING_ST, service_->getCurrState());
+    ASSERT_TRUE(isDoingHeartbeat());
+    ASSERT_TRUE(isCommunicationInterrupted());
+    ASSERT_FALSE(isFailureDetected());
+
+    // LOAD BALANCING state: simulate a lot of unanswered DHCP messages to
+    // the partner. This server should detect that the partner is not
+    // answering and transition to partner down state.
+    // LOAD BALANCING ---> PARTNER DOWN
+    simulateDroppedQueries();
+    EXPECT_EQ(HA_PARTNER_DOWN_ST, service_->getCurrState());
+    ASSERT_TRUE(isDoingHeartbeat());
+    ASSERT_TRUE(isCommunicationInterrupted());
+    ASSERT_TRUE(isFailureDetected());
+
+    // Start the partner again and transition it to the load balancing state.
+    partner.setControlResult(CONTROL_RESULT_SUCCESS);
+    partner.transition("load-balancing");
+
+    // PARTNER DOWN state: it is weird situation that the partner shows up in
+    // the load-balancing state, but you can't really preclude that. Our server
+    // would rather expect it to be in the waiting or syncing state after being
+    // down but we need to deal with any status returned. If the other server
+    // is doing load balancing then the queries sent to our server aren't
+    // handled. Since this is so unusual situation we transition to the waiting
+    // state to synchronize the database and gracefully transition to the load
+    // balancing state.
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_WAITING_ST, service_->getCurrState());
+    ASSERT_TRUE(isDoingHeartbeat());
+    ASSERT_FALSE(isCommunicationInterrupted());
+    ASSERT_FALSE(isFailureDetected());
+}
+
+// Test the following scenario:
+// 1. I show up in the waiting state.
+// 2. My partner appears to be in the partner-down state.
+// 3. I proceed to the syncing state to fetch leases from the partner.
+// 4. The first attempt to fetch leases is unsuccessful.
+// 5. I remain in the syncing state until I am finally successful.
+// 6. I proceed to the ready state.
+// 7. I see that the partner is still in partner-down state, so I
+//    wait for the partner to transition to load-balancing state.
+TEST_F(HAServiceStateMachineTest, waitingSyncingReadyLoadBalancing) {
+    // Partner is present and is in the PARTNER DOWN state.
+    HAPartner partner(listener2_, factory2_, "partner-down");
+    partner.startup();
+
+    // Start the server: offline ---> WAITING state.
+    startService(createValidConfiguration());
+    EXPECT_EQ(HA_WAITING_ST, service_->getCurrState());
+
+    // WAITING state: receive a response from the partner indicating that the
+    // partner is in the load balancing state. I should transition to the
+    // SYNCING state to fetch leases from the partner.
+    // WAITING ---> SYNCING state
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_SYNCING_ST, service_->getCurrState());
+
+    // We better stop the heartbeat to not interfere with the synchronous
+    // commands.
+    state_->stopHeartbeat();
+
+    // The database synchronization is synchronous operation so we need to run
+    // the partner's IO service in thread (in background).
+    testSynchronousCommands([this, &partner]() {
+
+        // SYNCING state: the partner is up but it won't respond to the lease4-get-all
+        // command correctly. This should leave us in the SYNCING state until we finally
+        // can synchronize.
+        service_->runModel(HAService::NOP_EVT);
+        EXPECT_EQ(HA_SYNCING_ST, service_->getCurrState());
+
+        // Enable the partner to correctly respond to the lease fetching and retry.
+        // We should successfully update the database and transition.
+        // SYNCING ---> READY
+        partner.enableRespondLeaseFetching();
+        // After previous attempt to synchronize the recorded partner state became
+        // "unavailable". This server won't synchronize until the heartbeat is
+        // sent which would indicate that the server is running. Therefore, we
+        // manually set the state of the partner to "partner-down".
+        state_->setPartnerState("partner-down");
+        service_->runModel(HAService::NOP_EVT);
+        EXPECT_EQ(HA_READY_ST, service_->getCurrState());
+    });
+
+    // READY state: I do another heartbeat but my partner still seems to be in the
+    // partner down state, so I can't transition to load balancing just yet. I
+    // still remain the READY state.
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_READY_ST, service_->getCurrState());
+
+    // READY state: partner transitions to the LOAD BALANCING state seeing that I
+    // am ready. I should see this transition and also transition to that state.
+    // READY ---> LOAD BALANCING.
+    partner.transition("load-balancing");
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_LOAD_BALANCING_ST, service_->getCurrState());
+}
+
+// Test the following scenario:
+// 1. I am a primary server which is started at the same time as secondary.
+// 2. I determine that we're the primary server so I go ahead and start
+//    synchronizing.
+// 3. Partner should also determine that it is a secondary server so it should
+//    remain in the waiting state until we're ready.
+// 4. I synchronize the database and transition to the ready state.
+// 5. The partner should see that I am ready and should start the transition.
+// 6. I remain ready until the partner gets to the ready state.
+// 7. I transition to the load balancing state when the partner is ready.
+// 8. The partner transitions to the load balancing state which doesn't
+//    affect my state.
+TEST_F(HAServiceStateMachineTest, waitingSyncingReadyLoadBalancingPrimary) {
+    // Partner is present and is in the WAITING state.
+    HAPartner partner(listener2_, factory2_);
+    partner.enableRespondLeaseFetching();
+    partner.startup();
+
+    // Start the server: offline ---> WAITING state.
+    startService(createValidConfiguration());
+    EXPECT_EQ(HA_WAITING_ST, service_->getCurrState());
+
+    // WAITING state: both servers are in this state but our server is primary,
+    // so it transitions to the syncing state. The peer remains in the WAITING
+    // state until we're ready.
+    // WAITING --->SYNCING
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_SYNCING_ST, service_->getCurrState());
+
+    // We better stop the heartbeat to not interfere with the synchronous
+    // commands.
+    state_->stopHeartbeat();
+
+    // SYNCING state: this server will synchronously fetch leases from the peer.
+    // Therefore, we need to run the IO service in the thread to allow for
+    // synchronous operations to complete. Once the leases are fetched it should
+    // transition to the READY state.
+    // SYNCING ---> READY.
+    testSynchronousCommands([this]() {
+        service_->runModel(HAService::NOP_EVT);
+        EXPECT_EQ(HA_READY_ST, service_->getCurrState());
+    });
+
+    // READY state: our partner sees that we're ready so it will start to
+    // synchronize. We reamain the READY state as long as the partner is not
+    // ready.
+    partner.transition("syncing");
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_READY_ST, service_->getCurrState());
+
+    // READY state: our partner appears to be ready. We can now start load
+    // balancing.
+    // READY ---> LOAD BALANCING.
+    partner.transition("ready");
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_LOAD_BALANCING_ST, service_->getCurrState());
+
+    // LOAD BALANCING state: our partner should eventually transition to the
+    // LOAD BALANCING state. This should not affect us doing load balancing.
+    partner.transition("load-balancing");
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_LOAD_BALANCING_ST, service_->getCurrState());
+}
+
+// Test the following scenario:
+// 1. I am secondary server configured for load balancing and started at the
+//    same time as the primary server.
+// 2. I determine that I am a secondary server, so I remain in the waiting
+//    state until the primary server indicates that it is ready.
+// 3. I start synchronizing my lease database when the partner is ready.
+// 4. I transition to the ready state and leave in that state until I see
+//    the primary transition to the load balancing state.
+// 5. I also transition to the load balancing state at that point.
+TEST_F(HAServiceStateMachineTest, waitingSyncingReadyLoadBalancingSecondary) {
+    // Partner is present and is in the WAITING state.
+    HAPartner partner(listener_, factory_);
+    partner.enableRespondLeaseFetching();
+    partner.startup();
+
+    // Create the configuration in which we're the secondary server doing
+    // load balancing.
+    HAConfigPtr valid_config = createValidConfiguration();
+    valid_config->setThisServerName("server2");
+
+    // Start the server: offline ---> WAITING state.
+    startService(valid_config);
+    EXPECT_EQ(HA_WAITING_ST, service_->getCurrState());
+
+    // WAITING state: our partner is a primary so we remain in the WAITING state
+    // until it indicates it is ready.
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_WAITING_ST, service_->getCurrState());
+
+    // WAITING state: the partner is synchronizing the lease database. We still
+    // remain in the WAITING state.
+    partner.transition("syncing");
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_WAITING_ST, service_->getCurrState());
+
+    // WAITING state: partner transitions to the READY state. This is a signal
+    // for us to start synchronization of the lease database.
+    // WAITING ---> SYNCING.
+    partner.transition("ready");
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_SYNCING_ST, service_->getCurrState());
+
+    // We better stop the heartbeat to not interfere with the synchronous
+    // commands.
+    state_->stopHeartbeat();
+
+    // SYNCING state: this server will synchronously fetch leases from the peer.
+    // Therefore, we need to run the IO service in the thread to allow for
+    // synchronous operations to complete. Once the leases are fetched it should
+    // transition to the READY state.
+    // SYNCING ---> READY.
+    testSynchronousCommands([this]() {
+        service_->runModel(HAService::NOP_EVT);
+        EXPECT_EQ(HA_READY_ST, service_->getCurrState());
+    });
+
+    // READY state: we remain ready until the other server transitions to the
+    // LOAD BALANCING state.
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_READY_ST, service_->getCurrState());
+
+    // READY state: our primary server transitions to the LOAD BALANCING state.
+    // We can now also transition to this state.
+    // READY ---> LOAD BALANCING.
+    partner.transition("load-balancing");
+    ASSERT_NO_FATAL_FAILURE(waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT));
+    EXPECT_EQ(HA_LOAD_BALANCING_ST, service_->getCurrState());
+}
+
+// This test checks all combinations of server and partner states and the
+// resulting state to which the server transitions. This server is primary.
+// There is another test which validates state transitions from the
+// secondary server perspective.
+TEST_F(HAServiceStateMachineTest, stateTransitionsLoadBalancingPrimary) {
+    partner_->startup();
+
+    startService(createValidConfiguration());
+
+    // LOAD BALANCING state transitions
+    {
+        SCOPED_TRACE("LOAD BALANCING state transitions");
+
+        testTransition(MyState(HA_LOAD_BALANCING_ST), PartnerState(HA_LOAD_BALANCING_ST),
+                       FinalState(HA_LOAD_BALANCING_ST));
+
+        testTransition(MyState(HA_LOAD_BALANCING_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_LOAD_BALANCING_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_LOAD_BALANCING_ST));
+
+        testTransition(MyState(HA_LOAD_BALANCING_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_LOAD_BALANCING_ST));
+
+        testTransition(MyState(HA_LOAD_BALANCING_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_LOAD_BALANCING_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_LOAD_BALANCING_ST));
+
+        testTransition(MyState(HA_LOAD_BALANCING_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_LOAD_BALANCING_ST));
+    }
+
+    // PARTNER DOWN state transitions
+    {
+        SCOPED_TRACE("PARTNER DOWN state transitions");
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_LOAD_BALANCING_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_LOAD_BALANCING_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_PARTNER_DOWN_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_PARTNER_DOWN_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_PARTNER_DOWN_ST));
+    }
+
+    // READY state transitions
+    {
+        SCOPED_TRACE("READY state transitions");
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_LOAD_BALANCING_ST),
+                       FinalState(HA_LOAD_BALANCING_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_READY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_LOAD_BALANCING_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_READY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_READY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_READY_ST));
+    }
+
+    // WAITING state transitions
+    {
+        SCOPED_TRACE("WAITING state transitions");
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_LOAD_BALANCING_ST),
+                       FinalState(HA_SYNCING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_SYNCING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_SYNCING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_SYNCING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_WAITING_ST));
+    }
+}
+
+// This test checks that the server in the load balancing mode does not
+// transition to the "syncing" state when "sync-leases" is disabled.
+TEST_F(HAServiceStateMachineTest, noSyncingTransitionsLoadBalancingPrimary) {
+    partner_->startup();
+
+    HAConfigPtr valid_config = createValidConfiguration();
+    valid_config->setSyncLeases(false);
+    startService(valid_config);
+
+    testTransition(MyState(HA_WAITING_ST), PartnerState(HA_LOAD_BALANCING_ST),
+                   FinalState(HA_READY_ST));
+
+    testTransition(MyState(HA_WAITING_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                   FinalState(HA_READY_ST));
+
+    testTransition(MyState(HA_WAITING_ST), PartnerState(HA_READY_ST),
+                   FinalState(HA_READY_ST));
+}
+
+// This test checks that the server in the load balancing mode transitions to
+// the "terminated" state when the clock skew gets high.
+TEST_F(HAServiceStateMachineTest, terminateTransitionsLoadBalancingPrimary) {
+    partner_->startup();
+
+    startService(createValidConfiguration());
+
+    testTerminateTransition(MyState(HA_LOAD_BALANCING_ST));
+    testTerminateTransition(MyState(HA_PARTNER_DOWN_ST));
+    testTerminateTransition(MyState(HA_READY_ST));
+    testTerminateTransition(MyState(HA_WAITING_ST));
+}
+
+// This test checks all combinations of server and partner states and the
+// resulting state to which the server transitions. This server is secondary.
+// There is another test which validates state transitions from the
+// primary server perspective.
+TEST_F(HAServiceStateMachineTest, stateTransitionsLoadBalancingSecondary) {
+    partner_.reset(new HAPartner(listener_, factory_));
+    partner_->startup();
+
+    HAConfigPtr valid_config = createValidConfiguration();
+    valid_config->setThisServerName("server2");
+    startService(valid_config);
+
+    partner_->startup();
+
+    // LOAD BALANCING state transitions
+    {
+        SCOPED_TRACE("LOAD BALANCING state transitions");
+
+        testTransition(MyState(HA_LOAD_BALANCING_ST), PartnerState(HA_LOAD_BALANCING_ST),
+                       FinalState(HA_LOAD_BALANCING_ST));
+
+        testTransition(MyState(HA_LOAD_BALANCING_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_LOAD_BALANCING_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_LOAD_BALANCING_ST));
+
+        testTransition(MyState(HA_LOAD_BALANCING_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_LOAD_BALANCING_ST));
+
+        testTransition(MyState(HA_LOAD_BALANCING_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_LOAD_BALANCING_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_LOAD_BALANCING_ST));
+
+        testTransition(MyState(HA_LOAD_BALANCING_ST), PartnerState(HA_UNAVAILABLE_ST),
+                         FinalState(HA_LOAD_BALANCING_ST));
+    }
+
+    // PARTNER DOWN state transitions
+    {
+        SCOPED_TRACE("PARTNER DOWN state transitions");
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_LOAD_BALANCING_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_LOAD_BALANCING_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_PARTNER_DOWN_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_PARTNER_DOWN_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_PARTNER_DOWN_ST));
+    }
+
+    // READY state transitions
+    {
+        SCOPED_TRACE("READY state transitions");
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_LOAD_BALANCING_ST),
+                       FinalState(HA_LOAD_BALANCING_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_READY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_READY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_READY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_READY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_READY_ST));
+    }
+
+    // WAITING state transitions
+    {
+        SCOPED_TRACE("WAITING state transitions");
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_LOAD_BALANCING_ST),
+                       FinalState(HA_SYNCING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_SYNCING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_SYNCING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_WAITING_ST));
+    }
+}
+
+// This test checks that the server in the load balancing mode does not
+// transition to the "syncing" state when "sync-leases" is disabled.
+// This is the secondary server case.
+TEST_F(HAServiceStateMachineTest, noSyncingTransitionsLoadBalancingSecondary) {
+    partner_.reset(new HAPartner(listener_, factory_));
+    partner_->startup();
+
+    HAConfigPtr valid_config = createValidConfiguration();
+    valid_config->setThisServerName("server2");
+    valid_config->setSyncLeases(false);
+    startService(valid_config);
+
+    testTransition(MyState(HA_WAITING_ST), PartnerState(HA_LOAD_BALANCING_ST),
+                   FinalState(HA_READY_ST));
+
+    testTransition(MyState(HA_WAITING_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                   FinalState(HA_READY_ST));
+
+    testTransition(MyState(HA_WAITING_ST), PartnerState(HA_READY_ST),
+                   FinalState(HA_READY_ST));
+}
+
+// This test checks that the secondary server in the load balancing mode
+// transitions to the "terminated" state when the clock skew gets high.
+TEST_F(HAServiceStateMachineTest, terminateTransitionsLoadBalancingSecondary) {
+    partner_.reset(new HAPartner(listener_, factory_));
+    partner_->startup();
+
+    HAConfigPtr valid_config = createValidConfiguration();
+    valid_config->setThisServerName("server2");
+    startService(valid_config);
+
+    testTerminateTransition(MyState(HA_LOAD_BALANCING_ST));
+    testTerminateTransition(MyState(HA_PARTNER_DOWN_ST));
+    testTerminateTransition(MyState(HA_READY_ST));
+    testTerminateTransition(MyState(HA_WAITING_ST));
+}
+
+// This test verifies that the backup server transitions to its own state.
+TEST_F(HAServiceStateMachineTest, stateTransitionsLoadBalancingBackup) {
+    HAConfigPtr valid_config = createValidConfiguration();
+
+    // server3 is marked as a backup server.
+    valid_config->setThisServerName("server3");
+    startService(valid_config);
+
+    // The server should transition to the backup state and stay there.
+    for (unsigned i = 0; i < 10; ++i) {
+        service_->runModel(HAService::NOP_EVT);
+        ASSERT_EQ(HA_BACKUP_ST, service_->getCurrState());
+        // In the backup state the DHCP service is disabled by default.
+        // It can only be enabled manually.
+        ASSERT_FALSE(service_->network_state_->isServiceEnabled());
+        ASSERT_EQ(0, service_->query_filter_.getServedScopes().size());
+    }
+}
+
+// This test verifies transitions from the syncing state in the load
+// balancing configuration.
+TEST_F(HAServiceStateMachineTest, syncingTransitionsLoadBalancing) {
+    HAConfigPtr valid_config = createValidConfiguration();
+    startService(valid_config);
+    waitForEvent(HAService::HA_HEARTBEAT_COMPLETE_EVT);
+
+    // The syncing state handler doesn't start synchronization until it
+    // detects that the partner is online. It may remember that from the
+    // previous heartbeat attempts. If the partner appears to be unavailable
+    // it will continue heartbeats before it synchronizes. This prevents the
+    // server from making endless attempts to synchronize without any chance
+    // to succeed. We verify that the server is not trying to synchronize
+    // by checking that the last event is not the one associated with the
+    // synchronization attempt.
+    ASSERT_NE(service_->getLastEvent(), HAService::HA_SYNCING_FAILED_EVT);
+    ASSERT_NE(service_->getLastEvent(), HAService::HA_SYNCING_SUCCEEDED_EVT);
+
+    // Run the syncing state handler.
+    testSyncingTransition(FinalState(HA_SYNCING_ST));
+
+    // We should see no synchronization attempts because the partner is
+    // offline.
+    EXPECT_NE(service_->getLastEvent(), HAService::HA_SYNCING_FAILED_EVT);
+    EXPECT_NE(service_->getLastEvent(), HAService::HA_SYNCING_SUCCEEDED_EVT);
+
+    // Startup the partner.
+    partner_->enableRespondLeaseFetching();
+    partner_->startup();
+
+    // We haven't been running heartbeats so we have to manually set the
+    // partner's state to something other than 'unavailable'.
+    state_->setPartnerState("ready");
+
+    // Retry the test.
+    testSyncingTransition(FinalState(HA_READY_ST));
+    // This time the server should have synchronized.
+    EXPECT_EQ(HAService::HA_SYNCING_SUCCEEDED_EVT, service_->getLastEvent());
+}
+
+// This test verifies that the server takes ownership of the given scopes
+// and whether the DHCP service is disabled or enabled in certain states.
+TEST_F(HAServiceStateMachineTest, scopesServingLoadBalancing) {
+    startService(createValidConfiguration());
+
+    // LOAD BALANCING and TERMINATED: serving my own scope.
+    expectScopes(MyState(HA_LOAD_BALANCING_ST), { "server1" }, true);
+    expectScopes(MyState(HA_TERMINATED_ST), { "server1" }, true);
+
+    // PARTNER DOWN: serving both scopes.
+    expectScopes(MyState(HA_PARTNER_DOWN_ST), { "server1", "server2" }, true);
+
+    // READY & WAITING: serving no scopes.
+    expectScopes(MyState(HA_READY_ST), { }, false);
+    expectScopes(MyState(HA_WAITING_ST), { }, false);
+}
+
+// This test verifies that the server does not take ownership of the
+// partner's scope when auto-failover parameter is set to false.
+TEST_F(HAServiceStateMachineTest, scopesServingLoadBalancingNoFailover) {
+    HAConfigPtr valid_config = createValidConfiguration();
+    valid_config->getThisServerConfig()->setAutoFailover(false);
+    startService(valid_config);
+
+    // LOAD BALANCING and TERMINATED: serving my own scope.
+    expectScopes(MyState(HA_LOAD_BALANCING_ST), { "server1" }, true);
+    expectScopes(MyState(HA_TERMINATED_ST), { "server1" }, true);
+
+    // PARTNER DOWN: still serving my own scope because auto-failover
+    // is disabled.
+    expectScopes(MyState(HA_PARTNER_DOWN_ST), { "server1" }, true);
+
+    // READY & WAITING: serving no scopes.
+    expectScopes(MyState(HA_READY_ST), { }, false);
+    expectScopes(MyState(HA_WAITING_ST), { }, false);
+}
+
+// This test verifies if the server would send lease updates to the partner
+// while being in various states. The HA configuration is load balancing.
+TEST_F(HAServiceStateMachineTest, shouldSendLeaseUpdatesLoadBalancing) {
+    HAConfigPtr valid_config = createValidConfiguration();
+    startService(valid_config);
+
+    HAConfig::PeerConfigPtr peer_config = valid_config->getFailoverPeerConfig();
+
+    EXPECT_TRUE(expectLeaseUpdates(MyState(HA_LOAD_BALANCING_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_PARTNER_DOWN_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_READY_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_SYNCING_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_TERMINATED_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_WAITING_ST), peer_config));
+}
+
+
+// This test verifies if the server would not send lease updates to the
+// partner if lease updates are administratively disabled.
+TEST_F(HAServiceStateMachineTest, shouldSendLeaseUpdatesDisabledLoadBalancing) {
+    HAConfigPtr valid_config = createValidConfiguration();
+    valid_config->setSendLeaseUpdates(false);
+    startService(valid_config);
+
+    HAConfig::PeerConfigPtr peer_config = valid_config->getFailoverPeerConfig();
+
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_LOAD_BALANCING_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_PARTNER_DOWN_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_READY_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_SYNCING_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_TERMINATED_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_WAITING_ST), peer_config));
+}
+
+// This test verifies if the server would send heartbeat to the partner
+// while being in various states. The HA configuration is load balancing.
+TEST_F(HAServiceStateMachineTest, heartbeatLoadBalancing) {
+    HAConfigPtr valid_config = createValidConfiguration();
+    startService(valid_config);
+
+    EXPECT_TRUE(expectHeartbeat(MyState(HA_LOAD_BALANCING_ST)));
+    EXPECT_TRUE(expectHeartbeat(MyState(HA_PARTNER_DOWN_ST)));
+    EXPECT_TRUE(expectHeartbeat(MyState(HA_READY_ST)));
+    EXPECT_FALSE(expectHeartbeat(MyState(HA_TERMINATED_ST)));
+    EXPECT_TRUE(expectHeartbeat(MyState(HA_WAITING_ST)));
+}
+
+// This test checks all combinations of server and partner states and the
+// resulting state to which the server transitions. This server is primary.
+// There is another test which validates state transitions from the
+// standby server perspective.
+TEST_F(HAServiceStateMachineTest, stateTransitionsHotStandbyPrimary) {
+    partner_->startup();
+
+    HAConfigPtr valid_config = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    valid_config->setHAMode("hot-standby");
+    valid_config->getPeerConfig("server2")->setRole("standby");
+
+    startService(valid_config);
+
+
+    // HOT STANDBY state transitions
+    {
+        SCOPED_TRACE("HOT STANDBY state transitions");
+
+        testTransition(MyState(HA_HOT_STANDBY_ST), PartnerState(HA_HOT_STANDBY_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+
+        testTransition(MyState(HA_HOT_STANDBY_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_HOT_STANDBY_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+
+        testTransition(MyState(HA_HOT_STANDBY_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+
+        testTransition(MyState(HA_HOT_STANDBY_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_HOT_STANDBY_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+
+        testTransition(MyState(HA_HOT_STANDBY_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+    }
+
+    // PARTNER DOWN state transitions
+    {
+        SCOPED_TRACE("PARTNER DOWN state transitions");
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_HOT_STANDBY_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_PARTNER_DOWN_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_PARTNER_DOWN_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_PARTNER_DOWN_ST));
+    }
+
+    // READY state transitions
+    {
+        SCOPED_TRACE("READY state transitions");
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_HOT_STANDBY_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_READY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_READY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_READY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_READY_ST));
+    }
+
+    // WAITING state transitions
+    {
+        SCOPED_TRACE("WAITING state transitions");
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_HOT_STANDBY_ST),
+                       FinalState(HA_SYNCING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_SYNCING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_SYNCING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_SYNCING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_WAITING_ST));
+    }
+}
+
+// This test checks that the server in the hot standby mode does not
+// transition to the "syncing" state when "sync-leases" is disabled.
+// This is the primary server case.
+TEST_F(HAServiceStateMachineTest, noSyncingTransitionsHotStandbyPrimary) {
+    partner_->startup();
+
+    HAConfigPtr valid_config = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    valid_config->setHAMode("hot-standby");
+    valid_config->getPeerConfig("server2")->setRole("standby");
+    valid_config->setSyncLeases(false);
+
+    startService(valid_config);
+
+    testTransition(MyState(HA_WAITING_ST), PartnerState(HA_LOAD_BALANCING_ST),
+                   FinalState(HA_READY_ST));
+
+    testTransition(MyState(HA_WAITING_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                   FinalState(HA_READY_ST));
+
+    testTransition(MyState(HA_WAITING_ST), PartnerState(HA_READY_ST),
+                   FinalState(HA_READY_ST));
+}
+
+// This test checks that the primary server in the hot standby mode
+// transitions to the "terminated" state when the clock skew gets high.
+TEST_F(HAServiceStateMachineTest, terminateTransitionsHotStandbyPrimary) {
+    partner_->startup();
+
+    HAConfigPtr valid_config = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    valid_config->setHAMode("hot-standby");
+    valid_config->getPeerConfig("server2")->setRole("standby");
+
+    startService(valid_config);
+
+    testTerminateTransition(MyState(HA_HOT_STANDBY_ST));
+    testTerminateTransition(MyState(HA_PARTNER_DOWN_ST));
+    testTerminateTransition(MyState(HA_READY_ST));
+    testTerminateTransition(MyState(HA_WAITING_ST));
+}
+
+// This test checks all combinations of server and partner states and the
+// resulting state to which the server transitions. This server is standby.
+// There is another test which validates state transitions from the
+// primary server perspective.
+TEST_F(HAServiceStateMachineTest, stateTransitionsHotStandbyStandby) {
+    partner_.reset(new HAPartner(listener_, factory_));
+    partner_->startup();
+
+    HAConfigPtr valid_config = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    valid_config->setThisServerName("server2");
+    valid_config->setHAMode("hot-standby");
+    valid_config->getPeerConfig("server2")->setRole("standby");
+
+    startService(valid_config);
+
+    // HOT STANDBY state transitions
+    {
+        SCOPED_TRACE("HOT STANDBY state transitions");
+
+        testTransition(MyState(HA_HOT_STANDBY_ST), PartnerState(HA_HOT_STANDBY_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+
+        testTransition(MyState(HA_HOT_STANDBY_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_HOT_STANDBY_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+
+        testTransition(MyState(HA_HOT_STANDBY_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+
+        testTransition(MyState(HA_HOT_STANDBY_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_HOT_STANDBY_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+
+        testTransition(MyState(HA_HOT_STANDBY_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+    }
+
+    // PARTNER DOWN state transitions
+    {
+        SCOPED_TRACE("PARTNER DOWN state transitions");
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_HOT_STANDBY_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_PARTNER_DOWN_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_PARTNER_DOWN_ST));
+
+        testTransition(MyState(HA_PARTNER_DOWN_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_PARTNER_DOWN_ST));
+    }
+
+    // READY state transitions
+    {
+        SCOPED_TRACE("READY state transitions");
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_HOT_STANDBY_ST),
+                       FinalState(HA_HOT_STANDBY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_READY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_READY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_READY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_READY_ST));
+
+        testTransition(MyState(HA_READY_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_READY_ST));
+    }
+
+    // WAITING state transitions
+    {
+        SCOPED_TRACE("WAITING state transitions");
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_HOT_STANDBY_ST),
+                       FinalState(HA_SYNCING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                       FinalState(HA_SYNCING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_READY_ST),
+                       FinalState(HA_SYNCING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_SYNCING_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_TERMINATED_ST),
+                       FinalState(HA_TERMINATED_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_WAITING_ST),
+                       FinalState(HA_WAITING_ST));
+
+        testTransition(MyState(HA_WAITING_ST), PartnerState(HA_UNAVAILABLE_ST),
+                       FinalState(HA_WAITING_ST));
+    }
+}
+
+// This test checks that the server in the hot standby mode does not
+// transition to the "syncing" state when "sync-leases" is disabled.
+// This is the standby server case.
+TEST_F(HAServiceStateMachineTest, noSyncingTransitionsHotStandbyStandby) {
+    partner_.reset(new HAPartner(listener_, factory_));
+    partner_->startup();
+
+    HAConfigPtr valid_config = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    valid_config->setThisServerName("server2");
+    valid_config->setHAMode("hot-standby");
+    valid_config->getPeerConfig("server2")->setRole("standby");
+    valid_config->setSyncLeases(false);
+
+    startService(valid_config);
+
+    testTransition(MyState(HA_WAITING_ST), PartnerState(HA_LOAD_BALANCING_ST),
+                   FinalState(HA_READY_ST));
+
+    testTransition(MyState(HA_WAITING_ST), PartnerState(HA_PARTNER_DOWN_ST),
+                   FinalState(HA_READY_ST));
+
+    testTransition(MyState(HA_WAITING_ST), PartnerState(HA_READY_ST),
+                   FinalState(HA_READY_ST));
+}
+
+// This test checks that the standby server in the hot standby mode
+// transitions to the "terminated" state when the clock skew gets high.
+TEST_F(HAServiceStateMachineTest, terminateTransitionsHotStandbyStandby) {
+    partner_.reset(new HAPartner(listener_, factory_));
+    partner_->startup();
+
+    HAConfigPtr valid_config = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    valid_config->setThisServerName("server2");
+    valid_config->setHAMode("hot-standby");
+    valid_config->getPeerConfig("server2")->setRole("standby");
+
+    startService(valid_config);
+
+    testTerminateTransition(MyState(HA_HOT_STANDBY_ST));
+    testTerminateTransition(MyState(HA_PARTNER_DOWN_ST));
+    testTerminateTransition(MyState(HA_READY_ST));
+    testTerminateTransition(MyState(HA_WAITING_ST));
+}
+
+// This test verifies that the server takes ownership of the given scopes
+// and whether the DHCP service is disabled or enabled in certain states.
+// This is primary server.
+TEST_F(HAServiceStateMachineTest, scopesServingHotStandbyPrimary) {
+    HAConfigPtr valid_config = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    valid_config->setHAMode("hot-standby");
+    valid_config->getPeerConfig("server2")->setRole("standby");
+
+    startService(valid_config);
+
+    // HOT STANDBY and TERMINATED: serving my own scope
+    expectScopes(MyState(HA_HOT_STANDBY_ST), { "server1" }, true);
+    expectScopes(MyState(HA_TERMINATED_ST), { "server1" }, true);
+
+    // PARTNER DOWN: still serving my own scope.
+    expectScopes(MyState(HA_PARTNER_DOWN_ST), { "server1" }, true);
+
+    // READY & WAITING: serving no scopes.
+    expectScopes(MyState(HA_READY_ST), { }, false);
+    expectScopes(MyState(HA_WAITING_ST), { }, false);
+}
+
+// This test verifies that auto-failover setting does not affect scopes
+// handling by the primary server in the hot-standby mode.
+TEST_F(HAServiceStateMachineTest, scopesServingHotStandbyPrimaryNoFailover) {
+    HAConfigPtr valid_config = createValidConfiguration();
+    // Turn it into hot-standby configuration.
+    valid_config->setHAMode("hot-standby");
+    valid_config->getPeerConfig("server2")->setRole("standby");
+
+    // Disable auto-failover.
+    valid_config->getThisServerConfig()->setAutoFailover(false);
+
+    startService(valid_config);
+
+    // HOT STANDBY and TERMINATED: serving my own scope
+    expectScopes(MyState(HA_HOT_STANDBY_ST), { "server1" }, true);
+    expectScopes(MyState(HA_TERMINATED_ST), { "server1" }, true);
+
+    // PARTNER DOWN: still serving my own scope.
+    expectScopes(MyState(HA_PARTNER_DOWN_ST), { "server1" }, true);
+
+    // READY & WAITING: serving no scopes.
+    expectScopes(MyState(HA_READY_ST), { }, false);
+    expectScopes(MyState(HA_WAITING_ST), { }, false);
+}
+
+// This test verifies if the server would send lease updates to the partner
+// while being in various states. The HA configuration is hot standby and
+// the server is primary.
+TEST_F(HAServiceStateMachineTest, shouldSendLeaseUpdatesHotStandbyPrimary) {
+    HAConfigPtr valid_config = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    valid_config->setHAMode("hot-standby");
+    valid_config->getPeerConfig("server2")->setRole("standby");
+
+    startService(valid_config);
+
+    HAConfig::PeerConfigPtr peer_config = valid_config->getFailoverPeerConfig();
+
+    EXPECT_TRUE(expectLeaseUpdates(MyState(HA_HOT_STANDBY_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_PARTNER_DOWN_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_READY_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_SYNCING_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_TERMINATED_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_WAITING_ST), peer_config));
+}
+
+// This test verifies if the server would send heartbeat to the partner
+// while being in various states. The HA configuration is hot standby.
+TEST_F(HAServiceStateMachineTest, heartbeatHotstandby) {
+    HAConfigPtr valid_config = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    valid_config->setHAMode("hot-standby");
+    valid_config->getPeerConfig("server2")->setRole("standby");
+
+    startService(valid_config);
+
+    EXPECT_TRUE(expectHeartbeat(MyState(HA_HOT_STANDBY_ST)));
+    EXPECT_TRUE(expectHeartbeat(MyState(HA_PARTNER_DOWN_ST)));
+    EXPECT_TRUE(expectHeartbeat(MyState(HA_READY_ST)));
+    EXPECT_FALSE(expectHeartbeat(MyState(HA_TERMINATED_ST)));
+    EXPECT_TRUE(expectHeartbeat(MyState(HA_WAITING_ST)));
+}
+
+// This test verifies that the server takes ownership of the given scopes
+// and whether the DHCP service is disabled or enabled in certain states.
+// This is standby server.
+TEST_F(HAServiceStateMachineTest, scopesServingHotStandbyStandby) {
+    HAConfigPtr valid_config = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    valid_config->setThisServerName("server2");
+    valid_config->setHAMode("hot-standby");
+    valid_config->getPeerConfig("server2")->setRole("standby");
+
+    startService(valid_config);
+
+    // HOT STANDBY and TERMINATED: serving no scopes becuase server 2 is active.
+    expectScopes(MyState(HA_HOT_STANDBY_ST), { }, false);
+    expectScopes(MyState(HA_TERMINATED_ST), { }, false);
+
+    // PARTNER DOWN: serving server1's scope.
+    expectScopes(MyState(HA_PARTNER_DOWN_ST), { "server1" }, true);
+
+    // READY & WAITING: serving no scopes.
+    expectScopes(MyState(HA_READY_ST), { }, false);
+    expectScopes(MyState(HA_WAITING_ST), { }, false);
+}
+
+// This test verifies that the standby server does not take ownership
+// of the primary server's scope when auto-failover is set to false
+TEST_F(HAServiceStateMachineTest, scopesServingHotStandbyStandbyNoFailover) {
+    HAConfigPtr valid_config = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    valid_config->setThisServerName("server2");
+    valid_config->setHAMode("hot-standby");
+    valid_config->getPeerConfig("server2")->setRole("standby");
+
+    // Disable auto-failover.
+    valid_config->getThisServerConfig()->setAutoFailover(false);
+
+    startService(valid_config);
+
+    // HOT STANDBY and TERMINATED: serving no scopes becuase server 2 is active.
+    expectScopes(MyState(HA_HOT_STANDBY_ST), { }, false);
+    expectScopes(MyState(HA_TERMINATED_ST), { }, false);
+
+    // PARTNER DOWN: still serving no scopes because auto-failover is
+    // set to false.
+    expectScopes(MyState(HA_PARTNER_DOWN_ST), { }, false);
+
+    // READY & WAITING: serving no scopes.
+    expectScopes(MyState(HA_READY_ST), { }, false);
+    expectScopes(MyState(HA_WAITING_ST), { }, false);
+}
+
+// This test verifies if the server would send lease updates to the partner
+// while being in various states. The HA configuration is hot standby and
+// the server is secondary.
+TEST_F(HAServiceStateMachineTest, shouldSendLeaseUpdatesHotStandbyStandby) {
+    HAConfigPtr valid_config = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    valid_config->setThisServerName("server2");
+    valid_config->setHAMode("hot-standby");
+    valid_config->getPeerConfig("server2")->setRole("standby");
+
+    startService(valid_config);
+
+    HAConfig::PeerConfigPtr peer_config = valid_config->getFailoverPeerConfig();
+
+    EXPECT_TRUE(expectLeaseUpdates(MyState(HA_HOT_STANDBY_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_PARTNER_DOWN_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_READY_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_SYNCING_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_TERMINATED_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_WAITING_ST), peer_config));
+}
+
+// This test verifies if the backup server doesn't send lease updates.
+TEST_F(HAServiceStateMachineTest, shouldSendLeaseUpdatesBackup) {
+    HAConfigPtr valid_config = createValidConfiguration();
+
+    // Turn it into hot-standby configuration.
+    valid_config->setThisServerName("server3");
+
+    startService(valid_config);
+
+    HAConfig::PeerConfigPtr peer_config = valid_config->getPeerConfig("server1");
+
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_BACKUP_ST), peer_config));
+    EXPECT_FALSE(expectLeaseUpdates(MyState(HA_WAITING_ST), peer_config));
+}
+
+}
diff --git a/src/hooks/dhcp/high_availability/tests/ha_test.cc b/src/hooks/dhcp/high_availability/tests/ha_test.cc
new file mode 100644 (file)
index 0000000..c3aefb6
--- /dev/null
@@ -0,0 +1,301 @@
+// Copyright (C) 2017-2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <config.h>
+
+#include <asiolink/asio_wrapper.h>
+#include <ha_test.h>
+#include <asiolink/interval_timer.h>
+#include <cc/command_interpreter.h>
+#include <cc/data.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
+#include <dhcp/option.h>
+#include <dhcp/option_int.h>
+#include <hooks/hooks.h>
+#include <hooks/hooks_manager.h>
+#include <util/range_utilities.h>
+#include <boost/bind.hpp>
+#include <utility>
+#include <vector>
+
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::hooks;
+
+namespace {
+
+/// @brief Structure that holds registered hook indexes.
+struct TestHooks {
+    int hooks_index_dhcp4_srv_configured_;
+
+    /// Constructor that registers hook points for the tests.
+    TestHooks() {
+        hooks_index_dhcp4_srv_configured_ = HooksManager::registerHook("dhcp4_srv_configured");
+    }
+};
+
+TestHooks Hooks;
+
+}
+
+namespace isc {
+namespace ha {
+namespace test {
+
+HATest::HATest()
+    : io_service_(new IOService()),
+      network_state_(new NetworkState(NetworkState::DHCPv4)) {
+}
+
+HATest::~HATest() {
+}
+
+void
+HATest::startHAService() {
+    if (HooksManager::calloutsPresent(Hooks.hooks_index_dhcp4_srv_configured_)) {
+        CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+        callout_handle->setArgument("io_context", io_service_);
+        callout_handle->setArgument("network_state", network_state_);
+        HooksManager::callCallouts(Hooks.hooks_index_dhcp4_srv_configured_,
+                                   *callout_handle);
+    }
+}
+
+void
+HATest::runIOService(long ms) {
+    io_service_->get_io_service().reset();
+    IntervalTimer timer(*io_service_);
+    timer.setup(boost::bind(&IOService::stop, io_service_), ms,
+                IntervalTimer::ONE_SHOT);
+    io_service_->run();
+    timer.cancel();
+}
+
+void
+HATest::runIOService(long ms, std::function<bool()> stop_condition) {
+    io_service_->get_io_service().reset();
+    IntervalTimer timer(*io_service_);
+    bool timeout = false;
+    timer.setup(boost::bind(&HATest::stopIOServiceHandler, this, boost::ref(timeout)),
+                ms, IntervalTimer::ONE_SHOT);
+
+    while (!stop_condition() && !timeout) {
+        io_service_->run_one();
+    }
+
+    timer.cancel();
+}
+
+boost::shared_ptr<util::thread::Thread>
+HATest::runIOServiceInThread() {
+    io_service_->get_io_service().reset();
+
+    bool running = false;
+    util::thread::Mutex mutex;
+    util::thread::CondVar condvar;
+
+    io_service_->post(boost::bind(&HATest::signalServiceRunning, this, boost::ref(running),
+                                  boost::ref(mutex), boost::ref(condvar)));
+    boost::shared_ptr<util::thread::Thread>
+        th(new util::thread::Thread(boost::bind(&IOService::run, io_service_.get())));
+
+    util::thread::Mutex::Locker lock(mutex);
+    while (!running) {
+        condvar.wait(mutex);
+    }
+
+    return (th);
+}
+
+void
+HATest::testSynchronousCommands(std::function<void()> commands) {
+    // Run IO service in thread.
+    auto thread = runIOServiceInThread();
+
+    // Run desired commands.
+    commands();
+
+    // Stop the IO service. This should cause the thread to terminate.
+    io_service_->stop();
+    thread->wait();
+}
+
+void
+HATest::signalServiceRunning(bool& running, util::thread::Mutex& mutex,
+                             util::thread::CondVar& condvar) {
+    {
+        util::thread::Mutex::Locker lock(mutex);
+        running = true;
+    }
+    condvar.signal();
+}
+
+void
+HATest::stopIOServiceHandler(bool& stop_flag) {
+    stop_flag = true;
+}
+
+ConstElementPtr
+HATest::createValidJsonConfiguration(const HAConfig::HAMode& ha_mode) const {
+    std::ostringstream config_text;
+    config_text <<
+        "["
+        "     {"
+        "         \"this-server-name\": \"server1\","
+        "         \"mode\": " << (ha_mode == HAConfig::LOAD_BALANCING ?
+                                  "\"load-balancing\"" : "\"hot-standby\"") << ","
+        "         \"heartbeat-delay\": 1000,"
+        "         \"max-response-delay\": 1000,"
+        "         \"max-ack-delay\": 10000,"
+        "         \"max-unacked-clients\": 10,"
+        "         \"peers\": ["
+        "             {"
+        "                 \"name\": \"server1\","
+        "                 \"url\": \"http://127.0.0.1:18123/\","
+        "                 \"role\": \"primary\","
+        "                 \"auto-failover\": true"
+        "             },"
+        "             {"
+        "                 \"name\": \"server2\","
+        "                 \"url\": \"http://127.0.0.1:18124/\","
+        "                 \"role\": " << (ha_mode == HAConfig::LOAD_BALANCING ?
+                                          "\"secondary\"" : "\"standby\"") << ","
+        "                 \"auto-failover\": true"
+        "             },"
+        "             {"
+        "                 \"name\": \"server3\","
+        "                 \"url\": \"http://127.0.0.1:18125/\","
+        "                 \"role\": \"backup\","
+        "                 \"auto-failover\": false"
+        "             }"
+        "         ]"
+        "     }"
+        "]";
+
+    return (Element::fromJSON(config_text.str()));
+}
+
+HAConfigPtr
+HATest::createValidConfiguration() const {
+    HAConfigPtr config_storage(new HAConfig());
+    HAConfigParser parser;
+
+    parser.parse(config_storage, createValidJsonConfiguration());
+    return (config_storage);
+}
+
+void
+HATest::checkAnswer(const isc::data::ConstElementPtr& answer,
+                    const int exp_status,
+                    const std::string& exp_txt) {
+    int rcode = 0;
+    isc::data::ConstElementPtr comment;
+    comment = isc::config::parseAnswer(rcode, answer);
+
+    EXPECT_EQ(exp_status, rcode)
+        << "Expected status code " << exp_status
+        << " but received " << rcode << ", comment: "
+        << (comment ? comment->str() : "(none)");
+
+    // Ok, parseAnswer interface is weird. If there are no arguments,
+    // it returns content of text. But if there is an argument,
+    // it returns the argument and it's not possible to retrieve
+    // "text" (i.e. comment).
+    if (comment->getType() != Element::string) {
+        comment = answer->get("text");
+    }
+
+    if (!exp_txt.empty()) {
+        EXPECT_EQ(exp_txt, comment->stringValue());
+    }
+}
+
+std::vector<uint8_t>
+HATest::randomKey(const size_t key_size) const {
+    std::vector<uint8_t> key(key_size);
+    util::fillRandom(key.begin(), key.end());
+    return (key);
+}
+
+
+Pkt4Ptr
+HATest::createMessage4(const uint8_t msg_type, const uint8_t hw_address_seed,
+                       const uint8_t client_id_seed, const uint16_t secs) const {
+    Pkt4Ptr message(new Pkt4(msg_type, 0x1234));
+
+    HWAddrPtr hw_address(new HWAddr(std::vector<uint8_t>(6, hw_address_seed),
+                                    HTYPE_ETHER));
+    message->setHWAddr(hw_address);
+    message->setSecs(secs);
+
+    if (client_id_seed > 0) {
+        OptionPtr opt_client_id(new Option(Option::V4, DHO_DHCP_CLIENT_IDENTIFIER,
+                                           std::vector<uint8_t>(6, client_id_seed)));
+        message->addOption(opt_client_id);
+    }
+
+    return (message);
+}
+
+Pkt4Ptr
+HATest::createQuery4(const std::string& hw_address_text) const {
+    Pkt4Ptr query4(new Pkt4(DHCPDISCOVER, 0x1234));
+    HWAddr hwaddr = HWAddr::fromText(hw_address_text);
+    query4->setHWAddr(HWAddrPtr(new HWAddr(hwaddr.hwaddr_, HTYPE_ETHER)));
+    return (query4);
+}
+
+Pkt4Ptr
+HATest::createQuery4(const std::vector<uint8_t>& hw_address,
+                     const std::vector<uint8_t>& client_id) const {
+    Pkt4Ptr query4(new Pkt4(DHCPDISCOVER, 0x1234));
+    query4->setHWAddr(HWAddrPtr(new HWAddr(hw_address, HTYPE_ETHER)));
+    if (!client_id.empty()) {
+        OptionPtr opt_client_id(new Option(Option::V4, DHO_DHCP_CLIENT_IDENTIFIER,
+                                           client_id));
+        query4->addOption(opt_client_id);
+    }
+    return (query4);
+}
+
+Pkt6Ptr
+HATest::createQuery6(const std::vector<uint8_t>& duid) const {
+    Pkt6Ptr query6(new Pkt6(DHCPV6_SOLICIT, 0x1234));
+    OptionPtr opt_duid(new Option(Option::V6, D6O_CLIENTID, duid));
+    query6->addOption(opt_duid);
+    return (query6);
+}
+
+Pkt6Ptr
+HATest::createMessage6(const uint8_t msg_type, const uint8_t duid_seed,
+                       const uint16_t elapsed_time) const {
+    Pkt6Ptr message(new Pkt6(msg_type, 0x1234));
+
+    OptionPtr opt_duid(new Option(Option::V6, D6O_CLIENTID, OptionBuffer(8, duid_seed)));
+    message->addOption(opt_duid);
+
+    OptionUint16Ptr opt_elapsed_time(new OptionUint16(Option::V6, D6O_ELAPSED_TIME,
+                                                      elapsed_time));
+    message->addOption(opt_elapsed_time);
+
+    return (message);
+}
+
+Pkt6Ptr
+HATest::createQuery6(const std::string& duid_text) const {
+    Pkt6Ptr query6(new Pkt6(DHCPV6_SOLICIT, 0x1234));
+    DUID duid = DUID::fromText(duid_text);
+    OptionPtr client_id(new Option(Option::V6, D6O_CLIENTID, duid.getDuid()));
+    query6->addOption(client_id);
+    return (query6);
+}
+
+} // end of namespace isc::ha::test
+} // end of namespace isc::ha
+} // end of namespace isc
diff --git a/src/hooks/dhcp/high_availability/tests/ha_test.h b/src/hooks/dhcp/high_availability/tests/ha_test.h
new file mode 100644 (file)
index 0000000..3738070
--- /dev/null
@@ -0,0 +1,237 @@
+// Copyright (C) 2017-2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <communication_state.h>
+#include <ha_config.h>
+#include <ha_config_parser.h>
+#include <asiolink/io_service.h>
+#include <cc/data.h>
+#include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt6.h>
+#include <dhcpsrv/network_state.h>
+#include <hooks/libinfo.h>
+#include <util/threads/sync.h>
+#include <util/threads/thread.h>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+#include <cstdint>
+#include <functional>
+#include <string>
+#include <vector>
+
+namespace isc {
+namespace ha {
+namespace test {
+
+/// @brief Derivation of the @c CommunicationState class which allows
+/// for modifications of poke time.
+///
+/// @tparam @c CommunicationState4 or @c CommunicationState6.
+template<typename StateType>
+class NakedCommunicationState : public StateType {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param io_service pointer to the IO service object.
+    explicit NakedCommunicationState(const asiolink::IOServicePtr& io_service,
+                                     const HAConfigPtr& config)
+        : StateType(io_service, config) {
+    }
+
+    /// @brief Modifies poke time by adding seconds to it.
+    ///
+    /// @param secs number of seconds to be added to the poke time. If
+    /// the value is negative it will set the poke time in the past
+    /// comparing to current value.
+    void modifyPokeTime(const long secs) {
+        StateType::poke_time_ += boost::posix_time::seconds(secs);
+    }
+
+    /// @brief Checks if the object was poked recently.
+    ///
+    /// @return true if the object was poked less than 5 seconds ago,
+    /// false otherwise.
+    bool isPoked() const {
+        return (StateType::getDurationInMillisecs() < 5000);
+    }
+
+    using StateType::config_;
+    using StateType::timer_;
+    using StateType::clock_skew_;
+    using StateType::last_clock_skew_warn_;
+};
+
+/// @brief Type of the NakedCommunicationState for DHCPv4.
+typedef NakedCommunicationState<CommunicationState4> NakedCommunicationState4;
+
+/// @brief Type of the pointer to the @c NakedCommunicationState4.
+typedef boost::shared_ptr<NakedCommunicationState4> NakedCommunicationState4Ptr;
+
+/// @brief Type of the NakedCommunicationState for DHCPv6.
+typedef NakedCommunicationState<CommunicationState6> NakedCommunicationState6;
+
+/// @brief Type of the pointer to the @c NakedCommunicationState6.
+typedef boost::shared_ptr<NakedCommunicationState6> NakedCommunicationState6Ptr;
+
+/// @brief General test fixture class for all HA unittests.
+///
+/// It provides basic functions to load and unload HA hooks library.
+/// All test classes should derive from this class.
+class HATest : public ::testing::Test {
+public:
+
+    /// @brief Constructor.
+    HATest();
+
+    /// @brief Destructor.
+    virtual ~HATest();
+
+    /// @brief Calls dhcp4_srv_configured callout to set IO service pointer.
+    void startHAService();
+
+    /// @brief Runs IO service for a specified amount of time.
+    ///
+    /// @param ms number of milliseconds for which the IO service should be
+    /// run.
+    void runIOService(long ms);
+
+    /// @brief Runs IO service until timeout occurs or until provided method
+    /// returns true.
+    ///
+    /// @param ms number of milliseconds for which the IO service should be
+    /// run.
+    /// @param stop_condition pointer to the function which returns true if
+    /// when the IO service should be stopped.
+    void runIOService(long ms, std::function<bool()> stop_condition);
+
+    /// @brief Runs IO service in a thread.
+    ///
+    /// @return Shared pointer to the thread.
+    boost::shared_ptr<util::thread::Thread>
+    runIOServiceInThread();
+
+    /// @brief Executes commands while running IO service in a thread.
+    ///
+    /// @param commands pointer to a function to be executed while IO service
+    /// is run in thread.
+    void testSynchronousCommands(std::function<void()> commands);
+
+protected:
+
+    /// @brief Signals that the IO service is running.
+    ///
+    /// @param running reference to the flag which is set to true when the
+    /// IO service starts running and executes this function.
+    /// @param mutex reference to the mutex used for synchronization.
+    /// @param condvar reference to condition variable used for synchronization.
+    void signalServiceRunning(bool& running, util::thread::Mutex& mutex,
+                              util::thread::CondVar& condvar);
+
+public:
+
+    /// @brief Handler for timeout during runIOService invocation.
+    ///
+    /// @param [out] stop_flag set to true when the handler is invoked.
+    void stopIOServiceHandler(bool& stop_flag);
+
+    /// @brief Return HA configuration with three servers in JSON format.
+    ///
+    /// @param ha_mode HA operation mode (default is load balancing).
+    /// @return Pointer to the unparsed configuration.
+    data::ConstElementPtr
+    createValidJsonConfiguration(const HAConfig::HAMode& ha_mode =
+                                 HAConfig::LOAD_BALANCING) const;
+
+    /// @brief Return HA configuration with three servers.
+    ///
+    /// @return Pointer to the parsed configuration.
+    HAConfigPtr createValidConfiguration() const;
+
+    /// @brief Checks the status code and message against expected values.
+    ///
+    /// @param answer Element set containing an integer response and string
+    /// comment.
+    /// @param exp_status Status code against which to compare the status.
+    /// @param exp_txt Expected text (not checked if empty)
+    void checkAnswer(const isc::data::ConstElementPtr& answer,
+                     const int exp_status,
+                     const std::string& exp_txt = "");
+
+    /// @brief Creates an identifier of arbitrary size with random values.
+    ///
+    /// This function is useful in generating random client identifiers and
+    /// HW addresses for load balancing tests.
+    ///
+    /// @param key_size Size of the generated random identifier.
+    std::vector<uint8_t> randomKey(const size_t key_size) const;
+
+    /// @brief Generates simple DHCPv4 message.
+    ///
+    /// @param msg_type DHCPv4 message type to be created.
+    /// @param hw_address_seed value from which HW address will be generated.
+    /// @param client_id_seed value from which client identifier will be
+    /// generated.
+    /// @param secs value to be stored in the "secs" field of the DHCPv4 message.
+    ///
+    /// @return Pointer to the created message.
+    dhcp::Pkt4Ptr createMessage4(const uint8_t msg_type,
+                                 const uint8_t hw_address_seed,
+                                 const uint8_t client_id_seed,
+                                 const uint16_t secs) const;
+
+    /// @brief Creates test DHCPv4 query instance.
+    ///
+    /// @param hw_address_text HW address to be included in the query. It is
+    /// used in load balancing.
+    ///
+    /// @return Pointer to the DHCPv4 query instance.
+    dhcp::Pkt4Ptr createQuery4(const std::string& hw_address_text) const;
+
+    /// @brief Creates test DHCPv4 query instance.
+    ///
+    /// @param hw_address HW address to be included in the query. It is used
+    /// in load balancing.
+    /// @param client_id optional client identifier.
+    dhcp::Pkt4Ptr createQuery4(const std::vector<uint8_t>& hw_address,
+                               const std::vector<uint8_t>& client_id =
+                               std::vector<uint8_t>()) const;
+
+    /// @brief Creates test DHCPv6 query instance.
+    ///
+    /// @param duid DUI to be included in the query. It is used in load balancing.
+    dhcp::Pkt6Ptr createQuery6(const std::vector<uint8_t>& duid) const;
+
+    /// @brief Generates simple DHCPv6 message.
+    ///
+    /// @param msg_type DHCPv6 message type to be created.
+    /// @param duid value from which DUID will be generated.
+    /// @param elapsed_time value of the Elapsed Time option.
+    ///
+    /// @return Pointer to the created message.
+    dhcp::Pkt6Ptr createMessage6(const uint8_t msg_type,
+                                 const uint8_t duid_seed,
+                                 const uint16_t elapsed_time) const;
+
+    /// @brief Creates test DHCPv6 query instance.
+    ///
+    /// @param duid_text DUID to be included in the query. It is used in load
+    /// balancing.
+    ///
+    /// @return Pointer to the DHCPv6 query instance.
+    dhcp::Pkt6Ptr createQuery6(const std::string& duid_text) const;
+
+    /// @brief Pointer to the IO service used in the tests.
+    asiolink::IOServicePtr io_service_;
+
+    /// @brief Object holding a state of the DHCP service.
+    dhcp::NetworkStatePtr network_state_;
+};
+
+} // end of namespace isc::ha::test
+} // end of namespace isc::ha
+} // end of namespace isc
diff --git a/src/hooks/dhcp/high_availability/tests/query_filter_unittest.cc b/src/hooks/dhcp/high_availability/tests/query_filter_unittest.cc
new file mode 100644 (file)
index 0000000..d0fb03c
--- /dev/null
@@ -0,0 +1,640 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <ha_test.h>
+#include <ha_config.h>
+#include <ha_config_parser.h>
+#include <query_filter.h>
+#include <cc/data.h>
+#include <exceptions/exceptions.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/hwaddr.h>
+#include <cstdint>
+#include <string>
+
+using namespace isc;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::ha;
+using namespace isc::ha::test;
+
+namespace  {
+
+/// @brief Test fixture class for @c QueryFilter class.
+using QueryFilterTest = HATest;
+
+// This test verifies the case when load balancing is enabled and
+// this server is primary.
+TEST_F(QueryFilterTest, loadBalancingThisPrimary) {
+    HAConfigPtr config = createValidConfiguration();
+
+    QueryFilter filter(config);
+
+    // By default the server1 should serve its own scope only. The
+    // server2 should serve its scope.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Count number of in scope packets.
+    unsigned in_scope = 0;
+    // Set the test size - 65535 queries.
+    const unsigned queries_num = 65535;
+    std::string scope_class;
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random HW address.
+        Pkt4Ptr query4 = createQuery4(randomKey(HWAddr::ETHERNET_HWADDR_LEN));
+        // If the query is in scope, increase the counter of packets in scope.
+        if (filter.inScope(query4, scope_class)) {
+            ASSERT_EQ("HA_server1", scope_class);
+            ASSERT_NE(scope_class, "HA_server2");
+            ++in_scope;
+        }
+    }
+
+    // We should have roughly 50/50 split of in scope and out of scope queries.
+    // However, we don't know exactly how many. To be safe we simply assume that
+    // we got more than 25% of in scope and more than 25% out of scope queries.
+    EXPECT_GT(in_scope, static_cast<unsigned>(queries_num / 4));
+    EXPECT_GT(queries_num - in_scope, static_cast<unsigned>(queries_num / 4));
+
+    // Simulate failover scenario.
+    filter.serveFailoverScopes();
+
+    // In the failover case, the server1 should also take responsibility for
+    // the server2's queries.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_TRUE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Repeat the test, but this time all should be in scope.
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random HW address.
+        Pkt4Ptr query4 = createQuery4(randomKey(HWAddr::ETHERNET_HWADDR_LEN));
+        // Every single query mist be in scope.
+        ASSERT_TRUE(filter.inScope(query4, scope_class));
+    }
+
+    // However, the one that lacks HW address and client id should be out of
+    // scope.
+    Pkt4Ptr query4(new Pkt4(DHCPDISCOVER, 1234));
+    EXPECT_FALSE(filter.inScope(query4, scope_class));
+}
+
+// This test verifies that client identifier is used for load balancing.
+TEST_F(QueryFilterTest, loadBalancingClientIdThisPrimary) {
+    HAConfigPtr config = createValidConfiguration();
+
+    QueryFilter filter(config);
+
+    // By default the server1 should serve its own scope only. The
+    // server2 should serve its scope.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Fixed HW address used in tests.
+    std::vector<uint8_t> hw_address(HWAddr::ETHERNET_HWADDR_LEN);
+
+    // Count number of in scope packets.
+    unsigned in_scope = 0;
+    // Set the test size - 65535 queries.
+    const unsigned queries_num = 65535;
+    std::string scope_class;
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random client identifier.
+        Pkt4Ptr query4 = createQuery4(hw_address, randomKey(8));
+        // If the query is in scope, increase the counter of packets in scope.
+        if (filter.inScope(query4, scope_class)) {
+            ASSERT_EQ("HA_server1", scope_class);
+            ASSERT_NE(scope_class, "HA_server2");
+            ++in_scope;
+        }
+    }
+
+    // We should have roughly 50/50 split of in scope and out of scope queries.
+    // However, we don't know exactly how many. To be safe we simply assume that
+    // we got more than 25% of in scope and more than 25% out of scope queries.
+    EXPECT_GT(in_scope, static_cast<unsigned>(queries_num / 4));
+    EXPECT_GT(queries_num - in_scope, static_cast<unsigned>(queries_num / 4));
+
+    // Simulate failover scenario.
+    filter.serveFailoverScopes();
+
+    // In the failover case, the server1 should also take responsibility for
+    // the server2's queries.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_TRUE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Repeat the test, but this time all should be in scope.
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random client identifier.
+        Pkt4Ptr query4 = createQuery4(hw_address, randomKey(8));
+        // Every single query mist be in scope.
+        ASSERT_TRUE(filter.inScope(query4, scope_class));
+    }
+}
+
+// This test verifies the case when load balancing is enabled and
+// this server is secondary.
+TEST_F(QueryFilterTest, loadBalancingThisSecondary) {
+    HAConfigPtr config = createValidConfiguration();
+
+    // We're now a secondary server.
+    config->setThisServerName("server2");
+
+    QueryFilter filter(config);
+
+    // By default the server2 should serve its own scope only. The
+    // server1 should serve its scope.
+    EXPECT_FALSE(filter.amServingScope("server1"));
+    EXPECT_TRUE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Count number of in scope packets.
+    unsigned in_scope = 0;
+    // Set the test size - 65535 queries.
+    const unsigned queries_num = 65535;
+    std::string scope_class;
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random HW address.
+        Pkt4Ptr query4 = createQuery4(randomKey(HWAddr::ETHERNET_HWADDR_LEN));
+        // If the query is in scope, increase the counter of packets in scope.
+        if (filter.inScope(query4, scope_class)) {
+            ASSERT_EQ("HA_server2", scope_class);
+            ASSERT_NE(scope_class, "HA_server1");
+            ++in_scope;
+        }
+    }
+
+    // We should have roughly 50/50 split of in scope and out of scope queries.
+    // However, we don't know exactly how many. To be safe we simply assume that
+    // we got more than 25% of in scope and more than 25% out of scope queries.
+    EXPECT_GT(in_scope, static_cast<unsigned>(queries_num / 4));
+    EXPECT_GT(queries_num - in_scope, static_cast<unsigned>(queries_num / 4));
+
+    // Simulate failover scenario.
+    filter.serveFailoverScopes();
+
+    // In this scenario, the server1 died, so the server2 should now serve
+    // both scopes.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_TRUE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Repeat the test, but this time all should be in scope.
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random HW address.
+        Pkt4Ptr query4 = createQuery4(randomKey(HWAddr::ETHERNET_HWADDR_LEN));
+        // Every single query must be in scope.
+        ASSERT_TRUE(filter.inScope(query4, scope_class));
+    }
+}
+
+// This test verifies the case when load balancing is enabled and
+// this server is backup.
+/// @todo Expand these tests once we implement the actual load balancing to
+/// verify which packets are in scope.
+TEST_F(QueryFilterTest, loadBalancingThisBackup) {
+    HAConfigPtr config = createValidConfiguration();
+
+    config->setThisServerName("server3");
+
+    QueryFilter filter(config);
+
+    // The backup server doesn't handle any DHCP traffic by default.
+    EXPECT_FALSE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Set the test size - 65535 queries.
+    const unsigned queries_num = 65535;
+    std::string scope_class;
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random HW address.
+        Pkt4Ptr query4 = createQuery4(randomKey(HWAddr::ETHERNET_HWADDR_LEN));
+        // None of the packets should be handlded by the backup server.
+        ASSERT_FALSE(filter.inScope(query4, scope_class));
+    }
+
+    // Simulate failover. Although, backup server never starts handling
+    // other server's traffic automatically, it can be manually instructed
+    // to do so. This simulates such scenario.
+    filter.serveFailoverScopes();
+
+    // The backup server now handles traffic of server 1 and server 2.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_TRUE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Repeat the test, but this time all should be in scope.
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random HW address.
+        Pkt4Ptr query4 = createQuery4(randomKey(HWAddr::ETHERNET_HWADDR_LEN));
+        // Every single query must be in scope.
+        ASSERT_TRUE(filter.inScope(query4, scope_class));
+    }
+}
+
+// This test verifies the case when hot-standby is enabled and this
+// server is primary.
+TEST_F(QueryFilterTest, hotStandbyThisPrimary) {
+    HAConfigPtr config = createValidConfiguration();
+
+    config->setHAMode("hot-standby");
+    config->getPeerConfig("server2")->setRole("standby");
+
+    QueryFilter filter(config);
+
+    Pkt4Ptr query4 = createQuery4("11:22:33:44:55:66");
+
+    // By default, only the primary server is active.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    std::string scope_class;
+
+    // It should process its queries.
+    EXPECT_TRUE(filter.inScope(query4, scope_class));
+
+    // Simulate failover scenario, in which the active server detects a
+    // failure of the standby server. This doesn't change anything in how
+    // the traffic is distributed.
+    filter.serveFailoverScopes();
+
+    // The server1 continues to process its own traffic.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    EXPECT_TRUE(filter.inScope(query4, scope_class));
+    EXPECT_EQ("HA_server1", scope_class);
+    EXPECT_NE(scope_class, "HA_server2");
+}
+
+// This test verifies the case when hot-standby is enabled and this
+// server is standby.
+TEST_F(QueryFilterTest, hotStandbyThisSecondary) {
+    HAConfigPtr config = createValidConfiguration();
+
+    config->setHAMode("hot-standby");
+    config->getPeerConfig("server2")->setRole("standby");
+    config->setThisServerName("server2");
+
+    QueryFilter filter(config);
+
+    Pkt4Ptr query4 = createQuery4("11:22:33:44:55:66");
+
+    // The server2 doesn't process any queries by default. The whole
+    // traffic is processed by the server1.
+    EXPECT_FALSE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    std::string scope_class;
+
+    EXPECT_FALSE(filter.inScope(query4, scope_class));
+    EXPECT_EQ("HA_server1", scope_class);
+    EXPECT_NE(scope_class, "HA_server2");
+
+    // Simulate failover case whereby the standby server detects a
+    // failure of the active server.
+    filter.serveFailoverScopes();
+
+    // The server2 now handles the traffic normally handled by the
+    // server1.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    EXPECT_TRUE(filter.inScope(query4, scope_class));
+    EXPECT_EQ("HA_server1", scope_class);
+    EXPECT_NE(scope_class, "HA_server2");
+}
+
+// This test verifies the case when hot-standby is enabled and this
+// server is backup.
+TEST_F(QueryFilterTest, hotStandbyThisBackup) {
+    HAConfigPtr config = createValidConfiguration();
+
+    config->setHAMode("hot-standby");
+    config->getPeerConfig("server2")->setRole("standby");
+    config->setThisServerName("server3");
+
+    QueryFilter filter(config);
+
+    Pkt4Ptr query4 = createQuery4("11:22:33:44:55:66");
+
+    // By default the backup server doesn't process any traffic.
+    EXPECT_FALSE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    std::string scope_class;
+
+    EXPECT_FALSE(filter.inScope(query4, scope_class));
+
+    // Simulate failover. Although, backup server never starts handling
+    // other server's traffic automatically, it can be manually instructed
+    // to do so. This simulates such scenario.
+    filter.serveFailoverScopes();
+
+    // The backup server now handles the entire traffic, i.e. the traffic
+    // that the primary server handles.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    EXPECT_TRUE(filter.inScope(query4, scope_class));
+}
+
+// This test verifies the case when load balancing is enabled and
+// this DHCPv6 server is primary.
+TEST_F(QueryFilterTest, loadBalancingThisPrimary6) {
+    HAConfigPtr config = createValidConfiguration();
+
+    QueryFilter filter(config);
+
+    // By default the server1 should serve its own scope only. The
+    // server2 should serve its scope.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Count number of in scope packets.
+    unsigned in_scope = 0;
+    // Set the test size - 65535 queries.
+    const unsigned queries_num = 65535;
+    std::string scope_class;
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random DUID.
+        Pkt6Ptr query6 = createQuery6(randomKey(10));
+        // If the query is in scope, increase the counter of packets in scope.
+        if (filter.inScope(query6, scope_class)) {
+            ASSERT_EQ("HA_server1", scope_class);
+            ASSERT_NE(scope_class, "HA_server2");
+            ++in_scope;
+        }
+    }
+
+    // We should have roughly 50/50 split of in scope and out of scope queries.
+    // However, we don't know exactly how many. To be safe we simply assume that
+    // we got more than 25% of in scope and more than 25% out of scope queries.
+    EXPECT_GT(in_scope, static_cast<unsigned>(queries_num / 4));
+    EXPECT_GT(queries_num - in_scope, static_cast<unsigned>(queries_num / 4));
+
+    // Simulate failover scenario.
+    filter.serveFailoverScopes();
+
+    // In the failover case, the server1 should also take responsibility for
+    // the server2's queries.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_TRUE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Repeat the test, but this time all should be in scope.
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random HW address.
+        Pkt6Ptr query6 = createQuery6(randomKey(10));
+        // Every single query mist be in scope.
+        ASSERT_TRUE(filter.inScope(query6, scope_class));
+    }
+
+    // However, the one that lacks DUID should be out of scope.
+    Pkt6Ptr query6(new Pkt6(DHCPV6_SOLICIT, 1234));
+    EXPECT_FALSE(filter.inScope(query6, scope_class));
+}
+
+// This test verifies the case when load balancing is enabled and
+// this server is secondary.
+TEST_F(QueryFilterTest, loadBalancingThisSecondary6) {
+    HAConfigPtr config = createValidConfiguration();
+
+    // We're now a secondary server.
+    config->setThisServerName("server2");
+
+    QueryFilter filter(config);
+
+    // By default the server2 should serve its own scope only. The
+    // server1 should serve its scope.
+    EXPECT_FALSE(filter.amServingScope("server1"));
+    EXPECT_TRUE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Count number of in scope packets.
+    unsigned in_scope = 0;
+    // Set the test size - 65535 queries.
+    const unsigned queries_num = 65535;
+    std::string scope_class;
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random HW address.
+        Pkt6Ptr query6 = createQuery6(randomKey(10));
+        // If the query is in scope, increase the counter of packets in scope.
+        if (filter.inScope(query6, scope_class)) {
+            ASSERT_EQ("HA_server2", scope_class);
+            ASSERT_NE(scope_class, "HA_server1");
+            ++in_scope;
+        }
+    }
+
+    // We should have roughly 50/50 split of in scope and out of scope queries.
+    // However, we don't know exactly how many. To be safe we simply assume that
+    // we got more than 25% of in scope and more than 25% out of scope queries.
+    EXPECT_GT(in_scope, static_cast<unsigned>(queries_num / 4));
+    EXPECT_GT(queries_num - in_scope, static_cast<unsigned>(queries_num / 4));
+
+    // Simulate failover scenario.
+    filter.serveFailoverScopes();
+
+    // In this scenario, the server1 died, so the server2 should now serve
+    // both scopes.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_TRUE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Repeat the test, but this time all should be in scope.
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random HW address.
+        Pkt6Ptr query6 = createQuery6(randomKey(HWAddr::ETHERNET_HWADDR_LEN));
+        // Every single query must be in scope.
+        ASSERT_TRUE(filter.inScope(query6, scope_class));
+    }
+}
+
+// This test verifies the case when load balancing is enabled and
+// this server is backup.
+/// @todo Expand these tests once we implement the actual load balancing to
+/// verify which packets are in scope.
+TEST_F(QueryFilterTest, loadBalancingThisBackup6) {
+    HAConfigPtr config = createValidConfiguration();
+
+    config->setThisServerName("server3");
+
+    QueryFilter filter(config);
+
+    // The backup server doesn't handle any DHCP traffic by default.
+    EXPECT_FALSE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Set the test size - 65535 queries.
+    const unsigned queries_num = 65535;
+    std::string scope_class;
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random HW address.
+        Pkt6Ptr query6 = createQuery6(randomKey(10));
+        // None of the packets should be handlded by the backup server.
+        ASSERT_FALSE(filter.inScope(query6, scope_class));
+    }
+
+    // Simulate failover. Although, backup server never starts handling
+    // other server's traffic automatically, it can be manually instructed
+    // to do so. This simulates such scenario.
+    filter.serveFailoverScopes();
+
+    // The backup server now handles traffic of server 1 and server 2.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_TRUE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Repeat the test, but this time all should be in scope.
+    for (unsigned i = 0; i < queries_num; ++i) {
+        // Create query with random HW address.
+        Pkt6Ptr query6 = createQuery6(randomKey(10));
+        // Every single query must be in scope.
+        ASSERT_TRUE(filter.inScope(query6, scope_class));
+    }
+}
+
+// This test verifies the case when hot-standby is enabled and this
+// server is primary.
+TEST_F(QueryFilterTest, hotStandbyThisPrimary6) {
+    HAConfigPtr config = createValidConfiguration();
+
+    config->setHAMode("hot-standby");
+    config->getPeerConfig("server2")->setRole("standby");
+
+    QueryFilter filter(config);
+
+    Pkt6Ptr query6 = createQuery6("01:02:11:22:33:44:55:66");
+
+    // By default, only the primary server is active.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    std::string scope_class;
+
+    // It should process its queries.
+    EXPECT_TRUE(filter.inScope(query6, scope_class));
+
+    // Simulate failover scenario, in which the active server detects a
+    // failure of the standby server. This doesn't change anything in how
+    // the traffic is distributed.
+    filter.serveFailoverScopes();
+
+    // The server1 continues to process its own traffic.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    EXPECT_TRUE(filter.inScope(query6, scope_class));
+    EXPECT_EQ("HA_server1", scope_class);
+    EXPECT_NE(scope_class, "HA_server2");
+}
+
+// This test verifies the case when hot-standby is enabled and this
+// server is standby.
+TEST_F(QueryFilterTest, hotStandbyThisSecondary6) {
+    HAConfigPtr config = createValidConfiguration();
+
+    config->setHAMode("hot-standby");
+    config->getPeerConfig("server2")->setRole("standby");
+    config->setThisServerName("server2");
+
+    QueryFilter filter(config);
+
+    Pkt6Ptr query6 = createQuery6("01:02:11:22:33:44:55:66");
+
+    // The server2 doesn't process any queries by default. The whole
+    // traffic is processed by the server1.
+    EXPECT_FALSE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    std::string scope_class;
+
+    EXPECT_FALSE(filter.inScope(query6, scope_class));
+    EXPECT_EQ("HA_server1", scope_class);
+    EXPECT_NE(scope_class, "HA_server2");
+
+    // Simulate failover case whereby the standby server detects a
+    // failure of the active server.
+    filter.serveFailoverScopes();
+
+    // The server2 now handles the traffic normally handled by the
+    // server1.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    EXPECT_TRUE(filter.inScope(query6, scope_class));
+    EXPECT_EQ("HA_server1", scope_class);
+    EXPECT_NE(scope_class, "HA_server2");
+}
+
+// This test verifies that it is possible to explicitly enable and
+// disable certain scopes.
+TEST_F(QueryFilterTest, explicitlyServeScopes) {
+    HAConfigPtr config = createValidConfiguration();
+
+    QueryFilter filter(config);
+
+    // Initially, the scopes should be set according to the load
+    // balancing configuration.
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Enable "server2" scope.
+    ASSERT_NO_THROW(filter.serveScope("server2"));
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_TRUE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Enable only "server2" scope.
+    ASSERT_NO_THROW(filter.serveScopeOnly("server2"));
+    EXPECT_FALSE(filter.amServingScope("server1"));
+    EXPECT_TRUE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Explicitly enable selected scopes.
+    ASSERT_NO_THROW(filter.serveScopes({ "server1", "server3" }));
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_TRUE(filter.amServingScope("server3"));
+
+    // Revert to defaults.
+    ASSERT_NO_THROW(filter.serveDefaultScopes());
+    EXPECT_TRUE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Disable all scopes.
+    ASSERT_NO_THROW(filter.serveNoScopes());
+    EXPECT_FALSE(filter.amServingScope("server1"));
+    EXPECT_FALSE(filter.amServingScope("server2"));
+    EXPECT_FALSE(filter.amServingScope("server3"));
+
+    // Test negative cases.
+    EXPECT_THROW(filter.serveScope("unsupported"), BadValue);
+    EXPECT_THROW(filter.serveScopeOnly("unsupported"), BadValue);
+    EXPECT_THROW(filter.serveScopes({ "server1", "unsupported" }), BadValue);
+}
+
+}
diff --git a/src/hooks/dhcp/high_availability/tests/run_unittests.cc b/src/hooks/dhcp/high_availability/tests/run_unittests.cc
new file mode 100644 (file)
index 0000000..6f007c7
--- /dev/null
@@ -0,0 +1,18 @@
+// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#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/hooks/dhcp/high_availability/version.cc b/src/hooks/dhcp/high_availability/version.cc
new file mode 100644 (file)
index 0000000..b273963
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the End User License
+// Agreement. See COPYING file in the premium/ directory.
+
+#include <config.h>
+#include <hooks/hooks.h>
+
+extern "C" {
+
+/// @brief returns Kea hooks version.
+int version() {
+    return (KEA_HOOKS_VERSION);
+}
+
+}