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)
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
-SUBDIRS = user_chk lease_cmds stat_cmds
+SUBDIRS = high_availability lease_cmds stat_cmds user_chk
--- /dev/null
+/ha_messages.cc
+/ha_messages.h
+/s-messages
+/html
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+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
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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.
+
+*/
--- /dev/null
+// 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"
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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
+
--- /dev/null
+// 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
--- /dev/null
+# 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.
--- /dev/null
+// 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
+
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+/ha_unittests
+/ha_unittests.log
+/ha_unittests.trs
+/test-suite.log
--- /dev/null
+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)
--- /dev/null
+// 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"));
+}
+
+
+}
--- /dev/null
+// 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());
+}
+
+}
--- /dev/null
+// 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
--- /dev/null
+// 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");
+ }
+
+}
+
+
+}
--- /dev/null
+// 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));
+}
+
+}
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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);
+}
+
+}
--- /dev/null
+// 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);
+}
--- /dev/null
+// 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);
+}
+
+}