Browse Source

[2981] Merge branch 'master' into trac2981

Conflicts:
	src/bin/dhcp6/config_parser.cc
	src/bin/dhcp6/dhcp6.spec
	src/bin/dhcp6/tests/Makefile.am
	src/bin/dhcp6/tests/config_parser_unittest.cc
	src/lib/dhcpsrv/dhcp_parsers.h
	src/lib/dhcpsrv/tests/Makefile.am
	src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc
Stephen Morris 11 years ago
parent
commit
3c285e7ef1
76 changed files with 5253 additions and 714 deletions
  1. 32 0
      ChangeLog
  2. 4 0
      doc/devel/mainpage.dox
  3. 109 2
      doc/guide/bind10-guide.xml
  4. 4 0
      src/bin/auth/auth_messages.mes
  5. 1 0
      src/bin/auth/auth_srv.cc
  6. 177 23
      src/bin/auth/datasrc_clients_mgr.h
  7. 110 27
      src/bin/auth/tests/datasrc_clients_builder_unittest.cc
  8. 51 9
      src/bin/auth/tests/datasrc_clients_mgr_unittest.cc
  9. 11 4
      src/bin/auth/tests/test_datasrc_clients_mgr.cc
  10. 25 5
      src/bin/auth/tests/test_datasrc_clients_mgr.h
  11. 2 0
      src/bin/cmdctl/tests/b10-certgen_test.py
  12. 15 5
      src/bin/cmdctl/tests/cmdctl_test.py
  13. 1 0
      src/bin/d2/Makefile.am
  14. 1 0
      src/bin/d2/tests/Makefile.am
  15. 8 6
      src/bin/d2/tests/d_test_stubs.h
  16. 1 0
      src/bin/dhcp4/Makefile.am
  17. 52 42
      src/bin/dhcp4/config_parser.cc
  18. 2 2
      src/bin/dhcp4/config_parser.h
  19. 29 5
      src/bin/dhcp4/ctrl_dhcp4_srv.cc
  20. 2 1
      src/bin/dhcp4/ctrl_dhcp4_srv.h
  21. 4 0
      src/bin/dhcp4/dhcp4.dox
  22. 3 3
      src/bin/dhcp4/dhcp4.spec
  23. 138 0
      src/bin/dhcp4/dhcp4_hooks.dox
  24. 3 0
      src/bin/dhcp4/dhcp4_log.h
  25. 35 1
      src/bin/dhcp4/dhcp4_messages.mes
  26. 230 8
      src/bin/dhcp4/dhcp4_srv.cc
  27. 68 1
      src/bin/dhcp4/dhcp4_srv.h
  28. 1 0
      src/bin/dhcp4/tests/Makefile.am
  29. 82 17
      src/bin/dhcp4/tests/config_parser_unittest.cc
  30. 818 6
      src/bin/dhcp4/tests/dhcp4_srv_unittest.cc
  31. 1 0
      src/bin/dhcp6/Makefile.am
  32. 51 42
      src/bin/dhcp6/config_parser.cc
  33. 3 3
      src/bin/dhcp6/config_parser.h
  34. 28 3
      src/bin/dhcp6/ctrl_dhcp6_srv.cc
  35. 4 0
      src/bin/dhcp6/dhcp6.dox
  36. 3 3
      src/bin/dhcp6/dhcp6.spec
  37. 139 0
      src/bin/dhcp6/dhcp6_hooks.dox
  38. 3 0
      src/bin/dhcp6/dhcp6_log.h
  39. 40 4
      src/bin/dhcp6/dhcp6_messages.mes
  40. 204 10
      src/bin/dhcp6/dhcp6_srv.cc
  41. 57 1
      src/bin/dhcp6/dhcp6_srv.h
  42. 1 1
      src/bin/dhcp6/tests/Makefile.am
  43. 99 20
      src/bin/dhcp6/tests/config_parser_unittest.cc
  44. 761 2
      src/bin/dhcp6/tests/dhcp6_srv_unittest.cc
  45. 17 2
      src/bin/memmgr/tests/memmgr_test.py
  46. 13 0
      src/bin/usermgr/tests/b10-cmdctl-usermgr_test.py
  47. 6 3
      src/lib/dhcp/iface_mgr.cc
  48. 8 0
      src/lib/dhcp/iface_mgr.h
  49. 2 0
      src/lib/dhcpsrv/Makefile.am
  50. 191 10
      src/lib/dhcpsrv/alloc_engine.cc
  51. 28 2
      src/lib/dhcpsrv/alloc_engine.h
  52. 54 1
      src/lib/dhcpsrv/cfgmgr.cc
  53. 92 0
      src/lib/dhcpsrv/cfgmgr.h
  54. 130 76
      src/lib/dhcpsrv/dhcp_parsers.cc
  55. 85 71
      src/lib/dhcpsrv/dhcp_parsers.h
  56. 3 0
      src/lib/dhcpsrv/dhcpsrv_log.h
  57. 26 0
      src/lib/dhcpsrv/dhcpsrv_messages.mes
  58. 471 27
      src/lib/dhcpsrv/tests/alloc_engine_unittest.cc
  59. 45 0
      src/lib/dhcpsrv/tests/cfgmgr_unittest.cc
  60. 71 39
      src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc
  61. 13 12
      src/lib/hooks/callout_manager.cc
  62. 2 29
      src/lib/hooks/callout_manager.h
  63. 29 2
      src/lib/hooks/hooks_manager.cc
  64. 40 0
      src/lib/hooks/hooks_manager.h
  65. 48 53
      src/lib/hooks/hooks_messages.mes
  66. 88 27
      src/lib/hooks/library_manager.cc
  67. 50 9
      src/lib/hooks/library_manager.h
  68. 28 13
      src/lib/hooks/library_manager_collection.cc
  69. 41 4
      src/lib/hooks/library_manager_collection.h
  70. 21 6
      src/lib/hooks/server_hooks.cc
  71. 13 3
      src/lib/hooks/server_hooks.h
  72. 0 4
      src/lib/hooks/tests/callout_manager_unittest.cc
  73. 97 27
      src/lib/hooks/tests/hooks_manager_unittest.cc
  74. 105 38
      src/lib/hooks/tests/library_manager_collection_unittest.cc
  75. 14 0
      src/lib/hooks/tests/library_manager_unittest.cc
  76. 9 0
      src/lib/util/tests/memory_segment_mapped_unittest.cc

+ 32 - 0
ChangeLog

@@ -1,3 +1,35 @@
+646.	[func]		stephen
+	Extended the hooks framework to add a "validate libraries" function.
+	This will be used to check libraries specified during BIND 10
+	configuration.
+	(Trac #3054, git 0f845ed94f462dee85b67f056656b2a197878b04)
+
+645.	[func]		tomek
+	Added initial set of hooks (pk4_receive, subnet4_select,
+	lease4_select, pkt4_send) to the DHCPv6 server.
+	(Trac #2994, git be65cfba939a6a7abd3c93931ce35c33d3e8247b)
+
+644.	[func]		marcin
+	b10-dhcp4, b10-dhcp6: Implemented selection of the interfaces
+	that server listens on, using Configuration Manager. It is
+	possible to specify interface names explicitly or use asterisk
+	to specify that server should listen on all available interfaces.
+	Sockets are reopened according to the new configuration as
+	soon as it is committed.
+	(Trac #1555, git f48a3bff3fbbd15584d788a264d5966154394f04)
+
+643.	[bug]		muks
+	When running some unittests as root that depended on insufficient
+	file permissions, the tests used to fail because the root user
+	could still access such files. Such tests are now skipped when
+	they are run as the root user.
+	(Trac #3056, git 92ebabdbcf6168666b03d7f7fbb31f899be39322)
+
+642.	[func]		tomek
+	Added initial set of hooks (pk6_receive, subnet6_select,
+	lease6_select, pkt6_send) to the DHCPv6 server.
+	(Trac #2995, git d6de376f97313ba40fef989e4a437d184fdf70cc)
+
 641.	[func]		stephen
 	Added the hooks framework. This allows shared libraries of
 	user-written functions to be loaded at run-time and the

+ 4 - 0
doc/devel/mainpage.dox

@@ -37,6 +37,8 @@
  * <a href="http://bind10.isc.org/">BIND10 webpage (http://bind10.isc.org)</a>
  * @section hooksFramework Hooks Framework
  * - @subpage hooksComponentDeveloperGuide
+ * - @subpage dhcpv4Hooks
+ * - @subpage dhcpv6Hooks
  *
  * @section dnsMaintenanceGuide DNS Maintenance Guide
  * - Authoritative DNS (todo)
@@ -48,10 +50,12 @@
  *   - @subpage dhcpv4Session
  *   - @subpage dhcpv4ConfigParser
  *   - @subpage dhcpv4ConfigInherit
+ *   - @subpage dhcpv4Other
  * - @subpage dhcp6
  *   - @subpage dhcpv6Session
  *   - @subpage dhcpv6ConfigParser
  *   - @subpage dhcpv6ConfigInherit
+ *   - @subpage dhcpv6Other
  * - @subpage libdhcp
  *   - @subpage libdhcpIntro
  *   - @subpage libdhcpRelay

+ 109 - 2
doc/guide/bind10-guide.xml

@@ -3599,7 +3599,7 @@ $</screen>
         will be available. It will look similar to this:
 <screen>
 &gt; <userinput>config show Dhcp4</userinput>
-Dhcp4/interface/	list	(default)
+Dhcp4/interfaces/	list	(default)
 Dhcp4/renew-timer	1000	integer	(default)
 Dhcp4/rebind-timer	2000	integer	(default)
 Dhcp4/valid-lifetime	4000	integer	(default)
@@ -3686,6 +3686,60 @@ Dhcp4/subnet4	[]	list	(default)
       </note>
       </section>
 
+      <section id="dhcp4-interface-selection">
+      <title>Interface selection</title>
+      <para>
+        When DHCPv4 server starts up, by default it will listen to the DHCP
+        traffic and respond to it on all interfaces detected during startup.
+        However, in many cases it is desired to configure the server to listen and
+        respond on selected interfaces only. The sample commands in this section
+        show how to make interface selection using bindctl.
+      </para>
+      <para>
+        The default configuration can be presented with the following command:
+        <screen>
+&gt; <userinput>config show Dhcp4/interfaces</userinput>
+<userinput>Dhcp4/interfaces[0] "*" string</userinput></screen>
+        An asterisk sign plays a role of the wildcard and means "listen on all interfaces".
+      </para>
+      <para>
+        In order to override the default configuration, the existing entry can be replaced
+        with the actual interface name:
+        <screen>
+&gt; <userinput>config set Dhcp4/interfaces[0] eth1</userinput>
+&gt; <userinput>config commit</userinput></screen>
+        Other interface names can be added on one-by-one basis:
+        <screen>
+&gt; <userinput>config add Dhcp4/interfaces eth2</userinput>
+&gt; <userinput>config commit</userinput></screen>
+        Configuration will now contain two interfaces which can be presented as follows:
+        <screen>
+&gt; <userinput>config show Dhcp4/interfaces</userinput>
+<userinput>Dhcp4/interfaces[0]	"eth1"	string</userinput>
+<userinput>Dhcp4/interfaces[1]	"eth2"	string</userinput></screen>
+        When configuration gets committed, the server will start to listen on
+        eth1 and eth2 interfaces only.
+      </para>
+      <para>
+        It is possible to use wildcard interface name (asterisk) concurrently with explicit
+        interface names:
+        <screen>
+&gt; <userinput>config add Dhcp4/interfaces *</userinput>
+&gt; <userinput>config commit</userinput></screen>
+        This will result in the following configuration:
+        <screen>
+&gt; <userinput>config show Dhcp4/interfaces</userinput>
+<userinput>Dhcp4/interfaces[0]	"eth1"	string</userinput>
+<userinput>Dhcp4/interfaces[1]	"eth2"	string</userinput>
+<userinput>Dhcp4/interfaces[2]	"*"	string</userinput></screen>
+        The presence of the wildcard name implies that server will listen on all interfaces.
+        In order to fall back to the previous configuration when server listens on eth1 and eth2:
+        <screen>
+&gt; <userinput>config remove Dhcp4/interfaces[2]</userinput>
+&gt; <userinput>config commit</userinput></screen>
+      </para>
+      </section>
+
       <section id="dhcp4-address-config">
       <title>Configuration of Address Pools</title>
       <para>
@@ -4366,7 +4420,7 @@ Dhcp4/renew-timer	1000	integer	(default)
         will be available. It will look similar to this:
 <screen>
 &gt; <userinput>config show Dhcp6</userinput>
-Dhcp6/interface/	list	(default)
+Dhcp6/interfaces/	list	(default)
 Dhcp6/renew-timer	1000	integer	(default)
 Dhcp6/rebind-timer	2000	integer	(default)
 Dhcp6/preferred-lifetime	3000	integer	(default)
@@ -4459,6 +4513,59 @@ Dhcp6/subnet6/	list
       </note>
       </section>
 
+      <section id="dhcp6-interface-selection">
+      <title>Interface selection</title>
+      <para>
+        When DHCPv6 server starts up, by default it will listen to the DHCP
+        traffic and respond to it on all interfaces detected during startup.
+        However, in many cases it is desired to configure the server to listen and
+        respond on selected interfaces only. The sample commands in this section
+        show how to make interface selection using bindctl.
+      </para>
+      <para>
+        The default configuration can be presented with the following command:
+        <screen>
+&gt; <userinput>config show Dhcp6/interfaces</userinput>
+<userinput>Dhcp6/interfaces[0] "*" string</userinput></screen>
+        An asterisk sign plays a role of the wildcard and means "listen on all interfaces".
+      </para>
+      <para>
+        In order to override the default configuration, the existing entry can be replaced
+        with the actual interface name:
+        <screen>
+&gt; <userinput>config set Dhcp6/interfaces[0] eth1</userinput>
+&gt; <userinput>config commit</userinput></screen>
+        Other interface names can be added on one-by-one basis:
+        <screen>
+&gt; <userinput>config add Dhcp6/interfaces eth2</userinput>
+&gt; <userinput>config commit</userinput></screen>
+        Configuration will now contain two interfaces which can be presented as follows:
+        <screen>
+&gt; <userinput>config show Dhcp6/interfaces</userinput>
+<userinput>Dhcp6/interfaces[0]	"eth1"	string</userinput>
+<userinput>Dhcp6/interfaces[1]	"eth2"	string</userinput></screen>
+        When configuration gets committed, the server will start to listen on
+        eth1 and eth2 interfaces only.
+      </para>
+      <para>
+        It is possible to use wildcard interface name (asterisk) concurrently with explicit
+        interface names:
+        <screen>
+&gt; <userinput>config add Dhcp6/interfaces *</userinput>
+&gt; <userinput>config commit</userinput></screen>
+        This will result in the following configuration:
+        <screen>
+&gt; <userinput>config show Dhcp6/interfaces</userinput>
+<userinput>Dhcp6/interfaces[0]	"eth1"	string</userinput>
+<userinput>Dhcp6/interfaces[1]	"eth2"	string</userinput>
+<userinput>Dhcp6/interfaces[2]	"*"	string</userinput></screen>
+        The presence of the wildcard name implies that server will listen on all interfaces.
+        In order to fall back to the previous configuration when server listens on eth1 and eth2:
+        <screen>
+&gt; <userinput>config remove Dhcp6/interfaces[2]</userinput>
+&gt; <userinput>config commit</userinput></screen>
+      </para>
+    </section>
 
     <section>
       <title>Subnet and Address Pool</title>

+ 4 - 0
src/bin/auth/auth_messages.mes

@@ -151,6 +151,10 @@ A separate thread for maintaining data source clients has been started.
 % AUTH_DATASRC_CLIENTS_BUILDER_STOPPED data source builder thread stopped
 The separate thread for maintaining data source clients has been stopped.
 
+% AUTH_DATASRC_CLIENTS_BUILDER_WAKE_ERR failed to wake up main thread: %1
+A low-level error happened when trying to send data to the main thread to wake
+it up. Terminating to prevent inconsistent state and possiblu hang ups.
+
 % AUTH_DATASRC_CLIENTS_SHUTDOWN_ERROR error on waiting for data source builder thread: %1
 This indicates that the separate thread for maintaining data source
 clients had been terminated due to an uncaught exception, and the

+ 1 - 0
src/bin/auth/auth_srv.cc

@@ -319,6 +319,7 @@ AuthSrvImpl::AuthSrvImpl(AbstractXfroutClient& xfrout_client,
     xfrin_session_(NULL),
     counters_(),
     keyring_(NULL),
+    datasrc_clients_mgr_(io_service_),
     ddns_base_forwarder_(ddns_forwarder),
     ddns_forwarder_(NULL),
     xfrout_connected_(false),

+ 177 - 23
src/bin/auth/datasrc_clients_mgr.h

@@ -29,6 +29,9 @@
 #include <datasrc/client_list.h>
 #include <datasrc/memory/zone_writer.h>
 
+#include <asiolink/io_service.h>
+#include <asiolink/local_socket.h>
+
 #include <auth/auth_log.h>
 #include <auth/datasrc_config.h>
 
@@ -36,11 +39,16 @@
 #include <boost/bind.hpp>
 #include <boost/shared_ptr.hpp>
 #include <boost/noncopyable.hpp>
+#include <boost/function.hpp>
+#include <boost/foreach.hpp>
 
 #include <exception>
 #include <cassert>
+#include <cerrno>
 #include <list>
 #include <utility>
+#include <sys/types.h>
+#include <sys/socket.h>
 
 namespace isc {
 namespace auth {
@@ -77,13 +85,40 @@ enum CommandID {
     NUM_COMMANDS
 };
 
+/// \brief Callback to be called when the command is completed.
+typedef boost::function<void ()> FinishedCallback;
+
 /// \brief The data type passed from DataSrcClientsMgr to
-/// DataSrcClientsBuilder.
+///     DataSrcClientsBuilder.
 ///
-/// The first element of the pair is the command ID, and the second element
-/// is its argument.  If the command doesn't take an argument it should be
-/// a null pointer.
-typedef std::pair<CommandID, data::ConstElementPtr> Command;
+/// This just holds the data items together, no logic or protection
+/// is present here.
+struct Command {
+    /// \brief Constructor
+    ///
+    /// It just initializes the member variables of the same names
+    /// as the parameters.
+    Command(CommandID id, const data::ConstElementPtr& params,
+            const FinishedCallback& callback) :
+        id(id),
+        params(params),
+        callback(callback)
+    {}
+    /// \brief The command to execute
+    CommandID id;
+    /// \brief Argument of the command.
+    ///
+    /// If the command takes no argument, it should be null pointer.
+    ///
+    /// This may be a null pointer if the command takes no parameters.
+    data::ConstElementPtr params;
+    /// \brief A callback to be called once the command finishes.
+    ///
+    /// This may be an empty boost::function. In such case, no callback
+    /// will be called after completion.
+    FinishedCallback callback;
+};
+
 } // namespace datasrc_clientmgr_internal
 
 /// \brief Frontend to the manager object for data source clients.
@@ -113,6 +148,24 @@ private:
                      boost::shared_ptr<datasrc::ConfigurableClientList> >
     ClientListsMap;
 
+    class FDGuard : boost::noncopyable {
+    public:
+        FDGuard(DataSrcClientsMgrBase *mgr) :
+            mgr_(mgr)
+        {}
+        ~FDGuard() {
+            if (mgr_->read_fd_ != -1) {
+                close(mgr_->read_fd_);
+            }
+            if (mgr_->write_fd_ != -1) {
+                close(mgr_->write_fd_);
+            }
+        }
+    private:
+        DataSrcClientsMgrBase* mgr_;
+    };
+    friend class FDGuard;
+
 public:
     /// \brief Thread-safe accessor to the data source client lists.
     ///
@@ -176,12 +229,20 @@ public:
     ///
     /// \throw std::bad_alloc internal memory allocation failure.
     /// \throw isc::Unexpected general unexpected system errors.
-    DataSrcClientsMgrBase() :
+    DataSrcClientsMgrBase(asiolink::IOService& service) :
         clients_map_(new ClientListsMap),
-        builder_(&command_queue_, &cond_, &queue_mutex_, &clients_map_,
-                 &map_mutex_),
-        builder_thread_(boost::bind(&BuilderType::run, &builder_))
-    {}
+        fd_guard_(new FDGuard(this)),
+        read_fd_(-1), write_fd_(-1),
+        builder_(&command_queue_, &callback_queue_, &cond_, &queue_mutex_,
+                 &clients_map_, &map_mutex_, createFds()),
+        builder_thread_(boost::bind(&BuilderType::run, &builder_)),
+        wakeup_socket_(service, read_fd_)
+    {
+        // Schedule wakeups when callbacks are pushed.
+        wakeup_socket_.asyncRead(
+            boost::bind(&DataSrcClientsMgrBase::processCallbacks, this, _1),
+            buffer, 1);
+    }
 
     /// \brief The destructor.
     ///
@@ -220,6 +281,7 @@ public:
                       AUTH_DATASRC_CLIENTS_SHUTDOWN_UNEXPECTED_ERROR);
         }
 
+        processCallbacks(); // Any leftover callbacks
         cleanup();              // see below
     }
 
@@ -234,11 +296,18 @@ public:
     /// \brief std::bad_alloc
     ///
     /// \param config_arg The new data source configuration.  Must not be NULL.
-    void reconfigure(data::ConstElementPtr config_arg) {
+    /// \param callback Called once the reconfigure command completes. It is
+    ///     called in the main thread (not in the work one). It should be
+    ///     exceptionless.
+    void reconfigure(const data::ConstElementPtr& config_arg,
+                     const datasrc_clientmgr_internal::FinishedCallback&
+                     callback = datasrc_clientmgr_internal::FinishedCallback())
+    {
         if (!config_arg) {
             isc_throw(InvalidParameter, "Invalid null config argument");
         }
-        sendCommand(datasrc_clientmgr_internal::RECONFIGURE, config_arg);
+        sendCommand(datasrc_clientmgr_internal::RECONFIGURE, config_arg,
+                    callback);
         reconfigureHook();      // for test's customization
     }
 
@@ -257,12 +326,18 @@ public:
     /// \param args Element argument that should be a map of the form
     /// { "class": "IN", "origin": "example.com" }
     /// (but class is optional and will default to IN)
+    /// \param callback Called once the loadZone command completes. It
+    ///     is called in the main thread, not in the work thread. It should
+    ///     be exceptionless.
     ///
     /// \exception CommandError if the args value is null, or not in
     ///                                 the expected format, or contains
     ///                                 a bad origin or class string
     void
-    loadZone(data::ConstElementPtr args) {
+    loadZone(const data::ConstElementPtr& args,
+             const datasrc_clientmgr_internal::FinishedCallback& callback =
+             datasrc_clientmgr_internal::FinishedCallback())
+    {
         if (!args) {
             isc_throw(CommandError, "loadZone argument empty");
         }
@@ -303,7 +378,7 @@ public:
         // implement it would be to factor out the code from
         // the start of doLoadZone(), and call it here too
 
-        sendCommand(datasrc_clientmgr_internal::LOADZONE, args);
+        sendCommand(datasrc_clientmgr_internal::LOADZONE, args, callback);
     }
 
 private:
@@ -317,30 +392,79 @@ private:
     void reconfigureHook() {}
 
     void sendCommand(datasrc_clientmgr_internal::CommandID command,
-                     data::ConstElementPtr arg)
+                     const data::ConstElementPtr& arg,
+                     const datasrc_clientmgr_internal::FinishedCallback&
+                     callback = datasrc_clientmgr_internal::FinishedCallback())
     {
         // The lock will be held until the end of this method.  Only
         // push_back has to be protected, but we can avoid having an extra
         // block this way.
         typename MutexType::Locker locker(queue_mutex_);
         command_queue_.push_back(
-            datasrc_clientmgr_internal::Command(command, arg));
+            datasrc_clientmgr_internal::Command(command, arg, callback));
         cond_.signal();
     }
 
+    int createFds() {
+        int fds[2];
+        int result = socketpair(AF_LOCAL, SOCK_STREAM, 0, fds);
+        if (result != 0) {
+            isc_throw(Unexpected, "Can't create socket pair: " <<
+                      strerror(errno));
+        }
+        read_fd_ = fds[0];
+        write_fd_ = fds[1];
+        return write_fd_;
+    }
+
+    void processCallbacks(const std::string& error = std::string()) {
+        // Schedule the next read.
+        wakeup_socket_.asyncRead(
+            boost::bind(&DataSrcClientsMgrBase::processCallbacks, this, _1),
+            buffer, 1);
+        if (!error.empty()) {
+            // Generally, there should be no errors (as we are the other end
+            // as well), but check just in case.
+            isc_throw(Unexpected, error);
+        }
+
+        // Steal the callbacks into local copy.
+        std::list<datasrc_clientmgr_internal::FinishedCallback> queue;
+        {
+            typename MutexType::Locker locker(queue_mutex_);
+            queue.swap(callback_queue_);
+        }
+
+        // Execute the callbacks
+        BOOST_FOREACH(const datasrc_clientmgr_internal::FinishedCallback&
+                      callback, queue) {
+            callback();
+        }
+    }
+
     //
     // The following are shared with the builder.
     //
     // The list is used as a one-way queue: back-in, front-out
     std::list<datasrc_clientmgr_internal::Command> command_queue_;
+    // Similar to above, for the callbacks that are ready to be called.
+    // While the command queue is for sending commands from the main thread
+    // to the work thread, this one is for the other direction. Protected
+    // by the same mutex (queue_mutex_).
+    std::list<datasrc_clientmgr_internal::FinishedCallback> callback_queue_;
     CondVarType cond_;          // condition variable for queue operations
     MutexType queue_mutex_;     // mutex to protect the queue
     datasrc::ClientListMapPtr clients_map_;
                                 // map of actual data source client objects
+    boost::scoped_ptr<FDGuard> fd_guard_; // A guard to close the fds.
+    int read_fd_, write_fd_;    // Descriptors for wakeup
     MutexType map_mutex_;       // mutex to protect the clients map
 
     BuilderType builder_;
     ThreadType builder_thread_; // for safety this should be placed last
+    isc::asiolink::LocalSocket wakeup_socket_; // For integration of read_fd_
+                                               // to the asio loop
+    char buffer[1];   // Buffer for the wakeup socket.
 };
 
 namespace datasrc_clientmgr_internal {
@@ -385,12 +509,15 @@ public:
     ///
     /// \throw None
     DataSrcClientsBuilderBase(std::list<Command>* command_queue,
+                              std::list<FinishedCallback>* callback_queue,
                               CondVarType* cond, MutexType* queue_mutex,
                               datasrc::ClientListMapPtr* clients_map,
-                              MutexType* map_mutex
+                              MutexType* map_mutex,
+                              int wake_fd
         ) :
-        command_queue_(command_queue), cond_(cond), queue_mutex_(queue_mutex),
-        clients_map_(clients_map), map_mutex_(map_mutex)
+        command_queue_(command_queue), callback_queue_(callback_queue),
+        cond_(cond), queue_mutex_(queue_mutex),
+        clients_map_(clients_map), map_mutex_(map_mutex), wake_fd_(wake_fd)
     {}
 
     /// \brief The main loop.
@@ -457,10 +584,12 @@ private:
 
     // The following are shared with the manager
     std::list<Command>* command_queue_;
+    std::list<FinishedCallback> *callback_queue_;
     CondVarType* cond_;
     MutexType* queue_mutex_;
     datasrc::ClientListMapPtr* clients_map_;
     MutexType* map_mutex_;
+    int wake_fd_;
 };
 
 // Shortcut typedef for normal use
@@ -494,6 +623,31 @@ DataSrcClientsBuilderBase<MutexType, CondVarType>::run() {
                               AUTH_DATASRC_CLIENTS_BUILDER_COMMAND_ERROR).
                         arg(e.what());
                 }
+                if (current_commands.front().callback) {
+                    // Lock the queue
+                    typename MutexType::Locker locker(*queue_mutex_);
+                    callback_queue_->
+                        push_back(current_commands.front().callback);
+                    // Wake up the other end. If it would block, there are data
+                    // and it'll wake anyway.
+                    int result = send(wake_fd_, "w", 1, MSG_DONTWAIT);
+                    if (result == -1 &&
+                        (errno != EWOULDBLOCK && errno != EAGAIN)) {
+                        // Note: the strerror might not be thread safe, as
+                        // subsequent call to it might change the returned
+                        // string. But that is unlikely and strerror_r is
+                        // not portable and we are going to terminate anyway,
+                        // so that's better than nothing.
+                        //
+                        // Also, this error handler is not tested. It should
+                        // be generally impossible to happen, so it is hard
+                        // to trigger in controlled way.
+                        LOG_FATAL(auth_logger,
+                                  AUTH_DATASRC_CLIENTS_BUILDER_WAKE_ERR).
+                            arg(strerror(errno));
+                        std::terminate();
+                    }
+                }
                 current_commands.pop_front();
             }
         }
@@ -515,7 +669,7 @@ bool
 DataSrcClientsBuilderBase<MutexType, CondVarType>::handleCommand(
     const Command& command)
 {
-    const CommandID cid = command.first;
+    const CommandID cid = command.id;
     if (cid >= NUM_COMMANDS) {
         // This shouldn't happen except for a bug within this file.
         isc_throw(Unexpected, "internal bug: invalid command, ID: " << cid);
@@ -526,12 +680,12 @@ DataSrcClientsBuilderBase<MutexType, CondVarType>::handleCommand(
     };
     LOG_DEBUG(auth_logger, DBGLVL_TRACE_BASIC,
               AUTH_DATASRC_CLIENTS_BUILDER_COMMAND).arg(command_desc.at(cid));
-    switch (command.first) {
+    switch (command.id) {
     case RECONFIGURE:
-        doReconfigure(command.second);
+        doReconfigure(command.params);
         break;
     case LOADZONE:
-        doLoadZone(command.second);
+        doLoadZone(command.params);
         break;
     case SHUTDOWN:
         return (false);

+ 110 - 27
src/bin/auth/tests/datasrc_clients_builder_unittest.cc

@@ -36,9 +36,13 @@
 
 #include <boost/function.hpp>
 
+#include <sys/types.h>
+#include <sys/socket.h>
+
 #include <cstdlib>
 #include <string>
 #include <sstream>
+#include <cerrno>
 
 using isc::data::ConstElementPtr;
 using namespace isc::dns;
@@ -54,17 +58,24 @@ protected:
     DataSrcClientsBuilderTest() :
         clients_map(new std::map<RRClass,
                     boost::shared_ptr<ConfigurableClientList> >),
-        builder(&command_queue, &cond, &queue_mutex, &clients_map, &map_mutex),
+        write_end(-1), read_end(-1),
+        builder(&command_queue, &callback_queue, &cond, &queue_mutex,
+                &clients_map, &map_mutex, generateSockets()),
         cond(command_queue, delayed_command_queue), rrclass(RRClass::IN()),
-        shutdown_cmd(SHUTDOWN, ConstElementPtr()),
-        noop_cmd(NOOP, ConstElementPtr())
+        shutdown_cmd(SHUTDOWN, ConstElementPtr(), FinishedCallback()),
+        noop_cmd(NOOP, ConstElementPtr(), FinishedCallback())
     {}
+    ~ DataSrcClientsBuilderTest() {
+
+    }
 
     void configureZones();      // used for loadzone related tests
 
     ClientListMapPtr clients_map; // configured clients
     std::list<Command> command_queue; // test command queue
     std::list<Command> delayed_command_queue; // commands available after wait
+    std::list<FinishedCallback> callback_queue; // Callbacks from commands
+    int write_end, read_end;
     TestDataSrcClientsBuilder builder;
     TestCondVar cond;
     TestMutex queue_mutex;
@@ -72,6 +83,15 @@ protected:
     const RRClass rrclass;
     const Command shutdown_cmd;
     const Command noop_cmd;
+private:
+    int generateSockets() {
+        int pair[2];
+        int result = socketpair(AF_LOCAL, SOCK_STREAM, 0, pair);
+        assert(result == 0);
+        write_end = pair[0];
+        read_end = pair[1];
+        return write_end;
+    }
 };
 
 TEST_F(DataSrcClientsBuilderTest, runSingleCommand) {
@@ -82,6 +102,45 @@ TEST_F(DataSrcClientsBuilderTest, runSingleCommand) {
     EXPECT_EQ(0, cond.wait_count); // no wait because command queue is not empty
     EXPECT_EQ(1, queue_mutex.lock_count);
     EXPECT_EQ(1, queue_mutex.unlock_count);
+    // No callback scheduled, none called.
+    EXPECT_TRUE(callback_queue.empty());
+    // Not woken up.
+    char c;
+    int result = recv(read_end, &c, 1, MSG_DONTWAIT);
+    EXPECT_EQ(-1, result);
+    EXPECT_TRUE(errno == EAGAIN || errno == EWOULDBLOCK);
+}
+
+// Just to have a valid function callback to pass
+void emptyCallsback() {}
+
+// Check a command finished callback is passed
+TEST_F(DataSrcClientsBuilderTest, commandFinished) {
+    command_queue.push_back(Command(SHUTDOWN, ConstElementPtr(),
+                                    emptyCallsback));
+    builder.run();
+    EXPECT_EQ(0, cond.wait_count); // no wait because command queue is not empty
+    // Once for picking up data, once for putting the callback there
+    EXPECT_EQ(2, queue_mutex.lock_count);
+    EXPECT_EQ(2, queue_mutex.unlock_count);
+    // There's one callback in the queue
+    ASSERT_EQ(1, callback_queue.size());
+    // Not using EXPECT_EQ, as that produces warning in printing out the result
+    EXPECT_TRUE(emptyCallsback == callback_queue.front());
+    // And we are woken up.
+    char c;
+    int result = recv(read_end, &c, 1, MSG_DONTWAIT);
+    EXPECT_EQ(1, result);
+}
+
+// Test that low-level errors with the synchronization socket
+// (an unexpected condition) is detected and program aborted.
+TEST_F(DataSrcClientsBuilderTest, finishedCrash) {
+    command_queue.push_back(Command(SHUTDOWN, ConstElementPtr(),
+                                    emptyCallsback));
+    // Break the socket
+    close(write_end);
+    EXPECT_DEATH_IF_SUPPORTED({builder.run();}, "");
 }
 
 TEST_F(DataSrcClientsBuilderTest, runMultiCommands) {
@@ -138,7 +197,7 @@ TEST_F(DataSrcClientsBuilderTest, reconfigure) {
     // the error handling
 
     // A command structure we'll modify to send different commands
-    Command reconfig_cmd(RECONFIGURE, ConstElementPtr());
+    Command reconfig_cmd(RECONFIGURE, ConstElementPtr(), FinishedCallback());
 
     // Initially, no clients should be there
     EXPECT_TRUE(clients_map->empty());
@@ -166,7 +225,7 @@ TEST_F(DataSrcClientsBuilderTest, reconfigure) {
         "}"
     );
 
-    reconfig_cmd.second = good_config;
+    reconfig_cmd.params = good_config;
     EXPECT_TRUE(builder.handleCommand(reconfig_cmd));
     EXPECT_EQ(1, clients_map->size());
     EXPECT_EQ(1, map_mutex.lock_count);
@@ -177,7 +236,7 @@ TEST_F(DataSrcClientsBuilderTest, reconfigure) {
     // If a 'bad' command argument got here, the config validation should
     // have failed already, but still, the handler should return true,
     // and the clients_map should not be updated.
-    reconfig_cmd.second = Element::create("{ \"foo\": \"bar\" }");
+    reconfig_cmd.params = Element::create("{ \"foo\": \"bar\" }");
     EXPECT_TRUE(builder.handleCommand(reconfig_cmd));
     EXPECT_EQ(working_config_clients, clients_map);
     // Building failed, so map mutex should not have been locked again
@@ -185,7 +244,7 @@ TEST_F(DataSrcClientsBuilderTest, reconfigure) {
 
     // The same for a configuration that has bad data for the type it
     // specifies
-    reconfig_cmd.second = bad_config;
+    reconfig_cmd.params = bad_config;
     builder.handleCommand(reconfig_cmd);
     EXPECT_TRUE(builder.handleCommand(reconfig_cmd));
     EXPECT_EQ(working_config_clients, clients_map);
@@ -194,21 +253,21 @@ TEST_F(DataSrcClientsBuilderTest, reconfigure) {
 
     // The same goes for an empty parameter (it should at least be
     // an empty map)
-    reconfig_cmd.second = ConstElementPtr();
+    reconfig_cmd.params = ConstElementPtr();
     EXPECT_TRUE(builder.handleCommand(reconfig_cmd));
     EXPECT_EQ(working_config_clients, clients_map);
     EXPECT_EQ(1, map_mutex.lock_count);
 
     // Reconfigure again with the same good clients, the result should
     // be a different map than the original, but not an empty one.
-    reconfig_cmd.second = good_config;
+    reconfig_cmd.params = good_config;
     EXPECT_TRUE(builder.handleCommand(reconfig_cmd));
     EXPECT_NE(working_config_clients, clients_map);
     EXPECT_EQ(1, clients_map->size());
     EXPECT_EQ(2, map_mutex.lock_count);
 
     // And finally, try an empty config to disable all datasource clients
-    reconfig_cmd.second = Element::createMap();
+    reconfig_cmd.params = Element::createMap();
     EXPECT_TRUE(builder.handleCommand(reconfig_cmd));
     EXPECT_EQ(0, clients_map->size());
     EXPECT_EQ(3, map_mutex.lock_count);
@@ -224,7 +283,8 @@ TEST_F(DataSrcClientsBuilderTest, shutdown) {
 TEST_F(DataSrcClientsBuilderTest, badCommand) {
     // out-of-range command ID
     EXPECT_THROW(builder.handleCommand(Command(NUM_COMMANDS,
-                                               ConstElementPtr())),
+                                               ConstElementPtr(),
+                                               FinishedCallback())),
                  isc::Unexpected);
 }
 
@@ -308,7 +368,8 @@ TEST_F(DataSrcClientsBuilderTest, loadZone) {
 
     const Command loadzone_cmd(LOADZONE, Element::fromJSON(
                                    "{\"class\": \"IN\","
-                                   " \"origin\": \"test1.example\"}"));
+                                   " \"origin\": \"test1.example\"}"),
+                               FinishedCallback());
     EXPECT_TRUE(builder.handleCommand(loadzone_cmd));
 
     // loadZone involves two critical sections: one for getting the zone
@@ -369,7 +430,8 @@ TEST_F(DataSrcClientsBuilderTest,
     // Now send the command to reload it
     const Command loadzone_cmd(LOADZONE, Element::fromJSON(
                                    "{\"class\": \"IN\","
-                                   " \"origin\": \"example.org\"}"));
+                                   " \"origin\": \"example.org\"}"),
+                               FinishedCallback());
     EXPECT_TRUE(builder.handleCommand(loadzone_cmd));
     // And now it should be present too.
     EXPECT_EQ(ZoneFinder::SUCCESS,
@@ -380,7 +442,8 @@ TEST_F(DataSrcClientsBuilderTest,
     // An error case: the zone has no configuration. (note .com here)
     const Command nozone_cmd(LOADZONE, Element::fromJSON(
                                  "{\"class\": \"IN\","
-                                 " \"origin\": \"example.com\"}"));
+                                 " \"origin\": \"example.com\"}"),
+                             FinishedCallback());
     EXPECT_THROW(builder.handleCommand(nozone_cmd),
                  TestDataSrcClientsBuilder::InternalCommandError);
     // The previous zone is not hurt in any way
@@ -403,7 +466,8 @@ TEST_F(DataSrcClientsBuilderTest,
     builder.handleCommand(
                      Command(LOADZONE, Element::fromJSON(
                                  "{\"class\": \"IN\","
-                                 " \"origin\": \"example.org\"}")));
+                                 " \"origin\": \"example.org\"}"),
+                             FinishedCallback()));
     // Only one mutex was needed because there was no actual reload.
     EXPECT_EQ(orig_lock_count + 1, map_mutex.lock_count);
     EXPECT_EQ(orig_unlock_count + 1, map_mutex.unlock_count);
@@ -421,7 +485,8 @@ TEST_F(DataSrcClientsBuilderTest,
         builder.handleCommand(
             Command(LOADZONE, Element::fromJSON(
                         "{\"class\": \"IN\","
-                        " \"origin\": \"nosuchzone.example\"}"))),
+                        " \"origin\": \"nosuchzone.example\"}"),
+                    FinishedCallback())),
         TestDataSrcClientsBuilder::InternalCommandError);
 
     // basically impossible case: in-memory cache is completely disabled.
@@ -441,7 +506,8 @@ TEST_F(DataSrcClientsBuilderTest,
     EXPECT_THROW(builder.handleCommand(
                      Command(LOADZONE, Element::fromJSON(
                                  "{\"class\": \"IN\","
-                                 " \"origin\": \"example.org\"}"))),
+                                 " \"origin\": \"example.org\"}"),
+                             FinishedCallback())),
                  TestDataSrcClientsBuilder::InternalCommandError);
 }
 
@@ -454,13 +520,21 @@ TEST_F(DataSrcClientsBuilderTest, loadBrokenZone) {
     // there's an error in the new zone file.  reload will be rejected.
     const Command loadzone_cmd(LOADZONE, Element::fromJSON(
                                    "{\"class\": \"IN\","
-                                   " \"origin\": \"test1.example\"}"));
+                                   " \"origin\": \"test1.example\"}"),
+                               FinishedCallback());
     EXPECT_THROW(builder.handleCommand(loadzone_cmd),
                  TestDataSrcClientsBuilder::InternalCommandError);
     zoneChecks(clients_map, rrclass);     // zone shouldn't be replaced
 }
 
 TEST_F(DataSrcClientsBuilderTest, loadUnreadableZone) {
+    // If the test is run as the root user, it will fail as insufficient
+    // permissions will not stop the root user from using a file.
+    if (getuid() == 0) {
+        std::cerr << "Skipping test as it's run as the root user" << std::endl;
+        return;
+    }
+
     configureZones();
 
     // install the zone file as unreadable
@@ -469,7 +543,8 @@ TEST_F(DataSrcClientsBuilderTest, loadUnreadableZone) {
                              TEST_DATA_BUILDDIR "/test1.zone.copied"));
     const Command loadzone_cmd(LOADZONE, Element::fromJSON(
                                    "{\"class\": \"IN\","
-                                   " \"origin\": \"test1.example\"}"));
+                                   " \"origin\": \"test1.example\"}"),
+                               FinishedCallback());
     EXPECT_THROW(builder.handleCommand(loadzone_cmd),
                  TestDataSrcClientsBuilder::InternalCommandError);
     zoneChecks(clients_map, rrclass);     // zone shouldn't be replaced
@@ -482,7 +557,8 @@ TEST_F(DataSrcClientsBuilderTest, loadZoneWithoutDataSrc) {
                      Command(LOADZONE,
                              Element::fromJSON(
                                  "{\"class\": \"IN\", "
-                                 " \"origin\": \"test1.example\"}"))),
+                                 " \"origin\": \"test1.example\"}"),
+                             FinishedCallback())),
                  TestDataSrcClientsBuilder::InternalCommandError);
 }
 
@@ -492,7 +568,8 @@ TEST_F(DataSrcClientsBuilderTest, loadZoneInvalidParams) {
     if (!isc::util::unittests::runningOnValgrind()) {
         // null arg: this causes assertion failure
         EXPECT_DEATH_IF_SUPPORTED({
-                builder.handleCommand(Command(LOADZONE, ElementPtr()));
+                builder.handleCommand(Command(LOADZONE, ElementPtr(),
+                                              FinishedCallback()));
             }, "");
     }
 
@@ -501,7 +578,8 @@ TEST_F(DataSrcClientsBuilderTest, loadZoneInvalidParams) {
                      Command(LOADZONE,
                              Element::fromJSON(
                                  "{\"origin\": \"test1.example\","
-                                 " \"class\": \"no_such_class\"}"))),
+                                 " \"class\": \"no_such_class\"}"),
+                             FinishedCallback())),
                  InvalidRRClass);
 
     // not a string
@@ -509,7 +587,8 @@ TEST_F(DataSrcClientsBuilderTest, loadZoneInvalidParams) {
                      Command(LOADZONE,
                              Element::fromJSON(
                                  "{\"origin\": \"test1.example\","
-                                 " \"class\": 1}"))),
+                                 " \"class\": 1}"),
+                             FinishedCallback())),
                  isc::data::TypeError);
 
     // class or origin is missing: result in assertion failure
@@ -517,7 +596,8 @@ TEST_F(DataSrcClientsBuilderTest, loadZoneInvalidParams) {
         EXPECT_DEATH_IF_SUPPORTED({
                 builder.handleCommand(Command(LOADZONE,
                                               Element::fromJSON(
-                                                  "{\"class\": \"IN\"}")));
+                                                  "{\"class\": \"IN\"}"),
+                                              FinishedCallback()));
             }, "");
     }
 
@@ -525,12 +605,14 @@ TEST_F(DataSrcClientsBuilderTest, loadZoneInvalidParams) {
     EXPECT_THROW(builder.handleCommand(
                      Command(LOADZONE,
                              Element::fromJSON(
-                                 "{\"class\": \"IN\", \"origin\": \"...\"}"))),
+                                 "{\"class\": \"IN\", \"origin\": \"...\"}"),
+                             FinishedCallback())),
                  EmptyLabel);
     EXPECT_THROW(builder.handleCommand(
                      Command(LOADZONE,
                              Element::fromJSON(
-                                 "{\"origin\": 10, \"class\": 1}"))),
+                                 "{\"origin\": 10, \"class\": 1}"),
+                             FinishedCallback())),
                  isc::data::TypeError);
 }
 
@@ -561,7 +643,8 @@ TEST_F(DataSrcClientsBuilderTest,
                      Command(LOADZONE,
                              Element::fromJSON(
                                  "{\"origin\": \"test1.example\","
-                                 " \"class\": \"IN\"}"))),
+                                 " \"class\": \"IN\"}"),
+                             FinishedCallback())),
                  TestDataSrcClientsBuilder::InternalCommandError);
 }
 

+ 51 - 9
src/bin/auth/tests/datasrc_clients_mgr_unittest.cc

@@ -38,13 +38,13 @@ void
 shutdownCheck() {
     // Check for common points on shutdown.  The manager should have acquired
     // the lock, put a SHUTDOWN command to the queue, and should have signaled
-    // the builder.
-    EXPECT_EQ(1, FakeDataSrcClientsBuilder::queue_mutex->lock_count);
+    // the builder. It should check again for the callback queue, with the lock
+    EXPECT_EQ(2, FakeDataSrcClientsBuilder::queue_mutex->lock_count);
     EXPECT_EQ(1, FakeDataSrcClientsBuilder::cond->signal_count);
     EXPECT_EQ(1, FakeDataSrcClientsBuilder::command_queue->size());
     const Command& cmd = FakeDataSrcClientsBuilder::command_queue->front();
-    EXPECT_EQ(SHUTDOWN, cmd.first);
-    EXPECT_FALSE(cmd.second);   // no argument
+    EXPECT_EQ(SHUTDOWN, cmd.id);
+    EXPECT_FALSE(cmd.params);   // no argument
 
     // Finally, the manager should wait for the thread to terminate.
     EXPECT_TRUE(FakeDataSrcClientsBuilder::thread_waited);
@@ -130,8 +130,8 @@ TEST(DataSrcClientsMgrTest, reconfigure) {
     // touch or refer to the map, so it shouldn't acquire the map lock.
     checkSharedMembers(1, 1, 0, 0, 1, 1);
     const Command& cmd1 = FakeDataSrcClientsBuilder::command_queue->front();
-    EXPECT_EQ(RECONFIGURE, cmd1.first);
-    EXPECT_EQ(reconfigure_arg, cmd1.second);
+    EXPECT_EQ(RECONFIGURE, cmd1.id);
+    EXPECT_EQ(reconfigure_arg, cmd1.params);
 
     // Non-null, but semantically invalid argument.  The manager doesn't do
     // this check, so it should result in the same effect.
@@ -140,8 +140,8 @@ TEST(DataSrcClientsMgrTest, reconfigure) {
     mgr.reconfigure(reconfigure_arg);
     checkSharedMembers(2, 2, 0, 0, 2, 1);
     const Command& cmd2 = FakeDataSrcClientsBuilder::command_queue->front();
-    EXPECT_EQ(RECONFIGURE, cmd2.first);
-    EXPECT_EQ(reconfigure_arg, cmd2.second);
+    EXPECT_EQ(RECONFIGURE, cmd2.id);
+    EXPECT_EQ(reconfigure_arg, cmd2.params);
 
     // Passing NULL argument is immediately rejected
     EXPECT_THROW(mgr.reconfigure(ConstElementPtr()), isc::InvalidParameter);
@@ -245,10 +245,52 @@ TEST(DataSrcClientsMgrTest, reload) {
     EXPECT_EQ(3, FakeDataSrcClientsBuilder::command_queue->size());
 }
 
+void
+callback(bool* called, int *tag_target, int tag_value) {
+    *called = true;
+    *tag_target = tag_value;
+}
+
+// Test we can wake up the main thread by writing to the file descriptor and
+// that the callbacks are executed and removed when woken up.
+TEST(DataSrcClientsMgrTest, wakeup) {
+    bool called = false;
+    int tag;
+    {
+        TestDataSrcClientsMgr mgr;
+        // There's some real file descriptor (or something that looks so)
+        ASSERT_GT(FakeDataSrcClientsBuilder::wakeup_fd, 0);
+        // Push a callback in and wake the manager
+        FakeDataSrcClientsBuilder::callback_queue->
+            push_back(boost::bind(callback, &called, &tag, 1));
+        EXPECT_EQ(1, write(FakeDataSrcClientsBuilder::wakeup_fd, "w", 1));
+        mgr.run_one();
+        EXPECT_TRUE(called);
+        EXPECT_EQ(1, tag);
+        EXPECT_TRUE(FakeDataSrcClientsBuilder::callback_queue->empty());
+
+        called = false;
+        // If we wake up and don't push anything, it doesn't break.
+        EXPECT_EQ(1, write(FakeDataSrcClientsBuilder::wakeup_fd, "w", 1));
+        mgr.run_one();
+        EXPECT_FALSE(called);
+
+        // When we terminate, it should process whatever is left
+        // of the callbacks. So push and terminate (and don't directly
+        // wake).
+        FakeDataSrcClientsBuilder::callback_queue->
+            push_back(boost::bind(callback, &called, &tag, 2));
+    }
+    EXPECT_TRUE(called);
+    EXPECT_EQ(2, tag);
+    EXPECT_TRUE(FakeDataSrcClientsBuilder::callback_queue->empty());
+}
+
 TEST(DataSrcClientsMgrTest, realThread) {
     // Using the non-test definition with a real thread.  Just checking
     // no disruption happens.
-    DataSrcClientsMgr mgr;
+    isc::asiolink::IOService service;
+    DataSrcClientsMgr mgr(service);
 }
 
 } // unnamed namespace

+ 11 - 4
src/bin/auth/tests/test_datasrc_clients_mgr.cc

@@ -26,7 +26,9 @@ namespace datasrc_clientmgr_internal {
 // Define static DataSrcClientsBuilder member variables.
 bool FakeDataSrcClientsBuilder::started = false;
 std::list<Command>* FakeDataSrcClientsBuilder::command_queue = NULL;
+std::list<FinishedCallback>* FakeDataSrcClientsBuilder::callback_queue = NULL;
 std::list<Command> FakeDataSrcClientsBuilder::command_queue_copy;
+std::list<FinishedCallback> FakeDataSrcClientsBuilder::callback_queue_copy;
 TestCondVar* FakeDataSrcClientsBuilder::cond = NULL;
 TestCondVar FakeDataSrcClientsBuilder::cond_copy;
 TestMutex* FakeDataSrcClientsBuilder::queue_mutex = NULL;
@@ -38,6 +40,7 @@ bool FakeDataSrcClientsBuilder::thread_waited = false;
 FakeDataSrcClientsBuilder::ExceptionFromWait
 FakeDataSrcClientsBuilder::thread_throw_on_wait =
     FakeDataSrcClientsBuilder::NOTHROW;
+int FakeDataSrcClientsBuilder::wakeup_fd = -1;
 
 template<>
 void
@@ -58,7 +61,7 @@ TestDataSrcClientsBuilder::doNoop() {
 
 template<>
 void
-TestDataSrcClientsMgr::cleanup() {
+TestDataSrcClientsMgrBase::cleanup() {
     using namespace datasrc_clientmgr_internal;
     // Make copy of some of the manager's member variables and reset the
     // corresponding pointers.  The currently pointed objects are in the
@@ -73,17 +76,21 @@ TestDataSrcClientsMgr::cleanup() {
     FakeDataSrcClientsBuilder::cond_copy = cond_;
     FakeDataSrcClientsBuilder::cond =
         &FakeDataSrcClientsBuilder::cond_copy;
+    FakeDataSrcClientsBuilder::callback_queue_copy =
+        *FakeDataSrcClientsBuilder::callback_queue;
+    FakeDataSrcClientsBuilder::callback_queue =
+        &FakeDataSrcClientsBuilder::callback_queue_copy;
 }
 
 template<>
 void
-TestDataSrcClientsMgr::reconfigureHook() {
+TestDataSrcClientsMgrBase::reconfigureHook() {
     using namespace datasrc_clientmgr_internal;
 
     // Simply replace the local map, ignoring bogus config value.
-    assert(command_queue_.front().first == RECONFIGURE);
+    assert(command_queue_.front().id == RECONFIGURE);
     try {
-        clients_map_ = configureDataSource(command_queue_.front().second);
+        clients_map_ = configureDataSource(command_queue_.front().params);
     } catch (...) {}
 }
 

+ 25 - 5
src/bin/auth/tests/test_datasrc_clients_mgr.h

@@ -20,6 +20,8 @@
 #include <auth/datasrc_clients_mgr.h>
 #include <datasrc/datasrc_config.h>
 
+#include <asiolink/io_service.h>
+
 #include <boost/function.hpp>
 
 #include <list>
@@ -131,15 +133,18 @@ public:
     // true iff a builder has started.
     static bool started;
 
-    // These three correspond to the resource shared with the manager.
+    // These five correspond to the resource shared with the manager.
     // xxx_copy will be set in the manager's destructor to record the
     // final state of the manager.
     static std::list<Command>* command_queue;
+    static std::list<FinishedCallback>* callback_queue;
     static TestCondVar* cond;
     static TestMutex* queue_mutex;
+    static int wakeup_fd;
     static isc::datasrc::ClientListMapPtr* clients_map;
     static TestMutex* map_mutex;
     static std::list<Command> command_queue_copy;
+    static std::list<FinishedCallback> callback_queue_copy;
     static TestCondVar cond_copy;
     static TestMutex queue_mutex_copy;
 
@@ -153,15 +158,18 @@ public:
 
     FakeDataSrcClientsBuilder(
         std::list<Command>* command_queue,
+        std::list<FinishedCallback>* callback_queue,
         TestCondVar* cond,
         TestMutex* queue_mutex,
         isc::datasrc::ClientListMapPtr* clients_map,
-        TestMutex* map_mutex)
+        TestMutex* map_mutex, int wakeup_fd)
     {
         FakeDataSrcClientsBuilder::started = false;
         FakeDataSrcClientsBuilder::command_queue = command_queue;
+        FakeDataSrcClientsBuilder::callback_queue = callback_queue;
         FakeDataSrcClientsBuilder::cond = cond;
         FakeDataSrcClientsBuilder::queue_mutex = queue_mutex;
+        FakeDataSrcClientsBuilder::wakeup_fd = wakeup_fd;
         FakeDataSrcClientsBuilder::clients_map = clients_map;
         FakeDataSrcClientsBuilder::map_mutex = map_mutex;
         FakeDataSrcClientsBuilder::thread_waited = false;
@@ -201,18 +209,30 @@ typedef DataSrcClientsMgrBase<
     datasrc_clientmgr_internal::TestThread,
     datasrc_clientmgr_internal::FakeDataSrcClientsBuilder,
     datasrc_clientmgr_internal::TestMutex,
-    datasrc_clientmgr_internal::TestCondVar> TestDataSrcClientsMgr;
+    datasrc_clientmgr_internal::TestCondVar> TestDataSrcClientsMgrBase;
 
 // A specialization of manager's "cleanup" called at the end of the
 // destructor.  We use this to record the final values of some of the class
 // member variables.
 template<>
 void
-TestDataSrcClientsMgr::cleanup();
+TestDataSrcClientsMgrBase::cleanup();
 
 template<>
 void
-TestDataSrcClientsMgr::reconfigureHook();
+TestDataSrcClientsMgrBase::reconfigureHook();
+
+// A (hackish) trick how to not require the IOService to be passed from the
+// tests. We can't create the io service as a member, because it would
+// get initialized too late.
+class TestDataSrcClientsMgr :
+    public asiolink::IOService,
+    public TestDataSrcClientsMgrBase {
+public:
+    TestDataSrcClientsMgr() :
+        TestDataSrcClientsMgrBase(*static_cast<asiolink::IOService*>(this))
+    {}
+};
 } // namespace auth
 } // namespace isc
 

+ 2 - 0
src/bin/cmdctl/tests/b10-certgen_test.py

@@ -200,6 +200,8 @@ class TestCertGenTool(unittest.TestCase):
         # No such file
         self.run_check(105, None, None, [self.TOOL, '-c', 'foo'])
 
+    @unittest.skipIf(os.getuid() == 0,
+                     'test cannot be run as root user')
     def test_permissions(self):
         """
         Test some combinations of correct and bad permissions.

+ 15 - 5
src/bin/cmdctl/tests/cmdctl_test.py

@@ -680,11 +680,15 @@ class TestSecureHTTPServer(unittest.TestCase):
         # Just some file that we know exists
         file_name = BUILD_FILE_PATH + 'cmdctl-keyfile.pem'
         check_file(file_name)
-        with UnreadableFile(file_name):
-            self.assertRaises(CmdctlException, check_file, file_name)
         self.assertRaises(CmdctlException, check_file, '/local/not-exist')
         self.assertRaises(CmdctlException, check_file, '/')
 
+    @unittest.skipIf(os.getuid() == 0,
+                     'test cannot be run as root user')
+    def test_check_file_for_unreadable(self):
+        file_name = BUILD_FILE_PATH + 'cmdctl-keyfile.pem'
+        with UnreadableFile(file_name):
+            self.assertRaises(CmdctlException, check_file, file_name)
 
     def test_check_key_and_cert(self):
         keyfile = BUILD_FILE_PATH + 'cmdctl-keyfile.pem'
@@ -702,6 +706,15 @@ class TestSecureHTTPServer(unittest.TestCase):
         self.assertRaises(CmdctlException, self.server._check_key_and_cert,
                          '/', certfile)
 
+        # All OK (also happens to check the context code above works)
+        self.server._check_key_and_cert(keyfile, certfile)
+
+    @unittest.skipIf(os.getuid() == 0,
+                     'test cannot be run as root user')
+    def test_check_key_and_cert_for_unreadable(self):
+        keyfile = BUILD_FILE_PATH + 'cmdctl-keyfile.pem'
+        certfile = BUILD_FILE_PATH + 'cmdctl-certfile.pem'
+
         # no read permission
         with UnreadableFile(certfile):
             self.assertRaises(CmdctlException,
@@ -713,9 +726,6 @@ class TestSecureHTTPServer(unittest.TestCase):
                               self.server._check_key_and_cert,
                               keyfile, certfile)
 
-        # All OK (also happens to check the context code above works)
-        self.server._check_key_and_cert(keyfile, certfile)
-
     def test_wrap_sock_in_ssl_context(self):
         sock = socket.socket()
 

+ 1 - 0
src/bin/d2/Makefile.am

@@ -71,6 +71,7 @@ b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
 b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la 
 b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/dns/libb10-dns++.la
 b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
+b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
 
 b10_dhcp_ddnsdir = $(pkgdatadir)
 b10_dhcp_ddns_DATA = dhcp-ddns.spec

+ 1 - 0
src/bin/d2/tests/Makefile.am

@@ -88,6 +88,7 @@ d2_unittests_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
 d2_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
 d2_unittests_LDADD += $(top_builddir)/src/lib/dns/libb10-dns++.la
 d2_unittests_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
+d2_unittests_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
 
 endif
 

+ 8 - 6
src/bin/d2/tests/d_test_stubs.h

@@ -608,11 +608,12 @@ public:
         try  {
             config_set_ = isc::data::Element::fromJSON(json_text);
         } catch (const isc::Exception &ex) {
-            return  ::testing::AssertionFailure() 
-                << "JSON text failed to parse:" << ex.what();
+            return (::testing::AssertionFailure(::testing::Message() << 
+                                                "JSON text failed to parse:" 
+                                                << ex.what())); 
         }
 
-        return ::testing::AssertionSuccess();
+        return (::testing::AssertionSuccess());
     }
 
 
@@ -628,11 +629,12 @@ public:
         isc::data::ConstElementPtr comment;
         comment = isc::config::parseAnswer(rcode, answer_);
         if (rcode == should_be) {
-            return testing::AssertionSuccess();
+            return (testing::AssertionSuccess());
         }
 
-        return ::testing::AssertionFailure() << "checkAnswer rcode:" 
-               << rcode << " comment: " << *comment;
+        return (::testing::AssertionFailure(::testing::Message() << 
+                                            "checkAnswer rcode:" << rcode 
+                                            << " comment: " << *comment));
     }
 
     /// @brief Configuration set being tested.

+ 1 - 0
src/bin/dhcp4/Makefile.am

@@ -63,6 +63,7 @@ b10_dhcp4_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
+b10_dhcp4_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
 
 b10_dhcp4dir = $(pkgdatadir)
 b10_dhcp4_DATA = dhcp4.spec

+ 52 - 42
src/bin/dhcp4/config_parser.cc

@@ -53,10 +53,10 @@ public:
     /// @param dummy first param, option names are always "Dhcp4/option-data[n]"
     /// @param options is the option storage in which to store the parsed option
     /// upon "commit".
-    /// @param global_context is a pointer to the global context which 
+    /// @param global_context is a pointer to the global context which
     /// stores global scope parameters, options, option defintions.
-    Dhcp4OptionDataParser(const std::string&, 
-        OptionStoragePtr options, ParserContextPtr global_context) 
+    Dhcp4OptionDataParser(const std::string&,
+        OptionStoragePtr options, ParserContextPtr global_context)
         :OptionDataParser("", options, global_context) {
     }
 
@@ -64,7 +64,7 @@ public:
     ///
     /// @param param_name name of the parameter to be parsed.
     /// @param options storage where the parameter value is to be stored.
-    /// @param global_context is a pointer to the global context which 
+    /// @param global_context is a pointer to the global context which
     /// stores global scope parameters, options, option defintions.
     /// @return returns a pointer to a new OptionDataParser. Caller is
     /// is responsible for deleting it when it is no longer needed.
@@ -75,16 +75,16 @@ public:
 
 protected:
     /// @brief Finds an option definition within the server's option space
-    /// 
-    /// Given an option space and an option code, find the correpsonding 
+    ///
+    /// Given an option space and an option code, find the correpsonding
     /// option defintion within the server's option defintion storage.
     ///
-    /// @param option_space name of the parameter option space 
-    /// @param option_code numeric value of the parameter to find 
-    /// @return OptionDefintionPtr of the option defintion or an 
+    /// @param option_space name of the parameter option space
+    /// @param option_code numeric value of the parameter to find
+    /// @return OptionDefintionPtr of the option defintion or an
     /// empty OptionDefinitionPtr if not found.
-    /// @throw DhcpConfigError if the option space requested is not valid 
-    /// for this server. 
+    /// @throw DhcpConfigError if the option space requested is not valid
+    /// for this server.
     virtual OptionDefinitionPtr findServerSpaceOptionDefinition (
                 std::string& option_space, uint32_t option_code) {
         OptionDefinitionPtr def;
@@ -100,11 +100,11 @@ protected:
     }
 };
 
-/// @brief Parser for IPv4 pool definitions.  
+/// @brief Parser for IPv4 pool definitions.
 ///
-/// This is the IPv4 derivation of the PoolParser class and handles pool 
-/// definitions, i.e. a list of entries of one of two syntaxes: min-max and 
-/// prefix/len for IPv4 pools. Pool4 objects are created and stored in chosen 
+/// This is the IPv4 derivation of the PoolParser class and handles pool
+/// definitions, i.e. a list of entries of one of two syntaxes: min-max and
+/// prefix/len for IPv4 pools. Pool4 objects are created and stored in chosen
 /// PoolStorage container.
 ///
 /// It is useful for parsing Dhcp4/subnet4[X]/pool parameters.
@@ -126,9 +126,9 @@ protected:
     ///
     /// @param addr is the IPv4 prefix of the pool.
     /// @param len is the prefix length.
-    /// @param ignored dummy parameter to provide symmetry between the 
+    /// @param ignored dummy parameter to provide symmetry between the
     /// PoolParser derivations. The V6 derivation requires a third value.
-    /// @return returns a PoolPtr to the new Pool4 object. 
+    /// @return returns a PoolPtr to the new Pool4 object.
     PoolPtr poolMaker (IOAddress &addr, uint32_t len, int32_t) {
         return (PoolPtr(new Pool4(addr, len)));
     }
@@ -137,9 +137,9 @@ protected:
     ///
     /// @param min is the first IPv4 address in the pool.
     /// @param max is the last IPv4 address in the pool.
-    /// @param ignored dummy parameter to provide symmetry between the 
+    /// @param ignored dummy parameter to provide symmetry between the
     /// PoolParser derivations. The V6 derivation requires a third value.
-    /// @return returns a PoolPtr to the new Pool4 object. 
+    /// @return returns a PoolPtr to the new Pool4 object.
     PoolPtr poolMaker (IOAddress &min, IOAddress &max, int32_t) {
         return (PoolPtr(new Pool4(min, max)));
     }
@@ -147,8 +147,8 @@ protected:
 
 /// @brief This class parses a single IPv4 subnet.
 ///
-/// This is the IPv4 derivation of the SubnetConfigParser class and it parses 
-/// the whole subnet definition. It creates parsersfor received configuration 
+/// This is the IPv4 derivation of the SubnetConfigParser class and it parses
+/// the whole subnet definition. It creates parsersfor received configuration
 /// parameters as needed.
 class Subnet4ConfigParser : public SubnetConfigParser {
 public:
@@ -158,7 +158,7 @@ public:
     /// stores global scope parameters, options, option defintions.
     Subnet4ConfigParser(const std::string&)
         :SubnetConfigParser("", globalContext()) {
-    } 
+    }
 
     /// @brief Adds the created subnet to a server's configuration.
     /// @throw throws Unexpected if dynamic cast fails.
@@ -167,7 +167,7 @@ public:
             Subnet4Ptr sub4ptr = boost::dynamic_pointer_cast<Subnet4>(subnet_);
             if (!sub4ptr) {
                 // If we hit this, it is a programming error.
-                isc_throw(Unexpected, 
+                isc_throw(Unexpected,
                           "Invalid cast in Subnet4ConfigParser::commit");
             }
 
@@ -191,13 +191,13 @@ protected:
             (config_id.compare("renew-timer") == 0)  ||
             (config_id.compare("rebind-timer") == 0))  {
             parser = new Uint32Parser(config_id, uint32_values_);
-        } else if ((config_id.compare("subnet") == 0) || 
+        } else if ((config_id.compare("subnet") == 0) ||
                    (config_id.compare("interface") == 0)) {
             parser = new StringParser(config_id, string_values_);
         } else if (config_id.compare("pool") == 0) {
             parser = new Pool4Parser(config_id, pools_);
         } else if (config_id.compare("option-data") == 0) {
-           parser = new OptionDataListParser(config_id, options_, 
+           parser = new OptionDataListParser(config_id, options_,
                                              global_context_,
                                              Dhcp4OptionDataParser::factory);
         } else {
@@ -210,7 +210,7 @@ protected:
 
 
     /// @brief Determines if the given option space name and code describe
-    /// a standard option for the DCHP4 server. 
+    /// a standard option for the DCHP4 server.
     ///
     /// @param option_space is the name of the option space to consider
     /// @param code is the numeric option code to consider
@@ -230,12 +230,12 @@ protected:
     }
 
     /// @brief Issues a DHCP4 server specific warning regarding duplicate subnet
-    /// options. 
-    /// 
+    /// options.
+    ///
     /// @param code is the numeric option code of the duplicate option
-    /// @param addr is the subnet address 
+    /// @param addr is the subnet address
     /// @todo a means to know the correct logger and perhaps a common
-    /// message would allow this method to be emitted by the base class. 
+    /// message would allow this method to be emitted by the base class.
     virtual void duplicate_option_warning(uint32_t code,
                                          isc::asiolink::IOAddress& addr) {
         LOG_WARN(dhcp4_logger, DHCP4_CONFIG_OPTION_DUPLICATE)
@@ -243,10 +243,10 @@ protected:
     }
 
     /// @brief Instantiates the IPv4 Subnet based on a given IPv4 address
-    /// and prefix length.  
-    /// 
+    /// and prefix length.
+    ///
     /// @param addr is IPv4 address of the subnet.
-    /// @param len is the prefix length 
+    /// @param len is the prefix length
     void initSubnet(isc::asiolink::IOAddress addr, uint8_t len) {
         // Get all 'time' parameters using inheritance.
         // If the subnet-specific value is defined then use it, else
@@ -338,32 +338,32 @@ namespace dhcp {
 ///
 /// @param config_id pointer to received global configuration entry
 /// @return parser for specified global DHCPv4 parameter
-/// @throw NotImplemented if trying to create a parser for unknown 
+/// @throw NotImplemented if trying to create a parser for unknown
 /// config element
 DhcpConfigParser* createGlobalDhcp4ConfigParser(const std::string& config_id) {
     DhcpConfigParser* parser = NULL;
     if ((config_id.compare("valid-lifetime") == 0)  ||
         (config_id.compare("renew-timer") == 0)  ||
         (config_id.compare("rebind-timer") == 0))  {
-        parser = new Uint32Parser(config_id, 
+        parser = new Uint32Parser(config_id,
                                  globalContext()->uint32_values_);
-    } else if (config_id.compare("interface") == 0) {
+    } else if (config_id.compare("interfaces") == 0) {
         parser = new InterfaceListConfigParser(config_id);
     } else if (config_id.compare("subnet4") == 0) {
         parser = new Subnets4ListConfigParser(config_id);
     } else if (config_id.compare("option-data") == 0) {
-        parser = new OptionDataListParser(config_id, 
-                                          globalContext()->options_, 
+        parser = new OptionDataListParser(config_id,
+                                          globalContext()->options_,
                                           globalContext(),
                                           Dhcp4OptionDataParser::factory);
     } else if (config_id.compare("option-def") == 0) {
-        parser  = new OptionDefListParser(config_id, 
+        parser  = new OptionDefListParser(config_id,
                                           globalContext()->option_defs_);
     } else if (config_id.compare("version") == 0) {
-        parser  = new StringParser(config_id, 
+        parser  = new StringParser(config_id,
                                     globalContext()->string_values_);
     } else if (config_id.compare("lease-database") == 0) {
-        parser = new DbAccessParser(config_id); 
+        parser = new DbAccessParser(config_id);
     } else {
         isc_throw(NotImplemented,
                 "Parser error: Global configuration parameter not supported: "
@@ -384,7 +384,7 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set) {
     /// @todo: Append most essential info here (like "2 new subnets configured")
     string config_details;
 
-    LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND, 
+    LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND,
               DHCP4_CONFIG_START).arg(config_set->str());
 
     // Some of the values specified in the configuration depend on
@@ -397,6 +397,7 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set) {
     ParserCollection independent_parsers;
     ParserPtr subnet_parser;
     ParserPtr option_parser;
+    ParserPtr iface_parser;
 
     // The subnet parsers implement data inheritance by directly
     // accessing global storage. For this reason the global data
@@ -428,6 +429,11 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set) {
                 subnet_parser = parser;
             } else if (config_pair.first == "option-data") {
                 option_parser = parser;
+            } else if (config_pair.first == "interfaces") {
+                // The interface parser is independent from any other
+                // parser and can be run here before any other parsers.
+                iface_parser = parser;
+                parser->build(config_pair.second);
             } else {
                 // Those parsers should be started before other
                 // parsers so we can call build straight away.
@@ -483,6 +489,10 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set) {
             if (subnet_parser) {
                 subnet_parser->commit();
             }
+
+            if (iface_parser) {
+                iface_parser->commit();
+            }
         }
         catch (const isc::Exception& ex) {
             LOG_ERROR(dhcp4_logger, DHCP4_PARSER_COMMIT_FAIL).arg(ex.what());

+ 2 - 2
src/bin/dhcp4/config_parser.h

@@ -30,7 +30,7 @@ namespace dhcp {
 
 class Dhcpv4Srv;
 
-/// @brief Configure DHCPv4 server (@c Dhcpv4Srv) with a set of configuration 
+/// @brief Configure DHCPv4 server (@c Dhcpv4Srv) with a set of configuration
 /// values.
 ///
 /// This function parses configuration information stored in @c config_set
@@ -44,7 +44,7 @@ class Dhcpv4Srv;
 /// (such as malformed configuration or invalid configuration parameter),
 /// this function returns appropriate error code.
 ///
-/// This function is called every time a new configuration is received. The 
+/// This function is called every time a new configuration is received. The
 /// extra parameter is a reference to DHCPv4 server component. It is currently
 /// not used and CfgMgr::instance() is accessed instead.
 ///

+ 29 - 5
src/bin/dhcp4/ctrl_dhcp4_srv.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -20,17 +20,17 @@
 #include <config/ccsession.h>
 #include <dhcp/iface_mgr.h>
 #include <dhcpsrv/dhcp_config_parser.h>
+#include <dhcpsrv/cfgmgr.h>
 #include <dhcp4/ctrl_dhcp4_srv.h>
 #include <dhcp4/dhcp4_log.h>
 #include <dhcp4/spec_config.h>
 #include <dhcp4/config_parser.h>
 #include <exceptions/exceptions.h>
 #include <util/buffer.h>
-#include <cassert>
-#include <iostream>
 
 #include <cassert>
 #include <iostream>
+#include <sstream>
 
 using namespace isc::asiolink;
 using namespace isc::cc;
@@ -101,7 +101,27 @@ ControlledDhcpv4Srv::dhcp4ConfigHandler(ConstElementPtr new_config) {
     }
 
     // Configure the server.
-    return (configureDhcp4Server(*server_, merged_config));
+    ConstElementPtr answer = configureDhcp4Server(*server_, merged_config);
+
+    // Check that configuration was successful. If not, do not reopen sockets.
+    int rcode = 0;
+    parseAnswer(rcode, answer);
+    if (rcode != 0) {
+        return (answer);
+    }
+
+    // Configuration may change active interfaces. Therefore, we have to reopen
+    // sockets according to new configuration. This operation is not exception
+    // safe and we really don't want to emit exceptions to the callback caller.
+    // Instead, catch an exception and create appropriate answer.
+    try {
+        server_->openActiveSockets(server_->getPort(), server_->useBroadcast());
+    } catch (std::exception& ex) {
+        std::ostringstream err;
+        err << "failed to open sockets after server reconfiguration: " << ex.what();
+        answer = isc::config::createAnswer(1, err.str());
+    }
+    return (answer);
 }
 
 ConstElementPtr
@@ -174,8 +194,13 @@ void ControlledDhcpv4Srv::establishSession() {
 
     try {
         configureDhcp4Server(*this, config_session_->getFullConfig());
+        // Configuration may disable or enable interfaces so we have to
+        // reopen sockets according to new configuration.
+        openActiveSockets(getPort(), useBroadcast());
+
     } catch (const DhcpConfigError& ex) {
         LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL).arg(ex.what());
+
     }
 
     /// Integrate the asynchronous I/O model of BIND 10 configuration
@@ -230,6 +255,5 @@ ControlledDhcpv4Srv::execDhcpv4ServerCommand(const std::string& command_id,
     }
 }
 
-
 };
 };

+ 2 - 1
src/bin/dhcp4/ctrl_dhcp4_srv.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013  Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -130,6 +130,7 @@ protected:
     /// when there is a new command or configuration sent over msgq.
     static void sessionReader(void);
 
+
     /// @brief IOService object, used for all ASIO operations.
     isc::asiolink::IOService io_service_;
 

+ 4 - 0
src/bin/dhcp4/dhcp4.dox

@@ -79,4 +79,8 @@ See \ref dhcpv6ConfigParser.
 Configuration inheritance in DHCPv4 follows exactly the same logic as its DHCPv6
 counterpart. See \ref dhcpv6ConfigInherit.
 
+@section dhcpv4Other Other DHCPv4 topics
+
+ For hooks API support in DHCPv4, see @ref dhcpv4Hooks.
+
 */

+ 3 - 3
src/bin/dhcp4/dhcp4.spec

@@ -3,16 +3,16 @@
     "module_name": "Dhcp4",
     "module_description": "DHCPv4 server daemon",
     "config_data": [
-      { "item_name": "interface",
+      { "item_name": "interfaces",
         "item_type": "list",
         "item_optional": false,
-        "item_default": [ "all" ],
+        "item_default": [ "*" ],
         "list_item_spec":
         {
           "item_name": "interface_name",
           "item_type": "string",
           "item_optional": false,
-          "item_default": "all"
+          "item_default": "*"
         }
       } ,
 

+ 138 - 0
src/bin/dhcp4/dhcp4_hooks.dox

@@ -0,0 +1,138 @@
+// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/**
+ @page dhcpv4Hooks The Hooks API for the DHCPv4 Server
+
+ @section dhcpv4HooksIntroduction Introduction
+ BIND10 features an API (the "Hooks" API) that allows user-written code to
+ be integrated into BIND 10 and called at specific points in its processing.
+ An overview of the API and a tutorial for writing such code can be found in
+ the @ref hooksDevelopersGuide.  Information for BIND 10 maintainers can be
+ found in the @ref hooksComponentDeveloperGuide.
+
+ This manual is more specialised and is aimed at developers of hook
+ code for the DHCPv4 server. It describes each hook point, what the callouts
+ attached to the hook are able to do, and the arguments passed to the
+ callouts.  Each entry in this manual has the following information:
+
+ - Name of the hook point.
+ - Arguments for the callout.  As well as the argument name and data type, the
+   information includes the direction, which can be one of:
+   - @b in - the server passes values to the callout but ignored any data
+     returned.
+   - @b out - the callout is expected to set this value.
+   - <b>in/out</b> - the server passes a value to the callout and uses whatever
+     value the callout sends back.  Note that the callout may choose not to
+     do any modification, in which case the server will use whatever value
+     it sent to the callout.
+ - Description of the hook. This explains where in the processing the hook
+   is located, the possible actions a callout attached to this hook could take,
+   and a description of the data passed to the callouts.
+ - Skip flag action: the action taken by the server if a callout chooses to set
+    the "skip" flag.
+
+@section dhcpv4HooksHookPoints Hooks in the DHCPv4 Server
+
+The following list is ordered by appearance of specific hook points during
+packet processing. Hook points that are not specific to packet processing
+(e.g. lease expiration) will be added to the end of this list.
+
+ @subsection dhcpv4HooksPkt4Receive pkt4_receive
+
+ - @b Arguments:
+   - name: @b query4, type: isc::dhcp::Pkt4Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed when an incoming DHCPv4
+   packet is received and its content is parsed. The sole argument -
+   query4 - contains a pointer to an isc::dhcp::Pkt4 object that contains
+   all information regarding incoming packet, including its source and
+   destination addresses, interface over which it was received, a list
+   of all options present within and relay information.  All fields of
+   the Pkt4 object can be modified at this time, except data_. (data_
+   contains the incoming packet as raw buffer. By the time this hook is
+   reached, that information has already parsed and is available though
+   other fields in the Pkt4 object.  For this reason, it doesn't make
+   sense to modify it.)
+
+ - <b>Skip flag action</b>: If any callout sets the skip flag, the server will
+   drop the packet and start processing the next one.  The reason for the drop
+   will be logged if logging is set to the appropriate debug level.
+
+@subsection dhcpv4HooksSubnet4Select subnet4_select
+
+ - @b Arguments:
+   - name: @b query4, type: isc::dhcp::Pkt4Ptr, direction: <b>in/out</b>
+   - name: @b subnet4, type: isc::dhcp::Subnet4Ptr, direction: <b>in/out</b>
+   - name: @b subnet4collection, type: const isc::dhcp::Subnet4Collection *, direction: <b>in</b>
+
+ - @b Description: this callout is executed when a subnet is being
+   selected for the incoming packet. All parameters and addresses
+   will be assigned from that subnet. A callout can select a
+   different subnet if it wishes so, the list of all subnets currently
+   configured being provided as 'subnet4collection'. The list itself must
+   not be modified.
+
+ - <b>Skip flag action</b>: If any callout installed on 'subnet4_select'
+   sets the skip flag, the server will not select any subnet. Packet processing
+   will continue, but will be severely limited (i.e. only global options
+   will be assigned).
+
+@subsection dhcpv4HooksLeaseSelect lease4_select
+
+ - @b Arguments:
+   - name: @b subnet4, type: isc::dhcp::Subnet4Ptr, direction: <b>in</b>
+   - name: @b fake_allocation, type: bool, direction: <b>in</b>
+   - name: @b lease4, type: isc::dhcp::Lease4Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed after the server engine
+   has selected a lease for client's request but before the lease
+   has been inserted into the database. Any modifications made to the
+   isc::dhcp::Lease4 object will be stored in the lease's record in the
+   database. The callout should make sure that any modifications are
+   sanity checked as the server will use that data as is with no further
+   checking.\n\n The server processes lease requests for DISCOVER and
+   REQUEST in a very similar way. The only major difference is that
+   for DISCOVER the lease is just selected, but not inserted into
+   the database.  It is possible to distinguish between DISCOVER and
+   REQUEST by checking value of the fake_allocation flag: a value of true
+   means that the lease won't be inserted into the database (DISCOVER),
+   a value of false means that it will (REQUEST).
+
+ - <b>Skip flag action</b>: If any callout installed on 'lease4_select'
+   sets the skip flag, the server will not assign any lease. Packet
+   processing will continue, but client will not get an address.
+
+@subsection dhcpv4HooksPkt4Send pkt4_send
+
+ - @b Arguments:
+   - name: @b response4, type: isc::dhcp::Pkt4Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed when server's response
+   is about to be send back to the client. The sole argument - response4 -
+   contains a pointer to an isc::dhcp::Pkt4 object that contains the
+   packet, with set source and destination addresses, interface over which
+   it will be send, list of all options and relay information.  All fields
+   of the Pkt4 object can be modified at this time, except bufferOut_.
+   (This is scratch space used for constructing the packet after all
+   pkt4_send callouts are complete, so any changes to that field will
+   be overwritten.)
+
+ - <b>Skip flag action</b>: if any callout sets the skip flag, the server
+   will drop this response packet. However, the original request packet
+   from a client was processed, so server's state was most likely changed
+   (e.g. lease was allocated). Setting this flag merely stops the change
+   being communicated to the client.
+
+*/

+ 3 - 0
src/bin/dhcp4/dhcp4_log.h

@@ -38,6 +38,9 @@ const int DBG_DHCP4_COMMAND = DBGLVL_COMMAND;
 // Trace basic operations within the code.
 const int DBG_DHCP4_BASIC = DBGLVL_TRACE_BASIC;
 
+// Trace hook related operations
+const int DBG_DHCP4_HOOKS = DBGLVL_TRACE_BASIC;
+
 // Trace detailed operations, including errors raised when processing invalid
 // packets.  (These are not logged at severities of WARN or higher for fear
 // that a set of deliberately invalid packets set to the server could overwhelm

+ 35 - 1
src/bin/dhcp4/dhcp4_messages.mes

@@ -14,6 +14,11 @@
 
 $NAMESPACE isc::dhcp
 
+% DHCP4_ACTIVATE_INTERFACE activating interface %1
+This message is printed when DHCPv4 server enabled an interface to be used
+to receive DHCPv4 traffic. IPv4 socket on this interface will be opened once
+Interface Manager starts up procedure of opening sockets.
+
 % DHCP4_CCSESSION_STARTED control channel session started on socket %1
 A debug message issued during startup after the IPv4 DHCP server has
 successfully established a session with the BIND 10 control channel.
@@ -37,7 +42,7 @@ This critical error message indicates that the initial DHCPv4
 configuration has failed. The server will start, but nothing will be
 served until the configuration has been corrected.
 
-% DHCP4_CONFIG_NEW_SUBNET A new subnet has been added to configuration: %1
+% DHCP4_CONFIG_NEW_SUBNET a new subnet has been added to configuration: %1
 This is an informational message reporting that the configuration has
 been extended to include the specified IPv4 subnet.
 
@@ -60,6 +65,30 @@ This informational message is printed every time DHCPv4 server is started
 and gives both the type and name of the database being used to store
 lease and other information.
 
+% DHCP4_DEACTIVATE_INTERFACE deactivate interface %1
+This message is printed when DHCPv4 server disables an interface from being
+used to receive DHCPv4 traffic. Sockets on this interface will not be opened
+by the Interface Manager until interface is enabled.
+
+% DHCP4_HOOK_PACKET_RCVD_SKIP received DHCPv4 packet was dropped, because a callout set the skip flag.
+This debug message is printed when a callout installed on the pkt4_receive
+hook point sets the skip flag. For this particular hook point, the
+setting of the flag instructs the server to drop the packet.
+
+% DHCP4_HOOK_PACKET_SEND_SKIP prepared DHCPv6 response was not sent, because a callout set skip flag.
+This debug message is printed when a callout installed on the pkt4_send
+hook point sets the skip flag. For this particular hook point, the setting
+of the flag instructs the server to drop the packet. This means that
+the client will not get any response, even though the server processed
+client's request and acted on it (e.g. possibly allocated a lease).
+
+% DHCP4_HOOK_SUBNET4_SELECT_SKIP no subnet was selected, because a callout set skip flag.
+This debug message is printed when a callout installed on the
+subnet4_select hook point sets the skip flag. For this particular hook
+point, the setting of the flag instructs the server not to choose a
+subnet, an action that severely limits further processing; the server
+will be only able to offer global options - no addresses will be assigned.
+
 % DHCP4_LEASE_ADVERT lease %1 advertised (client client-id %2, hwaddr %3)
 This debug message indicates that the server successfully advertised
 a lease. It is up to the client to choose one server out of othe advertised
@@ -82,6 +111,11 @@ specified client after receiving a REQUEST message from it.  There are many
 possible reasons for such a failure. Additional messages will indicate the
 reason.
 
+% DHCP4_NO_SOCKETS_OPEN no interface configured to listen to DHCP traffic
+This warning message is issued when current server configuration specifies
+no interfaces that server should listen on, or specified interfaces are not
+configured to receive the traffic.
+
 % DHCP4_NOT_RUNNING IPv4 DHCP server is not running
 A warning message is issued when an attempt is made to shut down the
 IPv4 DHCP server but it is not running.

+ 230 - 8
src/bin/dhcp4/dhcp4_srv.cc

@@ -30,6 +30,8 @@
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/utils.h>
 #include <dhcpsrv/addr_utilities.h>
+#include <hooks/hooks_manager.h>
+#include <hooks/callout_handle.h>
 
 #include <boost/algorithm/string/erase.hpp>
 
@@ -39,9 +41,30 @@
 using namespace isc;
 using namespace isc::asiolink;
 using namespace isc::dhcp;
+using namespace isc::hooks;
 using namespace isc::log;
 using namespace std;
 
+/// Structure that holds registered hook indexes
+struct Dhcp6Hooks {
+    int hook_index_pkt4_receive_;   ///< index for "pkt4_receive" hook point
+    int hook_index_subnet4_select_; ///< index for "subnet4_select" hook point
+    int hook_index_pkt4_send_;      ///< index for "pkt4_send" hook point
+
+    /// Constructor that registers hook points for DHCPv6 engine
+    Dhcp6Hooks() {
+        hook_index_pkt4_receive_   = HooksManager::registerHook("pkt4_receive");
+        hook_index_subnet4_select_ = HooksManager::registerHook("subnet4_select");
+        hook_index_pkt4_send_      = HooksManager::registerHook("pkt4_send");
+    }
+};
+
+// Declare a Hooks object. As this is outside any function or method, it
+// will be instantiated (and the constructor run) when the module is loaded.
+// As a result, the hook indexes will be defined before any method in this
+// module is called.
+Dhcp6Hooks Hooks;
+
 namespace isc {
 namespace dhcp {
 
@@ -58,7 +81,11 @@ static const char* SERVER_ID_FILE = "b10-dhcp4-serverid";
 // grants those options and a single, fixed, hardcoded lease.
 
 Dhcpv4Srv::Dhcpv4Srv(uint16_t port, const char* dbconfig, const bool use_bcast,
-                     const bool direct_response_desired) {
+                     const bool direct_response_desired)
+: serverid_(), shutdown_(true), alloc_engine_(), port_(port), 
+    use_bcast_(use_bcast), hook_index_pkt4_receive_(-1), 
+    hook_index_subnet4_select_(-1), hook_index_pkt4_send_(-1) {
+
     LOG_DEBUG(dhcp4_logger, DBG_DHCP4_START, DHCP4_OPEN_SOCKET).arg(port);
     try {
         // First call to instance() will create IfaceMgr (it's a singleton)
@@ -73,7 +100,7 @@ Dhcpv4Srv::Dhcpv4Srv(uint16_t port, const char* dbconfig, const bool use_bcast,
         if (port) {
             // open sockets only if port is non-zero. Port 0 is used
             // for non-socket related testing.
-            IfaceMgr::instance().openSockets4(port, use_bcast);
+            IfaceMgr::instance().openSockets4(port_, use_bcast_);
         }
 
         string srvid_file = CfgMgr::instance().getDataDir() + "/" + string(SERVER_ID_FILE);
@@ -103,6 +130,13 @@ Dhcpv4Srv::Dhcpv4Srv(uint16_t port, const char* dbconfig, const bool use_bcast,
         // Instantiate allocation engine
         alloc_engine_.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100));
 
+        // Register hook points
+        hook_index_pkt4_receive_   = Hooks.hook_index_pkt4_receive_;
+        hook_index_subnet4_select_ = Hooks.hook_index_subnet4_select_;
+        hook_index_pkt4_send_      = Hooks.hook_index_pkt4_send_;
+
+        /// @todo call loadLibraries() when handling configuration changes
+
     } catch (const std::exception &e) {
         LOG_ERROR(dhcp4_logger, DHCP4_SRV_CONSTRUCT_ERROR).arg(e.what());
         shutdown_ = true;
@@ -122,6 +156,16 @@ Dhcpv4Srv::shutdown() {
     shutdown_ = true;
 }
 
+Pkt4Ptr
+Dhcpv4Srv::receivePacket(int timeout) {
+    return (IfaceMgr::instance().receive4(timeout));
+}
+
+void
+Dhcpv4Srv::sendPacket(const Pkt4Ptr& packet) {
+    IfaceMgr::instance().send(packet);
+}
+
 bool
 Dhcpv4Srv::run() {
     while (!shutdown_) {
@@ -134,7 +178,7 @@ Dhcpv4Srv::run() {
         Pkt4Ptr rsp;
 
         try {
-            query = IfaceMgr::instance().receive4(timeout);
+            query = receivePacket(timeout);
         } catch (const std::exception& e) {
             LOG_ERROR(dhcp4_logger, DHCP4_PACKET_RECEIVE_FAIL).arg(e.what());
         }
@@ -154,8 +198,34 @@ Dhcpv4Srv::run() {
                       .arg(query->getType())
                       .arg(query->getIface());
             LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_QUERY_DATA)
+                      .arg(static_cast<int>(query->getType()))
                       .arg(query->toText());
 
+            // Let's execute all callouts registered for packet_received
+            if (HooksManager::calloutsPresent(hook_index_pkt4_receive_)) {
+                CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+                // Delete previously set arguments
+                callout_handle->deleteAllArguments();
+
+                // Pass incoming packet as argument
+                callout_handle->setArgument("query4", query);
+
+                // Call callouts
+                HooksManager::callCallouts(hook_index_pkt4_receive_,
+                                           *callout_handle);
+
+                // Callouts decided to skip the next processing step. The next
+                // processing step would to process the packet, so skip at this
+                // stage means drop.
+                if (callout_handle->getSkip()) {
+                    LOG_DEBUG(dhcp4_logger, DBG_DHCP4_HOOKS, DHCP4_HOOK_PACKET_RCVD_SKIP);
+                    continue;
+                }
+
+                callout_handle->getArgument("query4", query);
+            }
+
             try {
                 switch (query->getType()) {
                 case DHCPDISCOVER:
@@ -220,13 +290,39 @@ Dhcpv4Srv::run() {
                 rsp->setIface(query->getIface());
                 rsp->setIndex(query->getIndex());
 
+                // Execute all callouts registered for packet6_send
+                if (HooksManager::calloutsPresent(hook_index_pkt4_send_)) {
+                    CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+                    // Delete all previous arguments
+                    callout_handle->deleteAllArguments();
+
+                    // Clear skip flag if it was set in previous callouts
+                    callout_handle->setSkip(false);
+
+                    // Set our response
+                    callout_handle->setArgument("response4", rsp);
+
+                    // Call all installed callouts
+                    HooksManager::callCallouts(hook_index_pkt4_send_,
+                                               *callout_handle);
+
+                    // Callouts decided to skip the next processing step. The next
+                    // processing step would to send the packet, so skip at this
+                    // stage means "drop response".
+                    if (callout_handle->getSkip()) {
+                        LOG_DEBUG(dhcp4_logger, DBG_DHCP4_HOOKS, DHCP4_HOOK_PACKET_SEND_SKIP);
+                        continue;
+                    }
+                }
+
                 LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA,
                           DHCP4_RESPONSE_DATA)
                           .arg(rsp->getType()).arg(rsp->toText());
 
                 if (rsp->pack()) {
                     try {
-                        IfaceMgr::instance().send(rsp);
+                        sendPacket(rsp);
                     } catch (const std::exception& e) {
                         LOG_ERROR(dhcp4_logger, DHCP4_PACKET_SEND_FAIL).arg(e.what());
                     }
@@ -514,12 +610,15 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
     // allocation.
     bool fake_allocation = (question->getType() == DHCPDISCOVER);
 
+    CalloutHandlePtr callout_handle = getCalloutHandle(question);
+
     // Use allocation engine to pick a lease for this client. Allocation engine
     // will try to honour the hint, but it is just a hint - some other address
     // may be used instead. If fake_allocation is set to false, the lease will
     // be inserted into the LeaseMgr as well.
     Lease4Ptr lease = alloc_engine_->allocateAddress4(subnet, client_id, hwaddr,
-                                                      hint, fake_allocation);
+                                                      hint, fake_allocation,
+                                                      callout_handle);
 
     if (lease) {
         // We have a lease! Let's set it in the packet and send it back to
@@ -632,6 +731,9 @@ Dhcpv4Srv::getNetmaskOption(const Subnet4Ptr& subnet) {
 
 Pkt4Ptr
 Dhcpv4Srv::processDiscover(Pkt4Ptr& discover) {
+
+    sanityCheck(discover, FORBIDDEN);
+
     Pkt4Ptr offer = Pkt4Ptr
         (new Pkt4(DHCPOFFER, discover->getTransid()));
 
@@ -782,17 +884,50 @@ Dhcpv4Srv::serverReceivedPacketName(uint8_t type) {
 Subnet4Ptr
 Dhcpv4Srv::selectSubnet(const Pkt4Ptr& question) {
 
+    Subnet4Ptr subnet;
     // Is this relayed message?
     IOAddress relay = question->getGiaddr();
-    if (relay.toText() == "0.0.0.0") {
+    static const IOAddress notset("0.0.0.0");
 
+    if (relay != notset) {
         // Yes: Use relay address to select subnet
-        return (CfgMgr::instance().getSubnet4(relay));
+        subnet = CfgMgr::instance().getSubnet4(relay);
     } else {
 
         // No: Use client's address to select subnet
-        return (CfgMgr::instance().getSubnet4(question->getRemoteAddr()));
+        subnet = CfgMgr::instance().getSubnet4(question->getRemoteAddr());
     }
+
+    /// @todo Implement getSubnet4(interface-name)
+
+    // Let's execute all callouts registered for packet_received
+    if (HooksManager::calloutsPresent(hook_index_subnet4_select_)) {
+        CalloutHandlePtr callout_handle = getCalloutHandle(question);
+
+        // We're reusing callout_handle from previous calls
+        callout_handle->deleteAllArguments();
+
+        // Set new arguments
+        callout_handle->setArgument("query4", question);
+        callout_handle->setArgument("subnet4", subnet);
+        callout_handle->setArgument("subnet4collection", CfgMgr::instance().getSubnets4());
+
+        // Call user (and server-side) callouts
+        HooksManager::callCallouts(hook_index_subnet4_select_, *callout_handle);
+
+        // Callouts decided to skip this step. This means that no subnet will be
+        // selected. Packet processing will continue, but it will be severly limited
+        // (i.e. only global options will be assigned)
+        if (callout_handle->getSkip()) {
+            LOG_DEBUG(dhcp4_logger, DBG_DHCP4_HOOKS, DHCP4_HOOK_SUBNET4_SELECT_SKIP);
+            return (Subnet4Ptr());
+        }
+
+        // Use whatever subnet was specified by the callout
+        callout_handle->getArgument("subnet4", subnet);
+    }
+
+    return (subnet);
 }
 
 void
@@ -818,7 +953,94 @@ Dhcpv4Srv::sanityCheck(const Pkt4Ptr& pkt, RequirementLevel serverid) {
         // do nothing here
         ;
     }
+
+    // If there is HWAddress set and it is non-empty, then we're good
+    if (pkt->getHWAddr() && !pkt->getHWAddr()->hwaddr_.empty()) {
+        return;
+    }
+
+    // There has to be something to uniquely identify the client:
+    // either non-zero MAC address or client-id option present (or both)
+    OptionPtr client_id = pkt->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
+
+    // If there's no client-id (or a useless one is provided, i.e. 0 length)
+    if (!client_id || client_id->len() == client_id->getHeaderLen()) {
+        isc_throw(RFCViolation, "Missing or useless client-id and no HW address "
+                  " provided in message "
+                  << serverReceivedPacketName(pkt->getType()));
+    }
 }
 
+isc::hooks::CalloutHandlePtr Dhcpv4Srv::getCalloutHandle(const Pkt4Ptr& pkt) {
+    // This method returns a CalloutHandle for a given packet. It is guaranteed
+    // to return the same callout_handle (so user library contexts are
+    // preserved). This method works well if the server processes one packet
+    // at a time. Once the server architecture is extended to cover parallel
+    // packets processing (e.g. delayed-ack, some form of buffering etc.), this
+    // method has to be extended (e.g. store callouts in a map and use pkt as
+    // a key). Additional code would be required to release the callout handle
+    // once the server finished processing.
+
+    CalloutHandlePtr callout_handle;
+    static Pkt4Ptr old_pointer;
+
+    if (!callout_handle ||
+        old_pointer != pkt) {
+        // This is the first packet or a different packet than previously
+        // passed to getCalloutHandle()
+
+        // Remember the pointer to this packet
+        old_pointer = pkt;
+
+        callout_handle = HooksManager::createCalloutHandle();
+    }
+
+    return (callout_handle);
+}
+
+void
+Dhcpv4Srv::openActiveSockets(const uint16_t port,
+                             const bool use_bcast) {
+    IfaceMgr::instance().closeSockets();
+
+    // Get the reference to the collection of interfaces. This reference should
+    // be valid as long as the program is run because IfaceMgr is a singleton.
+    // Therefore we can safely iterate over instances of all interfaces and
+    // modify their flags. Here we modify flags which indicate whether socket
+    // should be open for a particular interface or not.
+    const IfaceMgr::IfaceCollection& ifaces = IfaceMgr::instance().getIfaces();
+    for (IfaceMgr::IfaceCollection::const_iterator iface = ifaces.begin();
+         iface != ifaces.end(); ++iface) {
+        Iface* iface_ptr = IfaceMgr::instance().getIface(iface->getName());
+        if (iface_ptr == NULL) {
+            isc_throw(isc::Unexpected, "Interface Manager returned NULL"
+                      << " instance of the interface when DHCPv4 server was"
+                      << " trying to reopen sockets after reconfiguration");
+        }
+        if (CfgMgr::instance().isActiveIface(iface->getName())) {
+            iface_ptr->inactive4_ = false;
+            LOG_INFO(dhcp4_logger, DHCP4_ACTIVATE_INTERFACE)
+                .arg(iface->getFullName());
+
+        } else {
+            // For deactivating interface, it should be sufficient to log it
+            // on the debug level because it is more useful to know what
+            // interface is activated which is logged on the info level.
+            LOG_DEBUG(dhcp4_logger, DBG_DHCP4_BASIC,
+                      DHCP4_DEACTIVATE_INTERFACE).arg(iface->getName());
+            iface_ptr->inactive4_ = true;
+
+        }
+    }
+    // Let's reopen active sockets. openSockets4 will check internally whether
+    // sockets are marked active or inactive.
+    // @todo Optimization: we should not reopen all sockets but rather select
+    // those that have been affected by the new configuration.
+    if (!IfaceMgr::instance().openSockets4(port, use_bcast)) {
+        LOG_WARN(dhcp4_logger, DHCP4_NO_SOCKETS_OPEN);
+    }
+}
+
+
 }   // namespace dhcp
 }   // namespace isc

+ 68 - 1
src/bin/dhcp4/dhcp4_srv.h

@@ -20,6 +20,7 @@
 #include <dhcp/option.h>
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/alloc_engine.h>
+#include <hooks/callout_handle.h>
 
 #include <boost/noncopyable.hpp>
 
@@ -79,7 +80,7 @@ public:
               const bool direct_response_desired = true);
 
     /// @brief Destructor. Used during DHCPv4 service shutdown.
-    ~Dhcpv4Srv();
+    virtual ~Dhcpv4Srv();
 
     /// @brief Main server processing loop.
     ///
@@ -113,6 +114,46 @@ public:
     ///         be freed by the caller.
     static const char* serverReceivedPacketName(uint8_t type);
 
+    ///
+    /// @name Public accessors returning values required to (re)open sockets.
+    ///
+    /// These accessors must be public because sockets are reopened from the
+    /// static configuration callback handler. This callback handler invokes
+    /// @c ControlledDhcpv4Srv::openActiveSockets which requires parameters
+    /// which has to be retrieved from the @c ControlledDhcpv4Srv object.
+    /// They are retrieved using these public functions
+    //@{
+    ///
+    /// @brief Get UDP port on which server should listen.
+    ///
+    /// Typically, server listens on UDP port number 67. Other ports are used
+    /// for testing purposes only.
+    ///
+    /// @return UDP port on which server should listen.
+    uint16_t getPort() const {
+        return (port_);
+    }
+
+    /// @brief Return bool value indicating that broadcast flags should be set
+    /// on sockets.
+    ///
+    /// @return A bool value indicating that broadcast should be used (if true).
+    bool useBroadcast() const {
+        return (use_bcast_);
+    }
+    //@}
+
+    /// @brief Open sockets which are marked as active in @c CfgMgr.
+    ///
+    /// This function reopens sockets according to the current settings in the
+    /// Configuration Manager. It holds the list of the interfaces which server
+    /// should listen on. This function will open sockets on these interfaces
+    /// only. This function is not exception safe.
+    ///
+    /// @param port UDP port on which server should listen.
+    /// @param use_bcast should broadcast flags be set on the sockets.
+    static void openActiveSockets(const uint16_t port, const bool use_bcast);
+
 protected:
 
     /// @brief verifies if specified packet meets RFC requirements
@@ -296,6 +337,18 @@ protected:
     /// initiate server shutdown procedure.
     volatile bool shutdown_;
 
+    /// @brief dummy wrapper around IfaceMgr::receive4
+    ///
+    /// This method is useful for testing purposes, where its replacement
+    /// simulates reception of a packet. For that purpose it is protected.
+    virtual Pkt4Ptr receivePacket(int timeout);
+
+    /// @brief dummy wrapper around IfaceMgr::send()
+    ///
+    /// This method is useful for testing purposes, where its replacement
+    /// simulates transmission of a packet. For that purpose it is protected.
+    virtual void sendPacket(const Pkt4Ptr& pkt);
+
 private:
 
     /// @brief Constructs netmask option based on subnet4
@@ -310,6 +363,20 @@ private:
     /// during normal operation (e.g. to use different allocators)
     boost::shared_ptr<AllocEngine> alloc_engine_;
 
+    uint16_t port_;  ///< UDP port number on which server listens.
+    bool use_bcast_; ///< Should broadcast be enabled on sockets (if true).
+
+    /// @brief returns callout handle for specified packet
+    ///
+    /// @param pkt packet for which the handle should be returned
+    ///
+    /// @return a callout handle to be used in hooks related to said packet
+    isc::hooks::CalloutHandlePtr getCalloutHandle(const Pkt4Ptr& pkt);
+
+    /// Indexes for registered hook points
+    int hook_index_pkt4_receive_;
+    int hook_index_subnet4_select_;
+    int hook_index_pkt4_send_;
 };
 
 }; // namespace isc::dhcp

+ 1 - 0
src/bin/dhcp4/tests/Makefile.am

@@ -71,6 +71,7 @@ dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
 endif
 
 noinst_PROGRAMS = $(TESTS)

+ 82 - 17
src/bin/dhcp4/tests/config_parser_unittest.cc

@@ -51,11 +51,12 @@ public:
         // deal with sockets here, just check if configuration handling
         // is sane.
         srv_.reset(new Dhcpv4Srv(0));
+        CfgMgr::instance().deleteActiveIfaces();
     }
 
     // Checks if global parameter of name have expected_value
     void checkGlobalUint32(string name, uint32_t expected_value) {
-        const Uint32StoragePtr uint32_defaults = 
+        const Uint32StoragePtr uint32_defaults =
                                         globalContext()->uint32_values_;
         try {
             uint32_t actual_value = uint32_defaults->getParam(name);
@@ -138,7 +139,7 @@ public:
     /// describing an option.
     std::string createConfigWithOption(const std::map<std::string, std::string>& params) {
         std::ostringstream stream;
-        stream << "{ \"interface\": [ \"all\" ],"
+        stream << "{ \"interfaces\": [ \"*\" ],"
             "\"rebind-timer\": 2000, "
             "\"renew-timer\": 1000, "
             "\"subnet4\": [ { "
@@ -245,7 +246,7 @@ public:
     void resetConfiguration() {
         ConstElementPtr status;
 
-        string config = "{ \"interface\": [ \"all\" ],"
+        string config = "{ \"interfaces\": [ \"*\" ],"
             "\"rebind-timer\": 2000, "
             "\"renew-timer\": 1000, "
             "\"valid-lifetime\": 4000, "
@@ -322,7 +323,7 @@ TEST_F(Dhcp4ParserTest, emptySubnet) {
     ConstElementPtr status;
 
     EXPECT_NO_THROW(status = configureDhcp4Server(*srv_,
-                    Element::fromJSON("{ \"interface\": [ \"all\" ],"
+                    Element::fromJSON("{ \"interfaces\": [ \"*\" ],"
                                       "\"rebind-timer\": 2000, "
                                       "\"renew-timer\": 1000, "
                                       "\"subnet4\": [  ], "
@@ -342,7 +343,7 @@ TEST_F(Dhcp4ParserTest, subnetGlobalDefaults) {
 
     ConstElementPtr status;
 
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
         "\"subnet4\": [ { "
@@ -372,7 +373,7 @@ TEST_F(Dhcp4ParserTest, subnetLocal) {
 
     ConstElementPtr status;
 
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
         "\"subnet4\": [ { "
@@ -403,7 +404,7 @@ TEST_F(Dhcp4ParserTest, poolOutOfSubnet) {
 
     ConstElementPtr status;
 
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
         "\"subnet4\": [ { "
@@ -427,7 +428,7 @@ TEST_F(Dhcp4ParserTest, poolPrefixLen) {
 
     ConstElementPtr status;
 
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
         "\"subnet4\": [ { "
@@ -949,7 +950,7 @@ TEST_F(Dhcp4ParserTest, optionStandardDefOverride) {
 // configuration does not include options configuration.
 TEST_F(Dhcp4ParserTest, optionDataDefaults) {
     ConstElementPtr x;
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000,"
         "\"renew-timer\": 1000,"
         "\"option-data\": [ {"
@@ -1022,7 +1023,7 @@ TEST_F(Dhcp4ParserTest, optionDataTwoSpaces) {
     // The definition is not required for the option that
     // belongs to the 'dhcp4' option space as it is the
     // standard option.
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000,"
         "\"renew-timer\": 1000,"
         "\"option-data\": [ {"
@@ -1100,7 +1101,7 @@ TEST_F(Dhcp4ParserTest, optionDataEncapsulate) {
     // at the very end (when all other parameters are configured).
 
     // Starting stage 1. Configure sub-options and their definitions.
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000,"
         "\"renew-timer\": 1000,"
         "\"option-data\": [ {"
@@ -1149,7 +1150,7 @@ TEST_F(Dhcp4ParserTest, optionDataEncapsulate) {
     // the configuration from the stage 2 is repeated because BIND
     // configuration manager sends whole configuration for the lists
     // where at least one element is being modified or added.
-    config = "{ \"interface\": [ \"all\" ],"
+    config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000,"
         "\"renew-timer\": 1000,"
         "\"option-data\": [ {"
@@ -1245,7 +1246,7 @@ TEST_F(Dhcp4ParserTest, optionDataEncapsulate) {
 // option setting.
 TEST_F(Dhcp4ParserTest, optionDataInSingleSubnet) {
     ConstElementPtr x;
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
         "\"option-data\": [ {"
@@ -1317,7 +1318,7 @@ TEST_F(Dhcp4ParserTest, optionDataInSingleSubnet) {
 // for multiple subnets.
 TEST_F(Dhcp4ParserTest, optionDataInMultipleSubnets) {
     ConstElementPtr x;
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
         "\"subnet4\": [ { "
@@ -1597,7 +1598,7 @@ TEST_F(Dhcp4ParserTest, stdOptionDataEncapsulate) {
     // In the first stahe we create definitions of suboptions
     // that we will add to the base option.
     // Let's create some dummy options: foo and foo2.
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000,"
         "\"renew-timer\": 1000,"
         "\"option-data\": [ {"
@@ -1650,7 +1651,7 @@ TEST_F(Dhcp4ParserTest, stdOptionDataEncapsulate) {
     // We add our dummy options to this option space and thus
     // they should be included as sub-options in the 'vendor-opts'
     // option.
-    config = "{ \"interface\": [ \"all\" ],"
+    config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000,"
         "\"renew-timer\": 1000,"
         "\"option-data\": [ {"
@@ -1749,5 +1750,69 @@ TEST_F(Dhcp4ParserTest, stdOptionDataEncapsulate) {
     EXPECT_FALSE(desc.option->getOption(3));
 }
 
+// This test verifies that it is possible to select subset of interfaces
+// on which server should listen.
+TEST_F(Dhcp4ParserTest, selectedInterfaces) {
+    ConstElementPtr x;
+    string config = "{ \"interfaces\": [ \"eth0\", \"eth1\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"valid-lifetime\": 4000 }";
 
-};
+    ElementPtr json = Element::fromJSON(config);
+
+    ConstElementPtr status;
+
+    // Make sure the config manager is clean and there is no hanging
+    // interface configuration.
+    ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth0"));
+    ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth1"));
+    ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth2"));
+
+    // Apply configuration.
+    EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+    ASSERT_TRUE(status);
+    checkResult(status, 0);
+
+    // eth0 and eth1 were explicitly selected. eth2 was not.
+    EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth0"));
+    EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth1"));
+    EXPECT_FALSE(CfgMgr::instance().isActiveIface("eth2"));
+}
+
+// This test verifies that it is possible to configure the server in such a way
+// that it listens on all interfaces.
+TEST_F(Dhcp4ParserTest, allInterfaces) {
+    ConstElementPtr x;
+    // This configuration specifies two interfaces on which server should listen
+    // but it also includes asterisk. The asterisk switches server into the
+    // mode when it listens on all interfaces regardless of what interface names
+    // were specified in the "interfaces" parameter.
+    string config = "{ \"interfaces\": [ \"eth0\", \"*\", \"eth1\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json = Element::fromJSON(config);
+
+    ConstElementPtr status;
+
+    // Make sure there is no old configuration.
+    ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth0"));
+    ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth1"));
+    ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth2"));
+
+    // Apply configuration.
+    EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+    ASSERT_TRUE(status);
+    checkResult(status, 0);
+
+    // All interfaces should be now active.
+    EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth0"));
+    EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth1"));
+    EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth2"));
+}
+
+
+
+}

+ 818 - 6
src/bin/dhcp4/tests/dhcp4_srv_unittest.cc

@@ -16,6 +16,7 @@
 #include <sstream>
 
 #include <asiolink/io_address.h>
+#include <config/ccsession.h>
 #include <dhcp/dhcp4.h>
 #include <dhcp/iface_mgr.h>
 #include <dhcp/option.h>
@@ -26,11 +27,15 @@
 #include <dhcp/pkt_filter_inet.h>
 #include <dhcp4/dhcp4_srv.h>
 #include <dhcp4/dhcp4_log.h>
+#include <dhcp4/config_parser.h>
+#include <hooks/server_hooks.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcpsrv/utils.h>
 #include <gtest/gtest.h>
+#include <hooks/server_hooks.h>
+#include <hooks/hooks_manager.h>
 
 #include <boost/scoped_ptr.hpp>
 
@@ -42,7 +47,9 @@
 using namespace std;
 using namespace isc;
 using namespace isc::dhcp;
+using namespace isc::data;
 using namespace isc::asiolink;
+using namespace isc::hooks;
 
 namespace {
 
@@ -79,6 +86,55 @@ public:
         : Dhcpv4Srv(port, "type=memfile", false, false) {
     }
 
+    /// @brief fakes packet reception
+    /// @param timeout ignored
+    ///
+    /// The method receives all packets queued in receive queue, one after
+    /// another. Once the queue is empty, it initiates the shutdown procedure.
+    ///
+    /// See fake_received_ field for description
+    virtual Pkt4Ptr receivePacket(int /*timeout*/) {
+
+        // If there is anything prepared as fake incoming traffic, use it
+        if (!fake_received_.empty()) {
+            Pkt4Ptr pkt = fake_received_.front();
+            fake_received_.pop_front();
+            return (pkt);
+        }
+
+        // If not, just trigger shutdown and return immediately
+        shutdown();
+        return (Pkt4Ptr());
+    }
+
+    /// @brief fake packet sending
+    ///
+    /// Pretend to send a packet, but instead just store it in fake_send_ list
+    /// where test can later inspect server's response.
+    virtual void sendPacket(const Pkt4Ptr& pkt) {
+        fake_sent_.push_back(pkt);
+    }
+
+    /// @brief adds a packet to fake receive queue
+    ///
+    /// See fake_received_ field for description
+    void fakeReceive(const Pkt4Ptr& pkt) {
+        fake_received_.push_back(pkt);
+    }
+
+    virtual ~NakedDhcpv4Srv() {
+    }
+
+    /// @brief packets we pretend to receive
+    ///
+    /// Instead of setting up sockets on interfaces that change between OSes, it
+    /// is much easier to fake packet reception. This is a list of packets that
+    /// we pretend to have received. You can schedule new packets to be received
+    /// using fakeReceive() and NakedDhcpv4Srv::receivePacket() methods.
+    list<Pkt4Ptr> fake_received_;
+
+    list<Pkt4Ptr> fake_sent_;
+
     using Dhcpv4Srv::adjustRemoteAddr;
     using Dhcpv4Srv::processDiscover;
     using Dhcpv4Srv::processRequest;
@@ -97,11 +153,11 @@ static const char* SRVID_FILE = "server-id-test.txt";
 
 /// @brief Dummy Packet Filtering class.
 ///
-/// This class reports capability to respond directly to the
-/// client which doesn't have address configured yet.
+/// This class reports capability to respond directly to the client which
+/// doesn't have address configured yet.
 ///
-/// All packet and socket handling functions do nothing because
-/// they are not used in unit tests.
+/// All packet and socket handling functions do nothing because they are not
+/// used in unit tests.
 class PktFilterTest : public PktFilter {
 public:
 
@@ -137,7 +193,9 @@ public:
     ///
     /// Initializes common objects used in many tests.
     /// Also sets up initial configuration in CfgMgr.
-    Dhcpv4SrvTest() {
+    Dhcpv4SrvTest() :
+        rcode_(0)
+    {
         subnet_ = Subnet4Ptr(new Subnet4(IOAddress("192.0.2.0"), 24, 1000,
                                          2000, 3000));
         pool_ = Pool4Ptr(new Pool4(IOAddress("192.0.2.100"), IOAddress("192.0.2.110")));
@@ -153,6 +211,19 @@ public:
 
         // it's ok if that fails. There should not be such a file anyway
         unlink(SRVID_FILE);
+
+        const IfaceMgr::IfaceCollection& ifaces = IfaceMgr::instance().getIfaces();
+
+        // There must be some interface detected
+        if (ifaces.empty()) {
+            // We can't use ASSERT in constructor
+            ADD_FAILURE() << "No interfaces detected.";
+        }
+
+        valid_iface_ = ifaces.begin()->getName();
+    }
+
+    virtual ~Dhcpv4SrvTest() {
     }
 
     /// @brief Add 'Parameter Request List' option to the packet.
@@ -561,6 +632,13 @@ public:
 
     /// @brief A client-id used in most tests
     ClientIdPtr client_id_;
+
+    int rcode_;
+
+    ConstElementPtr comment_;
+
+    // Name of a valid network interface
+    string valid_iface_;
 };
 
 // Sanity check. Verifies that both Dhcpv4Srv and its derived
@@ -964,6 +1042,7 @@ TEST_F(Dhcpv4SrvTest, DiscoverNoClientId) {
     Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
     dis->setRemoteAddr(IOAddress("192.0.2.1"));
     dis->setYiaddr(hint);
+    dis->setHWAddr(generateHWAddr(6));
 
     // Pass it to the server and get an offer
     Pkt4Ptr offer = srv->processDiscover(dis);
@@ -1325,8 +1404,9 @@ TEST_F(Dhcpv4SrvTest, sanityCheck) {
     ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
 
     Pkt4Ptr pkt = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+    pkt->setHWAddr(generateHWAddr(6));
 
-    // Client-id is optional for information-request, so
+    // Server-id is optional for information-request, so
     EXPECT_NO_THROW(srv->sanityCheck(pkt, Dhcpv4Srv::OPTIONAL));
 
     // Empty packet, no server-id
@@ -1340,6 +1420,11 @@ TEST_F(Dhcpv4SrvTest, sanityCheck) {
     // Server-id is forbidden, but present => exception
     EXPECT_THROW(srv->sanityCheck(pkt, Dhcpv4Srv::FORBIDDEN),
                  RFCViolation);
+
+    // There's no client-id and no HWADDR. Server needs something to
+    // identify the client
+    pkt->setHWAddr(generateHWAddr(0));
+    EXPECT_THROW(srv->sanityCheck(pkt, Dhcpv4Srv::MANDATORY), RFCViolation);
 }
 
 // This test verifies that incoming (positive) RELEASE can be handled properly.
@@ -1535,4 +1620,731 @@ TEST_F(Dhcpv4SrvTest, ServerID) {
     EXPECT_EQ(srvid_text, text);
 }
 
+/// @todo Implement tests for subnetSelect See tests in dhcp6_srv_unittest.cc:
+/// selectSubnetAddr, selectSubnetIface, selectSubnetRelayLinkaddr,
+/// selectSubnetRelayInterfaceId. Note that the concept of interface-id is not
+/// present in the DHCPv4, so not everything is applicable directly.
+/// See ticket #3057
+
+// Checks if hooks are registered properly.
+TEST_F(Dhcpv4SrvTest, Hooks) {
+    NakedDhcpv4Srv srv(0);
+
+    // check if appropriate hooks are registered
+    int hook_index_pkt4_received = -1;
+    int hook_index_select_subnet = -1;
+    int hook_index_pkt4_send     = -1;
+
+    // check if appropriate indexes are set
+    EXPECT_NO_THROW(hook_index_pkt4_received = ServerHooks::getServerHooks()
+                    .getIndex("pkt4_receive"));
+    EXPECT_NO_THROW(hook_index_select_subnet = ServerHooks::getServerHooks()
+                    .getIndex("subnet4_select"));
+    EXPECT_NO_THROW(hook_index_pkt4_send     = ServerHooks::getServerHooks()
+                    .getIndex("pkt4_send"));
+
+    EXPECT_TRUE(hook_index_pkt4_received > 0);
+    EXPECT_TRUE(hook_index_select_subnet > 0);
+    EXPECT_TRUE(hook_index_pkt4_send > 0);
+}
+
+    // a dummy MAC address
+    const uint8_t dummyMacAddr[] = {0, 1, 2, 3, 4, 5};
+
+    // A dummy MAC address, padded with 0s
+    const uint8_t dummyChaddr[16] = {0, 1, 2, 3, 4, 5, 0, 0,
+                                     0, 0, 0, 0, 0, 0, 0, 0 };
+
+    // Let's use some creative test content here (128 chars + \0)
+    const uint8_t dummyFile[] = "Lorem ipsum dolor sit amet, consectetur "
+        "adipiscing elit. Proin mollis placerat metus, at "
+        "lacinia orci ornare vitae. Mauris amet.";
+
+    // Yet another type of test content (64 chars + \0)
+    const uint8_t dummySname[] = "Lorem ipsum dolor sit amet, consectetur "
+        "adipiscing elit posuere.";
+
+/// @brief a class dedicated to Hooks testing in DHCPv4 server
+///
+/// This class has a number of static members, because each non-static
+/// method has implicit 'this' parameter, so it does not match callout
+/// signature and couldn't be registered. Furthermore, static methods
+/// can't modify non-static members (for obvious reasons), so many
+/// fields are declared static. It is still better to keep them as
+/// one class rather than unrelated collection of global objects.
+class HooksDhcpv4SrvTest : public Dhcpv4SrvTest {
+
+public:
+
+    /// @brief creates Dhcpv4Srv and prepares buffers for callouts
+    HooksDhcpv4SrvTest() {
+
+        // Allocate new DHCPv6 Server
+        srv_ = new NakedDhcpv4Srv(0);
+
+        // clear static buffers
+        resetCalloutBuffers();
+    }
+
+    /// @brief destructor (deletes Dhcpv4Srv)
+    virtual ~HooksDhcpv4SrvTest() {
+
+        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("pkt4_receive");
+        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("pkt4_send");
+        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("subnet4_select");
+
+        delete srv_;
+    }
+
+    /// @brief creates an option with specified option code
+    ///
+    /// This method is static, because it is used from callouts
+    /// that do not have a pointer to HooksDhcpv4SSrvTest object
+    ///
+    /// @param option_code code of option to be created
+    ///
+    /// @return pointer to create option object
+    static OptionPtr createOption(uint16_t option_code) {
+
+        char payload[] = {
+            0xa, 0xb, 0xc, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14
+        };
+
+        OptionBuffer tmp(payload, payload + sizeof(payload));
+        return OptionPtr(new Option(Option::V4, option_code, tmp));
+    }
+
+    /// @brief Generates test packet.
+    ///
+    /// Allocates and generates on-wire buffer that represents test packet, with all
+    /// fixed fields set to non-zero values.  Content is not always reasonable.
+    ///
+    /// See generateTestPacket1() function that returns exactly the same packet as
+    /// Pkt4 object.
+    ///
+    /// @return pointer to allocated Pkt4 object
+    // Returns a vector containing a DHCPv4 packet header.
+    Pkt4Ptr
+    generateSimpleDiscover() {
+
+        // That is only part of the header. It contains all "short" fields,
+        // larger fields are constructed separately.
+        uint8_t hdr[] = {
+            1, 6, 6, 13,            // op, htype, hlen, hops,
+            0x12, 0x34, 0x56, 0x78, // transaction-id
+            0, 42, 0x80, 0x00,      // 42 secs, BROADCAST flags
+            192, 0, 2, 1,           // ciaddr
+            1, 2, 3, 4,             // yiaddr
+            192, 0, 2, 255,         // siaddr
+            255, 255, 255, 255,     // giaddr
+        };
+
+        // Initialize the vector with the header fields defined above.
+        vector<uint8_t> buf(hdr, hdr + sizeof(hdr));
+
+        // Append the large header fields.
+        copy(dummyChaddr, dummyChaddr + Pkt4::MAX_CHADDR_LEN, back_inserter(buf));
+        copy(dummySname, dummySname + Pkt4::MAX_SNAME_LEN, back_inserter(buf));
+        copy(dummyFile, dummyFile + Pkt4::MAX_FILE_LEN, back_inserter(buf));
+
+        // Should now have all the header, so check.  The "static_cast" is used
+        // to get round an odd bug whereby the linker appears not to find the
+        // definition of DHCPV4_PKT_HDR_LEN if it appears within an EXPECT_EQ().
+        EXPECT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN), buf.size());
+
+        // Add magic cookie
+        buf.push_back(0x63);
+        buf.push_back(0x82);
+        buf.push_back(0x53);
+        buf.push_back(0x63);
+
+        // Add message type DISCOVER
+        buf.push_back(static_cast<uint8_t>(DHO_DHCP_MESSAGE_TYPE));
+        buf.push_back(1); // length (just one byte)
+        buf.push_back(static_cast<uint8_t>(DHCPDISCOVER));
+
+        return (Pkt4Ptr(new Pkt4(&buf[0], buf.size())));
+    }
+
+    /// test callback that stores received callout name and pkt4 value
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt4_receive_callout(CalloutHandle& callout_handle) {
+        callback_name_ = string("pkt4_receive");
+
+        callout_handle.getArgument("query4", callback_pkt4_);
+
+        callback_argument_names_ = callout_handle.getArgumentNames();
+        return (0);
+    }
+
+    /// test callback that changes client-id value
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt4_receive_change_clientid(CalloutHandle& callout_handle) {
+
+        Pkt4Ptr pkt;
+        callout_handle.getArgument("query4", pkt);
+
+        // get rid of the old client-id
+        pkt->delOption(DHO_DHCP_CLIENT_IDENTIFIER);
+
+        // add a new option
+        pkt->addOption(createOption(DHO_DHCP_CLIENT_IDENTIFIER));
+
+        // carry on as usual
+        return pkt4_receive_callout(callout_handle);
+    }
+
+    /// test callback that deletes client-id
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt4_receive_delete_clientid(CalloutHandle& callout_handle) {
+
+        Pkt4Ptr pkt;
+        callout_handle.getArgument("query4", pkt);
+
+        // get rid of the old client-id (and no HWADDR)
+        vector<uint8_t> mac;
+        pkt->delOption(DHO_DHCP_CLIENT_IDENTIFIER);
+        pkt->setHWAddr(1, 0, mac); // HWtype 1, hwardware len = 0
+
+        // carry on as usual
+        return pkt4_receive_callout(callout_handle);
+    }
+
+    /// test callback that sets skip flag
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt4_receive_skip(CalloutHandle& callout_handle) {
+
+        Pkt4Ptr pkt;
+        callout_handle.getArgument("query4", pkt);
+
+        callout_handle.setSkip(true);
+
+        // carry on as usual
+        return pkt4_receive_callout(callout_handle);
+    }
+
+    /// Test callback that stores received callout name and pkt4 value
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt4_send_callout(CalloutHandle& callout_handle) {
+        callback_name_ = string("pkt4_send");
+
+        callout_handle.getArgument("response4", callback_pkt4_);
+
+        callback_argument_names_ = callout_handle.getArgumentNames();
+        return (0);
+    }
+
+    // Test callback that changes server-id
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt4_send_change_serverid(CalloutHandle& callout_handle) {
+
+        Pkt4Ptr pkt;
+        callout_handle.getArgument("response4", pkt);
+
+        // get rid of the old server-id
+        pkt->delOption(DHO_DHCP_SERVER_IDENTIFIER);
+
+        // add a new option
+        pkt->addOption(createOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+        // carry on as usual
+        return pkt4_send_callout(callout_handle);
+    }
+
+    /// test callback that deletes server-id
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt4_send_delete_serverid(CalloutHandle& callout_handle) {
+
+        Pkt4Ptr pkt;
+        callout_handle.getArgument("response4", pkt);
+
+        // get rid of the old client-id
+        pkt->delOption(DHO_DHCP_SERVER_IDENTIFIER);
+
+        // carry on as usual
+        return pkt4_send_callout(callout_handle);
+    }
+
+    /// Test callback that sets skip flag
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt4_send_skip(CalloutHandle& callout_handle) {
+
+        Pkt4Ptr pkt;
+        callout_handle.getArgument("response4", pkt);
+
+        callout_handle.setSkip(true);
+
+        // carry on as usual
+        return pkt4_send_callout(callout_handle);
+    }
+
+    /// Test callback that stores received callout name and subnet4 values
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    subnet4_select_callout(CalloutHandle& callout_handle) {
+        callback_name_ = string("subnet4_select");
+
+        callout_handle.getArgument("query4", callback_pkt4_);
+        callout_handle.getArgument("subnet4", callback_subnet4_);
+        callout_handle.getArgument("subnet4collection", callback_subnet4collection_);
+
+        callback_argument_names_ = callout_handle.getArgumentNames();
+        return (0);
+    }
+
+    /// Test callback that picks the other subnet if possible.
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    subnet4_select_different_subnet_callout(CalloutHandle& callout_handle) {
+
+        // Call the basic calllout to record all passed values
+        subnet4_select_callout(callout_handle);
+
+        const Subnet4Collection* subnets;
+        Subnet4Ptr subnet;
+        callout_handle.getArgument("subnet4", subnet);
+        callout_handle.getArgument("subnet4collection", subnets);
+
+        // Let's change to a different subnet
+        if (subnets->size() > 1) {
+            subnet = (*subnets)[1]; // Let's pick the other subnet
+            callout_handle.setArgument("subnet4", subnet);
+        }
+
+        return (0);
+    }
+
+    /// resets buffers used to store data received by callouts
+    void resetCalloutBuffers() {
+        callback_name_ = string("");
+        callback_pkt4_.reset();
+        callback_subnet4_.reset();
+        callback_subnet4collection_ = NULL;
+        callback_argument_names_.clear();
+    }
+
+    /// pointer to Dhcpv4Srv that is used in tests
+    NakedDhcpv4Srv* srv_;
+
+    // The following fields are used in testing pkt4_receive_callout
+
+    /// String name of the received callout
+    static string callback_name_;
+
+    /// Pkt4 structure returned in the callout
+    static Pkt4Ptr callback_pkt4_;
+
+    /// Pointer to a subnet received by callout
+    static Subnet4Ptr callback_subnet4_;
+
+    /// A list of all available subnets (received by callout)
+    static const Subnet4Collection* callback_subnet4collection_;
+
+    /// A list of all received arguments
+    static vector<string> callback_argument_names_;
+};
+
+// The following fields are used in testing pkt4_receive_callout.
+// See fields description in the class for details
+string HooksDhcpv4SrvTest::callback_name_;
+Pkt4Ptr HooksDhcpv4SrvTest::callback_pkt4_;
+Subnet4Ptr HooksDhcpv4SrvTest::callback_subnet4_;
+const Subnet4Collection* HooksDhcpv4SrvTest::callback_subnet4collection_;
+vector<string> HooksDhcpv4SrvTest::callback_argument_names_;
+
+
+// Checks if callouts installed on pkt4_received are indeed called and the
+// all necessary parameters are passed.
+//
+// Note that the test name does not follow test naming convention,
+// but the proper hook name is "pkt4_receive".
+TEST_F(HooksDhcpv4SrvTest, simple_pkt4_receive) {
+
+    // Install pkt4_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt4_receive", pkt4_receive_callout));
+
+    // Let's create a simple DISCOVER
+    Pkt4Ptr sol = Pkt4Ptr(generateSimpleDiscover());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive4(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt4_receive callback.
+    srv_->run();
+
+    // check that the callback called is indeed the one we installed
+    EXPECT_EQ("pkt4_receive", callback_name_);
+
+    // check that pkt4 argument passing was successful and returned proper value
+    EXPECT_TRUE(callback_pkt4_.get() == sol.get());
+
+    // Check that all expected parameters are there
+    vector<string> expected_argument_names;
+    expected_argument_names.push_back(string("query4"));
+
+    EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+}
+
+// Checks if callouts installed on pkt4_received is able to change
+// the values and the parameters are indeed used by the server.
+TEST_F(HooksDhcpv4SrvTest, valueChange_pkt4_receive) {
+
+    // Install pkt4_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt4_receive", pkt4_receive_change_clientid));
+
+    // Let's create a simple DISCOVER
+    Pkt4Ptr sol = Pkt4Ptr(generateSimpleDiscover());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive4(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt4_receive callback.
+    srv_->run();
+
+    // check that the server did send a reposonce
+    ASSERT_EQ(1, srv_->fake_sent_.size());
+
+    // Make sure that we received a response
+    Pkt4Ptr adv = srv_->fake_sent_.front();
+    ASSERT_TRUE(adv);
+
+    // Get client-id...
+    OptionPtr clientid = adv->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
+
+    // ... and check if it is the modified value
+    OptionPtr expected = createOption(DHO_DHCP_CLIENT_IDENTIFIER);
+    EXPECT_TRUE(clientid->equal(expected));
+}
+
+// Checks if callouts installed on pkt4_received is able to delete
+// existing options and that change impacts server processing (mandatory
+// client-id option is deleted, so the packet is expected to be dropped)
+TEST_F(HooksDhcpv4SrvTest, deleteClientId_pkt4_receive) {
+
+    // Install pkt4_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt4_receive", pkt4_receive_delete_clientid));
+
+    // Let's create a simple DISCOVER
+    Pkt4Ptr sol = Pkt4Ptr(generateSimpleDiscover());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive4(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt4_receive callback.
+    srv_->run();
+
+    // Check that the server dropped the packet and did not send a response
+    ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+// Checks if callouts installed on pkt4_received is able to set skip flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv4SrvTest, skip_pkt4_receive) {
+
+    // Install pkt4_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt4_receive", pkt4_receive_skip));
+
+    // Let's create a simple DISCOVER
+    Pkt4Ptr sol = Pkt4Ptr(generateSimpleDiscover());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive4(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt4_receive callback.
+    srv_->run();
+
+    // check that the server dropped the packet and did not produce any response
+    ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+
+// Checks if callouts installed on pkt4_send are indeed called and the
+// all necessary parameters are passed.
+TEST_F(HooksDhcpv4SrvTest, simple_pkt4_send) {
+
+    // Install pkt4_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt4_send", pkt4_send_callout));
+
+    // Let's create a simple DISCOVER
+    Pkt4Ptr sol = Pkt4Ptr(generateSimpleDiscover());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive4(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt4_receive callback.
+    srv_->run();
+
+    // Check that the callback called is indeed the one we installed
+    EXPECT_EQ("pkt4_send", callback_name_);
+
+    // Check that there is one packet sent
+    ASSERT_EQ(1, srv_->fake_sent_.size());
+    Pkt4Ptr adv = srv_->fake_sent_.front();
+
+    // Check that pkt4 argument passing was successful and returned proper value
+    EXPECT_TRUE(callback_pkt4_.get() == adv.get());
+
+    // Check that all expected parameters are there
+    vector<string> expected_argument_names;
+    expected_argument_names.push_back(string("response4"));
+    EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+}
+
+// Checks if callouts installed on pkt4_send is able to change
+// the values and the packet sent contains those changes
+TEST_F(HooksDhcpv4SrvTest, valueChange_pkt4_send) {
+
+    // Install pkt4_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt4_send", pkt4_send_change_serverid));
+
+    // Let's create a simple DISCOVER
+    Pkt4Ptr sol = Pkt4Ptr(generateSimpleDiscover());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive4(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt4_receive callback.
+    srv_->run();
+
+    // check that the server did send a reposonce
+    ASSERT_EQ(1, srv_->fake_sent_.size());
+
+    // Make sure that we received a response
+    Pkt4Ptr adv = srv_->fake_sent_.front();
+    ASSERT_TRUE(adv);
+
+    // Get client-id...
+    OptionPtr clientid = adv->getOption(DHO_DHCP_SERVER_IDENTIFIER);
+
+    // ... and check if it is the modified value
+    OptionPtr expected = createOption(DHO_DHCP_SERVER_IDENTIFIER);
+    EXPECT_TRUE(clientid->equal(expected));
+}
+
+// Checks if callouts installed on pkt4_send is able to delete
+// existing options and that server applies those changes. In particular,
+// we are trying to send a packet without server-id. The packet should
+// be sent
+TEST_F(HooksDhcpv4SrvTest, deleteServerId_pkt4_send) {
+
+    // Install pkt4_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt4_send", pkt4_send_delete_serverid));
+
+    // Let's create a simple DISCOVER
+    Pkt4Ptr sol = Pkt4Ptr(generateSimpleDiscover());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive4(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt4_receive callback.
+    srv_->run();
+
+    // Check that the server indeed sent a malformed ADVERTISE
+    ASSERT_EQ(1, srv_->fake_sent_.size());
+
+    // Get that ADVERTISE
+    Pkt4Ptr adv = srv_->fake_sent_.front();
+    ASSERT_TRUE(adv);
+
+    // Make sure that it does not have server-id
+    EXPECT_FALSE(adv->getOption(DHO_DHCP_SERVER_IDENTIFIER));
+}
+
+// Checks if callouts installed on pkt4_skip is able to set skip flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv4SrvTest, skip_pkt4_send) {
+
+    // Install pkt4_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt4_send", pkt4_send_skip));
+
+    // Let's create a simple REQUEST
+    Pkt4Ptr sol = Pkt4Ptr(generateSimpleDiscover());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive4(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt4_receive callback.
+    srv_->run();
+
+    // check that the server dropped the packet and did not produce any response
+    ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+// This test checks if subnet4_select callout is triggered and reports
+// valid parameters
+TEST_F(HooksDhcpv4SrvTest, subnet4_select) {
+
+    // Install pkt4_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "subnet4_select", subnet4_select_callout));
+
+    // Configure 2 subnets, both directly reachable over local interface
+    // (let's not complicate the matter with relays)
+    string config = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet4\": [ { "
+        "    \"pool\": [ \"192.0.2.0/25\" ],"
+        "    \"subnet\": \"192.0.2.0/24\", "
+        "    \"interface\": \"" + valid_iface_ + "\" "
+        " }, {"
+        "    \"pool\": [ \"192.0.3.0/25\" ],"
+        "    \"subnet\": \"192.0.3.0/24\" "
+        " } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json = Element::fromJSON(config);
+    ConstElementPtr status;
+
+    // Configure the server and make sure the config is accepted
+    EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+    ASSERT_TRUE(status);
+    comment_ = config::parseAnswer(rcode_, status);
+    ASSERT_EQ(0, rcode_);
+
+    // Prepare discover packet. Server should select first subnet for it
+    Pkt4Ptr sol = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+    sol->setRemoteAddr(IOAddress("192.0.2.1"));
+    sol->setIface(valid_iface_);
+    OptionPtr clientid = generateClientId();
+    sol->addOption(clientid);
+
+    // Pass it to the server and get an advertise
+    Pkt4Ptr adv = srv_->processDiscover(sol);
+
+    // check if we get response at all
+    ASSERT_TRUE(adv);
+
+    // Check that the callback called is indeed the one we installed
+    EXPECT_EQ("subnet4_select", callback_name_);
+
+    // Check that pkt4 argument passing was successful and returned proper value
+    EXPECT_TRUE(callback_pkt4_.get() == sol.get());
+
+    const Subnet4Collection* exp_subnets = CfgMgr::instance().getSubnets4();
+
+    // The server is supposed to pick the first subnet, because of matching
+    // interface. Check that the value is reported properly.
+    ASSERT_TRUE(callback_subnet4_);
+    EXPECT_EQ(exp_subnets->front().get(), callback_subnet4_.get());
+
+    // Server is supposed to report two subnets
+    ASSERT_EQ(exp_subnets->size(), callback_subnet4collection_->size());
+
+    // Compare that the available subnets are reported as expected
+    EXPECT_TRUE((*exp_subnets)[0].get() == (*callback_subnet4collection_)[0].get());
+    EXPECT_TRUE((*exp_subnets)[1].get() == (*callback_subnet4collection_)[1].get());
+}
+
+// This test checks if callout installed on subnet4_select hook point can pick
+// a different subnet.
+TEST_F(HooksDhcpv4SrvTest, subnet_select_change) {
+
+    // Install pkt4_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "subnet4_select", subnet4_select_different_subnet_callout));
+
+    // Configure 2 subnets, both directly reachable over local interface
+    // (let's not complicate the matter with relays)
+    string config = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet4\": [ { "
+        "    \"pool\": [ \"192.0.2.0/25\" ],"
+        "    \"subnet\": \"192.0.2.0/24\", "
+        "    \"interface\": \"" + valid_iface_ + "\" "
+        " }, {"
+        "    \"pool\": [ \"192.0.3.0/25\" ],"
+        "    \"subnet\": \"192.0.3.0/24\" "
+        " } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json = Element::fromJSON(config);
+    ConstElementPtr status;
+
+    // Configure the server and make sure the config is accepted
+    EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+    ASSERT_TRUE(status);
+    comment_ = config::parseAnswer(rcode_, status);
+    ASSERT_EQ(0, rcode_);
+
+    // Prepare discover packet. Server should select first subnet for it
+    Pkt4Ptr sol = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+    sol->setRemoteAddr(IOAddress("192.0.2.1"));
+    sol->setIface(valid_iface_);
+    OptionPtr clientid = generateClientId();
+    sol->addOption(clientid);
+
+    // Pass it to the server and get an advertise
+    Pkt4Ptr adv = srv_->processDiscover(sol);
+
+    // check if we get response at all
+    ASSERT_TRUE(adv);
+
+    // The response should have an address from second pool, so let's check it
+    IOAddress addr = adv->getYiaddr();
+    EXPECT_NE("0.0.0.0", addr.toText());
+
+    // Get all subnets and use second subnet for verification
+    const Subnet4Collection* subnets = CfgMgr::instance().getSubnets4();
+    ASSERT_EQ(2, subnets->size());
+
+    // Advertised address must belong to the second pool (in subnet's range,
+    // in dynamic pool)
+    EXPECT_TRUE((*subnets)[1]->inRange(addr));
+    EXPECT_TRUE((*subnets)[1]->inPool(addr));
+}
+
+
+
 } // end of anonymous namespace

+ 1 - 0
src/bin/dhcp6/Makefile.am

@@ -65,6 +65,7 @@ b10_dhcp6_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
 b10_dhcp6_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
 b10_dhcp6_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
 b10_dhcp6_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
+b10_dhcp6_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
 
 b10_dhcp6dir = $(pkgdatadir)
 b10_dhcp6_DATA = dhcp6.spec

+ 51 - 42
src/bin/dhcp6/config_parser.cc

@@ -67,10 +67,10 @@ public:
     /// @param dummy first param, option names are always "Dhcp6/option-data[n]"
     /// @param options is the option storage in which to store the parsed option
     /// upon "commit".
-    /// @param global_context is a pointer to the global context which 
+    /// @param global_context is a pointer to the global context which
     /// stores global scope parameters, options, option defintions.
-    Dhcp6OptionDataParser(const std::string&, OptionStoragePtr options, 
-                         ParserContextPtr global_context) 
+    Dhcp6OptionDataParser(const std::string&, OptionStoragePtr options,
+                         ParserContextPtr global_context)
         :OptionDataParser("", options, global_context) {
     }
 
@@ -78,7 +78,7 @@ public:
     ///
     /// @param param_name name of the parameter to be parsed.
     /// @param options storage where the parameter value is to be stored.
-    /// @param global_context is a pointer to the global context which 
+    /// @param global_context is a pointer to the global context which
     /// stores global scope parameters, options, option defintions.
     /// @return returns a pointer to a new OptionDataParser. Caller is
     /// is responsible for deleting it when it is no longer needed.
@@ -90,16 +90,16 @@ public:
 
 protected:
     /// @brief Finds an option definition within the server's option space
-    /// 
-    /// Given an option space and an option code, find the correpsonding 
+    ///
+    /// Given an option space and an option code, find the correpsonding
     /// option defintion within the server's option defintion storage.
     ///
-    /// @param option_space name of the parameter option space 
-    /// @param option_code numeric value of the parameter to find 
-    /// @return OptionDefintionPtr of the option defintion or an 
+    /// @param option_space name of the parameter option space
+    /// @param option_code numeric value of the parameter to find
+    /// @return OptionDefintionPtr of the option defintion or an
     /// empty OptionDefinitionPtr if not found.
-    /// @throw DhcpConfigError if the option space requested is not valid 
-    /// for this server. 
+    /// @throw DhcpConfigError if the option space requested is not valid
+    /// for this server.
     virtual OptionDefinitionPtr findServerSpaceOptionDefinition (
                             std::string& option_space, uint32_t option_code) {
         OptionDefinitionPtr def;
@@ -115,11 +115,11 @@ protected:
     }
 };
 
-/// @brief Parser for IPv4 pool definitions.  
+/// @brief Parser for IPv4 pool definitions.
 ///
-/// This is the IPv6 derivation of the PoolParser class and handles pool 
-/// definitions, i.e. a list of entries of one of two syntaxes: min-max and 
-/// prefix/len for IPv6 pools. Pool6 objects are created and stored in chosen 
+/// This is the IPv6 derivation of the PoolParser class and handles pool
+/// definitions, i.e. a list of entries of one of two syntaxes: min-max and
+/// prefix/len for IPv6 pools. Pool6 objects are created and stored in chosen
 /// PoolStorage container.
 ///
 /// It is useful for parsing Dhcp6/subnet6[X]/pool parameters.
@@ -142,9 +142,9 @@ protected:
     /// @param addr is the IPv6 prefix of the pool.
     /// @param len is the prefix length.
     /// @param ptype is the type of IPv6 pool (Pool6::Pool6Type). Note this is
-    /// passed in as an int32_t and cast to Pool6Type to accommodate a 
+    /// passed in as an int32_t and cast to Pool6Type to accommodate a
     /// polymorphic interface.
-    /// @return returns a PoolPtr to the new Pool4 object. 
+    /// @return returns a PoolPtr to the new Pool4 object.
     PoolPtr poolMaker (IOAddress &addr, uint32_t len, int32_t ptype)
     {
         return (PoolPtr(new Pool6(static_cast<isc::dhcp::Pool6::Pool6Type>
@@ -156,9 +156,9 @@ protected:
     /// @param min is the first IPv6 address in the pool.
     /// @param max is the last IPv6 address in the pool.
     /// @param ptype is the type of IPv6 pool (Pool6::Pool6Type). Note this is
-    /// passed in as an int32_t and cast to Pool6Type to accommodate a 
+    /// passed in as an int32_t and cast to Pool6Type to accommodate a
     /// polymorphic interface.
-    /// @return returns a PoolPtr to the new Pool4 object. 
+    /// @return returns a PoolPtr to the new Pool4 object.
     PoolPtr poolMaker (IOAddress &min, IOAddress &max, int32_t ptype)
     {
         return (PoolPtr(new Pool6(static_cast<isc::dhcp::Pool6::Pool6Type>
@@ -168,8 +168,8 @@ protected:
 
 /// @brief This class parses a single IPv6 subnet.
 ///
-/// This is the IPv6 derivation of the SubnetConfigParser class and it parses 
-/// the whole subnet definition. It creates parsersfor received configuration 
+/// This is the IPv6 derivation of the SubnetConfigParser class and it parses
+/// the whole subnet definition. It creates parsersfor received configuration
 /// parameters as needed.
 class Subnet6ConfigParser : public SubnetConfigParser {
 public:
@@ -178,7 +178,7 @@ public:
     ///
     /// @param ignored first parameter
     /// stores global scope parameters, options, option defintions.
-    Subnet6ConfigParser(const std::string&) 
+    Subnet6ConfigParser(const std::string&)
         :SubnetConfigParser("", globalContext()) {
     }
 
@@ -220,7 +220,7 @@ protected:
         } else if (config_id.compare("pool") == 0) {
             parser = new Pool6Parser(config_id, pools_);
         } else if (config_id.compare("option-data") == 0) {
-           parser = new OptionDataListParser(config_id, options_, 
+           parser = new OptionDataListParser(config_id, options_,
                                              global_context_,
                                              Dhcp6OptionDataParser::factory);
         } else {
@@ -233,14 +233,14 @@ protected:
 
 
     /// @brief Determines if the given option space name and code describe
-    /// a standard option for the DHCP6 server. 
+    /// a standard option for the DHCP6 server.
     ///
     /// @param option_space is the name of the option space to consider
     /// @param code is the numeric option code to consider
     /// @return returns true if the space and code are part of the server's
     /// standard options.
     bool isServerStdOption(std::string option_space, uint32_t code) {
-        return ((option_space.compare("dhcp6") == 0) 
+        return ((option_space.compare("dhcp6") == 0)
                 && LibDHCP::isStandardOption(Option::V6, code));
     }
 
@@ -253,23 +253,23 @@ protected:
     }
 
     /// @brief Issues a DHCP6 server specific warning regarding duplicate subnet
-    /// options. 
-    /// 
+    /// options.
+    ///
     /// @param code is the numeric option code of the duplicate option
     /// @param addr is the subnet address
     /// @todo A means to know the correct logger and perhaps a common
     /// message would allow this message to be emitted by the base class.
-    virtual void duplicate_option_warning(uint32_t code, 
+    virtual void duplicate_option_warning(uint32_t code,
                                          isc::asiolink::IOAddress& addr) {
         LOG_WARN(dhcp6_logger, DHCP6_CONFIG_OPTION_DUPLICATE)
             .arg(code).arg(addr.toText());
     }
 
     /// @brief Instantiates the IPv6 Subnet based on a given IPv6 address
-    /// and prefix length.  
-    /// 
+    /// and prefix length.
+    ///
     /// @param addr is IPv6 prefix of the subnet.
-    /// @param len is the prefix length 
+    /// @param len is the prefix length
     void initSubnet(isc::asiolink::IOAddress addr, uint8_t len) {
         // Get all 'time' parameters using inheritance.
         // If the subnet-specific value is defined then use it, else
@@ -292,13 +292,13 @@ protected:
 
         // Specifying both interface for locally reachable subnets and
         // interface id for relays is mutually exclusive. Need to test for
-        // this condition. 
+        // this condition.
         if (!ifaceid.empty()) {
             std::string iface;
             try {
                 iface = string_values_->getParam("interface");
             } catch (const DhcpConfigError &) {
-                // iface not mandatory 
+                // iface not mandatory
             }
 
             if (!iface.empty()) {
@@ -403,7 +403,7 @@ namespace dhcp {
 ///
 /// @param config_id pointer to received global configuration entry
 /// @return parser for specified global DHCPv6 parameter
-/// @throw NotImplemented if trying to create a parser for unknown config 
+/// @throw NotImplemented if trying to create a parser for unknown config
 /// element
 DhcpConfigParser* createGlobal6DhcpConfigParser(const std::string& config_id) {
     DhcpConfigParser* parser = NULL;
@@ -411,22 +411,22 @@ DhcpConfigParser* createGlobal6DhcpConfigParser(const std::string& config_id) {
         (config_id.compare("valid-lifetime") == 0)  ||
         (config_id.compare("renew-timer") == 0)  ||
         (config_id.compare("rebind-timer") == 0))  {
-        parser = new Uint32Parser(config_id, 
+        parser = new Uint32Parser(config_id,
                                  globalContext()->uint32_values_);
-    } else if (config_id.compare("interface") == 0) {
+    } else if (config_id.compare("interfaces") == 0) {
         parser = new InterfaceListConfigParser(config_id);
     } else if (config_id.compare("subnet6") == 0) {
         parser = new Subnets6ListConfigParser(config_id);
     } else if (config_id.compare("option-data") == 0) {
-        parser = new OptionDataListParser(config_id, 
-                                          globalContext()->options_, 
+        parser = new OptionDataListParser(config_id,
+                                          globalContext()->options_,
                                           globalContext(),
                                           Dhcp6OptionDataParser::factory);
     } else if (config_id.compare("option-def") == 0) {
-        parser  = new OptionDefListParser(config_id, 
+        parser  = new OptionDefListParser(config_id,
                                           globalContext()->option_defs_);
     } else if (config_id.compare("version") == 0) {
-        parser  = new StringParser(config_id, 
+        parser  = new StringParser(config_id,
                                    globalContext()->string_values_);
     } else if (config_id.compare("lease-database") == 0) {
         parser = new DbAccessParser(config_id);
@@ -452,7 +452,7 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) {
     /// @todo: Append most essential info here (like "2 new subnets configured")
     string config_details;
 
-    LOG_DEBUG(dhcp6_logger, DBG_DHCP6_COMMAND, 
+    LOG_DEBUG(dhcp6_logger, DBG_DHCP6_COMMAND,
               DHCP6_CONFIG_START).arg(config_set->str());
 
     // Some of the values specified in the configuration depend on
@@ -465,6 +465,7 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) {
     ParserCollection independent_parsers;
     ParserPtr subnet_parser;
     ParserPtr option_parser;
+    ParserPtr iface_parser;
 
     // Some of the parsers alter state of the system that can't easily
     // be undone. (Or alter it in a way such that undoing the change
@@ -502,7 +503,6 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) {
                 subnet_parser = parser;
             } else if (config_pair.first == "option-data") {
                 option_parser = parser;
-
             } else if (config_pair.first == "hooks-libraries") {
                 // Executing the commit will alter currently loaded hooks
                 // libraries. Check if the supplied libraries are valid,
@@ -510,6 +510,11 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) {
                 // committed.
                 hooks_parser = parser;
                 hooks_parser->build(config_pair.second);
+            } else if (config_pair.first == "interfaces") {
+                // The interface parser is independent from any other parser and
+                // can be run here before other parsers.
+                parser->build(config_pair.second);
+                iface_parser = parser;
             } else {
                 // Those parsers should be started before other
                 // parsers so we can call build straight away.
@@ -563,6 +568,10 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) {
             if (subnet_parser) {
                 subnet_parser->commit();
             }
+
+            if (iface_parser) {
+                iface_parser->commit();
+            }
         }
         catch (const isc::Exception& ex) {
             LOG_ERROR(dhcp6_logger, DHCP6_PARSER_COMMIT_FAIL).arg(ex.what());

+ 3 - 3
src/bin/dhcp6/config_parser.h

@@ -31,8 +31,8 @@ class Dhcpv6Srv;
 
 /// @brief Configures DHCPv6 server
 ///
-/// This function is called every time a new configuration is received. The 
-/// extra parameter is a reference to DHCPv6 server component. It is currently 
+/// This function is called every time a new configuration is received. The
+/// extra parameter is a reference to DHCPv6 server component. It is currently
 /// not used and CfgMgr::instance() is accessed instead.
 ///
 /// This method does not throw. It catches all exceptions and returns them as
@@ -53,7 +53,7 @@ configureDhcp6Server(Dhcpv6Srv& server, isc::data::ConstElementPtr config_set);
 ///
 /// @returns a reference to the global context
 ParserContextPtr& globalContext();
- 
+
 }; // end of isc::dhcp namespace
 }; // end of isc namespace
 

+ 28 - 3
src/bin/dhcp6/ctrl_dhcp6_srv.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2013  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -20,6 +20,7 @@
 #include <config/ccsession.h>
 #include <dhcp/iface_mgr.h>
 #include <dhcpsrv/dhcp_config_parser.h>
+#include <dhcpsrv/cfgmgr.h>
 #include <dhcp6/config_parser.h>
 #include <dhcp6/ctrl_dhcp6_srv.h>
 #include <dhcp6/dhcp6_log.h>
@@ -100,7 +101,27 @@ ControlledDhcpv6Srv::dhcp6ConfigHandler(ConstElementPtr new_config) {
     }
 
     // Configure the server.
-    return (configureDhcp6Server(*server_, merged_config));
+    ConstElementPtr answer = configureDhcp6Server(*server_, merged_config);
+
+    // Check that configuration was successful. If not, do not reopen sockets.
+    int rcode = 0;
+    parseAnswer(rcode, answer);
+    if (rcode != 0) {
+        return (answer);
+    }
+
+    // Configuration may change active interfaces. Therefore, we have to reopen
+    // sockets according to new configuration. This operation is not exception
+    // safe and we really don't want to emit exceptions to the callback caller.
+    // Instead, catch an exception and create appropriate answer.
+    try {
+        server_->openActiveSockets(server_->getPort());
+    } catch (const std::exception& ex) {
+        std::ostringstream err;
+        err << "failed to open sockets after server reconfiguration: " << ex.what();
+        answer = isc::config::createAnswer(1, err.str());
+    }
+    return (answer);
 }
 
 ConstElementPtr
@@ -172,8 +193,13 @@ void ControlledDhcpv6Srv::establishSession() {
     try {
         // Pull the full configuration out from the session.
         configureDhcp6Server(*this, config_session_->getFullConfig());
+        // Configuration may disable or enable interfaces so we have to
+        // reopen sockets according to new configuration.
+        openActiveSockets(getPort());
+
     } catch (const DhcpConfigError& ex) {
         LOG_ERROR(dhcp6_logger, DHCP6_CONFIG_LOAD_FAIL).arg(ex.what());
+
     }
 
     /// Integrate the asynchronous I/O model of BIND 10 configuration
@@ -228,6 +254,5 @@ ControlledDhcpv6Srv::execDhcpv6ServerCommand(const std::string& command_id,
     }
 }
 
-
 };
 };

+ 4 - 0
src/bin/dhcp6/dhcp6.dox

@@ -92,4 +92,8 @@
 
  @todo Add section about setting up options and their definitions with bindctl.
 
+ @section dhcpv6Other Other DHCPv6 topics
+
+ For hooks API support in DHCPv6, see @ref dhcpv6Hooks.
+
  */

+ 3 - 3
src/bin/dhcp6/dhcp6.spec

@@ -16,16 +16,16 @@
         }
       },
  
-      { "item_name": "interface",
+      { "item_name": "interfaces",
         "item_type": "list",
         "item_optional": false,
-        "item_default": [ "all" ],
+        "item_default": [ "*" ],
         "list_item_spec":
         {
           "item_name": "interface_name",
           "item_type": "string",
           "item_optional": false,
-          "item_default": "all"
+          "item_default": "*"
         }
       } ,
 

+ 139 - 0
src/bin/dhcp6/dhcp6_hooks.dox

@@ -0,0 +1,139 @@
+// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/**
+ @page dhcpv6Hooks The Hooks API for the DHCPv6 Server
+
+ @section dhcpv6HooksIntroduction Introduction
+ BIND10 features an API (the "Hooks" API) that allows user-written code to
+ be integrated into BIND 10 and called at specific points in its processing.
+ An overview of the API and a tutorial for writing such code can be found in
+ the @ref hooksDevelopersGuide.  Information for BIND 10 maintainers can be
+ found in the @ref hooksComponentDeveloperGuide.
+
+ This manual is more specialised and is aimed at developers of hook
+ code for the DHCPv6 server. It describes each hook point, what the callouts
+ attached to the hook are able to do, and the arguments passed to the
+ callouts.  Each entry in this manual has the following information:
+
+ - Name of the hook point.
+ - Arguments for the callout.  As well as the argument name and data type, the
+   information includes the direction, which can be one of:
+   - @b in - the server passes values to the callout but ignored any data
+     returned.
+   - @b out - the callout is expected to set this value.
+   - <b>in/out</b> - the server passes a value to the callout and uses whatever
+     value the callout sends back.  Note that the callout may choose not to
+     do any modification, in which case the server will use whatever value
+     it sent to the callout.
+ - Description of the hook. This explains where in the processing the hook
+   is located, the possible actions a callout attached to this hook could take,
+   and a description of the data passed to the callouts.
+ - Skip flag action: the action taken by the server if a callout chooses to set
+    the "skip" flag.
+
+@section dhcpv6HooksHookPoints Hooks in the DHCPv6 Server
+
+The following list is ordered by appearance of specific hook points during
+packet processing. Hook points that are not specific to packet processing
+(e.g. lease expiration) will be added to the end of this list.
+
+ @subsection dhcpv6HooksPkt6Receive pkt6_receive
+
+ - @b Arguments:
+   - name: @b query6, type: isc::dhcp::Pkt6Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed when an incoming DHCPv6
+   packet is received and its content is parsed. The sole argument -
+   query6 - contains a pointer to an isc::dhcp::Pkt6 object that contains
+   all information regarding incoming packet, including its source and
+   destination addresses, interface over which it was received, a list
+   of all options present within and relay information.  All fields of
+   the Pkt6 object can be modified at this time, except data_. (data_
+   contains the incoming packet as raw buffer. By the time this hook is
+   reached, that information has already parsed and is available though
+   other fields in the Pkt6 object.  For this reason, it doesn't make
+   sense to modify it.)
+
+ - <b>Skip flag action</b>: If any callout sets the skip flag, the server will
+   drop the packet and start processing the next one.  The reason for the drop
+   will be logged if logging is set to the appropriate debug level.
+
+@subsection dhcpv6HooksSubnet6Select subnet6_select
+
+ - @b Arguments:
+   - name: @b query6, type: isc::dhcp::Pkt6Ptr, direction: <b>in/out</b>
+   - name: @b subnet6, type: isc::dhcp::Subnet6Ptr, direction: <b>in/out</b>
+   - name: @b subnet6collection, type: const isc::dhcp::Subnet6Collection *, direction: <b>in</b>
+
+ - @b Description: this callout is executed when a subnet is being
+   selected for the incoming packet. All parameters, addresses and
+   prefixes will be assigned from that subnet. A callout can select a
+   different subnet if it wishes so, the list of all subnets currently
+   configured being provided as 'subnet6collection'. The list itself must
+   not be modified.
+
+ - <b>Skip flag action</b>: If any callout installed on 'subnet6_select'
+   sets the skip flag, the server will not select any subnet. Packet processing
+   will continue, but will be severely limited (i.e. only global options
+   will be assigned).
+
+@subsection dhcpv6HooksLeaseSelect lease6_select
+
+ - @b Arguments:
+   - name: @b subnet6, type: isc::dhcp::Subnet6Ptr, direction: <b>in</b>
+   - name: @b fake_allocation, type: bool, direction: <b>in</b>
+   - name: @b lease6, type: isc::dhcp::Lease6Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed after the server engine
+   has selected a lease for client's request but before the lease
+   has been inserted into the database. Any modifications made to the
+   isc::dhcp::Lease6 object will be stored in the lease's record in the
+   database. The callout should make sure that any modifications are
+   sanity checked as the server will use that data as is with no further
+   checking.\n\n The server processes lease requests for SOLICIT and
+   REQUEST in a very similar way. The only major difference is that
+   for SOLICIT the lease is just selected; it is not inserted into
+   the database.  It is possible to distinguish between SOLICIT and
+   REQUEST by checking value of the fake_allocation flag: a value of true
+   means that the lease won't be inserted into the database (SOLICIT),
+   a value of false means that it will (REQUEST).
+
+ - <b>Skip flag action</b>: If any callout installed on 'lease6_select'
+   sets the skip flag, the server will not assign that particular lease.
+   Packet processing will continue and the client may get other addresses
+   or prefixes if it requested more than one address and/or prefix.
+
+@subsection dhcpv6HooksPkt6Send pkt6_send
+
+ - @b Arguments:
+   - name: @b response6, type: isc::dhcp::Pkt6Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed when server's response
+   is about to be send back to the client. The sole argument - response6 -
+   contains a pointer to an isc::dhcp::Pkt6 object that contains the
+   packet, with set source and destination addresses, interface over which
+   it will be send, list of all options and relay information.  All fields
+   of the Pkt6 object can be modified at this time, except bufferOut_.
+   (This is scratch space used for constructing the packet after all
+   pkt6_send callouts are complete, so any changes to that field will
+   be overwritten.)
+
+ - <b>Skip flag action</b>: if any callout sets the skip flag, the server
+   will drop this response packet. However, the original request packet
+   from a client was processed, so server's state was most likely changed
+   (e.g. lease was allocated). Setting this flag merely stops the change
+   being communicated to the client.
+
+*/

+ 3 - 0
src/bin/dhcp6/dhcp6_log.h

@@ -38,6 +38,9 @@ const int DBG_DHCP6_COMMAND = DBGLVL_COMMAND;
 // Trace basic operations within the code.
 const int DBG_DHCP6_BASIC = DBGLVL_TRACE_BASIC;
 
+// Trace hook related operations
+const int DBG_DHCP6_HOOKS = DBGLVL_TRACE_BASIC;
+
 // Trace detailed operations, including errors raised when processing invalid
 // packets.  (These are not logged at severities of WARN or higher for fear
 // that a set of deliberately invalid packets set to the server could overwhelm

+ 40 - 4
src/bin/dhcp6/dhcp6_messages.mes

@@ -14,6 +14,11 @@
 
 $NAMESPACE isc::dhcp
 
+% DHCP6_ACTIVATE_INTERFACE activating interface %1
+This message is printed when DHCPv6 server enabled an interface to be used
+to receive DHCPv6 traffic. IPv6 socket on this interface will be opened once
+Interface Manager starts up procedure of opening sockets.
+
 % DHCP6_CCSESSION_STARTED control channel session started on socket %1
 A debug message issued during startup after the IPv6 DHCP server has
 successfully established a session with the BIND 10 control channel.
@@ -65,6 +70,32 @@ This informational message is printed every time the IPv6 DHCP server
 is started.  It indicates what database backend type is being to store
 lease and other information.
 
+% DHCP6_DEACTIVATE_INTERFACE deactivate interface %1
+This message is printed when DHCPv6 server disables an interface from being
+used to receive DHCPv6 traffic. Sockets on this interface will not be opened
+by the Interface Manager until interface is enabled.
+
+% DHCP6_HOOK_PACKET_RCVD_SKIP received DHCPv6 packet was dropped, because a callout set skip flag.
+This debug message is printed when a callout installed on the pkt6_receive
+hook point sets the skip flag. For this particular hook point, the
+setting of the flag by a callout instructs the server to drop the packet.
+
+% DHCP6_HOOK_PACKET_SEND_SKIP prepared DHCPv6 response was not sent, because a callout set skip flag.
+This debug message is printed when a callout installed on the pkt6_send
+hook point sets the skip flag. For this particular hook point, the setting
+of the flag by a callout instructs the server to drop the packet. This
+effectively means that the client will not get any response, even though
+the server processed client's request and acted on it (e.g. possibly
+allocated a lease).
+
+% DHCP6_HOOK_SUBNET6_SELECT_SKIP no subnet was selected, because a callout set skip flag.
+This debug message is printed when a callout installed on the
+subnet6_select hook point sets the skip flag. For this particular hook
+point, the setting of the flag instructs the server not to choose a
+subnet, an action that severely limits further processing; the server
+will be only able to offer global options - no addresses or prefixes
+will be assigned.
+
 % DHCP6_LEASE_ADVERT lease %1 advertised (client duid=%2, iaid=%3)
 This debug message indicates that the server successfully advertised
 a lease. It is up to the client to choose one server out of the
@@ -101,6 +132,11 @@ IPv6 DHCP server but it is not running.
 During startup the IPv6 DHCP server failed to detect any network
 interfaces and is therefore shutting down.
 
+% DHCP6_NO_SOCKETS_OPEN no interface configured to listen to DHCP traffic
+This warning message is issued when current server configuration specifies
+no interfaces that server should listen on, or specified interfaces are not
+configured to receive the traffic.
+
 % DHCP6_OPEN_SOCKET opening sockets on port %1
 A debug message issued during startup, this indicates that the IPv6 DHCP
 server is about to open sockets on the specified port.
@@ -159,10 +195,10 @@ actions and committal of changes failed.  The message has been output in
 response to a non-BIND 10 exception being raised.  Additional messages
 may give further information.
 
-The most likely cause of this is that the specification file for the server
-(which details the allowable contents of the configuration) is not correct for
-this version of BIND 10.  This former may be the result of an interrupted
-installation of an update to BIND 10.
+The most likely cause of this is that the specification file for the
+server (which details the allowable contents of the configuration) is
+not correct for this version of BIND 10.  This may be the result of an
+interrupted installation of an update to BIND 10.
 
 % DHCP6_PARSER_FAIL failed to create or run parser for configuration element %1: %2
 On receipt of message containing details to a change of its configuration,

+ 204 - 10
src/bin/dhcp6/dhcp6_srv.cc

@@ -37,6 +37,8 @@
 #include <util/io_utilities.h>
 #include <util/range_utilities.h>
 #include <util/encode/hex.h>
+#include <hooks/hooks_manager.h>
+#include <hooks/callout_handle.h>
 
 #include <boost/foreach.hpp>
 #include <boost/tokenizer.hpp>
@@ -50,9 +52,34 @@
 using namespace isc;
 using namespace isc::asiolink;
 using namespace isc::dhcp;
+using namespace isc::hooks;
 using namespace isc::util;
 using namespace std;
 
+namespace {
+
+/// Structure that holds registered hook indexes
+struct Dhcp6Hooks {
+    int hook_index_pkt6_receive_;   ///< index for "pkt6_receive" hook point
+    int hook_index_subnet6_select_; ///< index for "subnet6_select" hook point
+    int hook_index_pkt6_send_;      ///< index for "pkt6_send" hook point
+
+    /// Constructor that registers hook points for DHCPv6 engine
+    Dhcp6Hooks() {
+        hook_index_pkt6_receive_   = HooksManager::registerHook("pkt6_receive");
+        hook_index_subnet6_select_ = HooksManager::registerHook("subnet6_select");
+        hook_index_pkt6_send_      = HooksManager::registerHook("pkt6_send");
+    }
+};
+
+// Declare a Hooks object. As this is outside any function or method, it
+// will be instantiated (and the constructor run) when the module is loaded.
+// As a result, the hook indexes will be defined before any method in this
+// module is called.
+Dhcp6Hooks Hooks;
+
+}; // anonymous namespace
+
 namespace isc {
 namespace dhcp {
 
@@ -67,7 +94,9 @@ namespace dhcp {
 static const char* SERVER_DUID_FILE = "b10-dhcp6-serverid";
 
 Dhcpv6Srv::Dhcpv6Srv(uint16_t port)
-    : alloc_engine_(), serverid_(), shutdown_(true) {
+:alloc_engine_(), serverid_(), shutdown_(true), hook_index_pkt6_receive_(-1),
+    hook_index_subnet6_select_(-1), hook_index_pkt6_send_(-1), port_(port)
+{
 
     LOG_DEBUG(dhcp6_logger, DBG_DHCP6_START, DHCP6_OPEN_SOCKET).arg(port);
 
@@ -82,7 +111,7 @@ Dhcpv6Srv::Dhcpv6Srv(uint16_t port)
                 LOG_ERROR(dhcp6_logger, DHCP6_NO_INTERFACES);
                 return;
             }
-            IfaceMgr::instance().openSockets6(port);
+            IfaceMgr::instance().openSockets6(port_);
         }
 
         string duid_file = CfgMgr::instance().getDataDir() + "/" + string(SERVER_DUID_FILE);
@@ -106,6 +135,13 @@ Dhcpv6Srv::Dhcpv6Srv(uint16_t port)
         // Instantiate allocation engine
         alloc_engine_.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100));
 
+        // Register hook points
+        hook_index_pkt6_receive_   = Hooks.hook_index_pkt6_receive_;
+        hook_index_subnet6_select_ = Hooks.hook_index_subnet6_select_;
+        hook_index_pkt6_send_      = Hooks.hook_index_pkt6_send_;
+
+        /// @todo call loadLibraries() when handling configuration changes
+
     } catch (const std::exception &e) {
         LOG_ERROR(dhcp6_logger, DHCP6_SRV_CONSTRUCT_ERROR).arg(e.what());
         return;
@@ -126,9 +162,17 @@ void Dhcpv6Srv::shutdown() {
     shutdown_ = true;
 }
 
+Pkt6Ptr Dhcpv6Srv::receivePacket(int timeout) {
+    return (IfaceMgr::instance().receive6(timeout));
+}
+
+void Dhcpv6Srv::sendPacket(const Pkt6Ptr& packet) {
+    IfaceMgr::instance().send(packet);
+}
+
 bool Dhcpv6Srv::run() {
     while (!shutdown_) {
-        /// @todo: calculate actual timeout to the next event (e.g. lease
+        /// @todo Calculate actual timeout to the next event (e.g. lease
         /// expiration) once we have lease database. The idea here is that
         /// it is possible to do everything in a single process/thread.
         /// For now, we are just calling select for 1000 seconds. There
@@ -142,7 +186,7 @@ bool Dhcpv6Srv::run() {
         Pkt6Ptr rsp;
 
         try {
-            query = IfaceMgr::instance().receive6(timeout);
+            query = receivePacket(timeout);
         } catch (const std::exception& e) {
             LOG_ERROR(dhcp6_logger, DHCP6_PACKET_RECEIVE_FAIL).arg(e.what());
         }
@@ -160,6 +204,30 @@ bool Dhcpv6Srv::run() {
                       .arg(query->getBuffer().getLength())
                       .arg(query->toText());
 
+            // Let's execute all callouts registered for packet_received
+            if (HooksManager::getHooksManager().calloutsPresent(hook_index_pkt6_receive_)) {
+                CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+                // Delete previously set arguments
+                callout_handle->deleteAllArguments();
+
+                // Pass incoming packet as argument
+                callout_handle->setArgument("query6", query);
+
+                // Call callouts
+                HooksManager::callCallouts(hook_index_pkt6_receive_, *callout_handle);
+
+                // Callouts decided to skip the next processing step. The next
+                // processing step would to process the packet, so skip at this
+                // stage means drop.
+                if (callout_handle->getSkip()) {
+                    LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_PACKET_RCVD_SKIP);
+                    continue;
+                }
+
+                callout_handle->getArgument("query6", query);
+            }
+
             try {
                 switch (query->getType()) {
                 case DHCPV6_SOLICIT:
@@ -204,7 +272,7 @@ bool Dhcpv6Srv::run() {
             } catch (const RFCViolation& e) {
                 LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_REQUIRED_OPTIONS_CHECK_FAIL)
                     .arg(query->getName())
-                    .arg(query->getRemoteAddr())
+                    .arg(query->getRemoteAddr().toText())
                     .arg(e.what());
 
             } catch (const isc::Exception& e) {
@@ -218,7 +286,7 @@ bool Dhcpv6Srv::run() {
                 // packets.)
                 LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_PACKET_PROCESS_FAIL)
                     .arg(query->getName())
-                    .arg(query->getRemoteAddr())
+                    .arg(query->getRemoteAddr().toText())
                     .arg(e.what());
             }
 
@@ -230,13 +298,35 @@ bool Dhcpv6Srv::run() {
                 rsp->setIndex(query->getIndex());
                 rsp->setIface(query->getIface());
 
+                // Execute all callouts registered for packet6_send
+                if (HooksManager::getHooksManager().calloutsPresent(hook_index_pkt6_send_)) {
+                    CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+                    // Delete all previous arguments
+                    callout_handle->deleteAllArguments();
+
+                    // Set our response
+                    callout_handle->setArgument("response6", rsp);
+
+                    // Call all installed callouts
+                    HooksManager::callCallouts(hook_index_pkt6_send_, *callout_handle);
+
+                    // Callouts decided to skip the next processing step. The next
+                    // processing step would to send the packet, so skip at this
+                    // stage means "drop response".
+                    if (callout_handle->getSkip()) {
+                        LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_PACKET_SEND_SKIP);
+                        continue;
+                    }
+                }
+
                 LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL_DATA,
                           DHCP6_RESPONSE_DATA)
                     .arg(static_cast<int>(rsp->getType())).arg(rsp->toText());
 
                 if (rsp->pack()) {
                     try {
-                        IfaceMgr::instance().send(rsp);
+                        sendPacket(rsp);
                     } catch (const std::exception& e) {
                         LOG_ERROR(dhcp6_logger, DHCP6_PACKET_SEND_FAIL).arg(e.what());
                     }
@@ -560,6 +650,37 @@ Dhcpv6Srv::selectSubnet(const Pkt6Ptr& question) {
         }
     }
 
+    // Let's execute all callouts registered for packet_received
+    if (HooksManager::getHooksManager().calloutsPresent(hook_index_subnet6_select_)) {
+        CalloutHandlePtr callout_handle = getCalloutHandle(question);
+
+        // We're reusing callout_handle from previous calls
+        callout_handle->deleteAllArguments();
+
+        // Set new arguments
+        callout_handle->setArgument("query6", question);
+        callout_handle->setArgument("subnet6", subnet);
+
+        // We pass pointer to const collection for performance reasons.
+        // Otherwise we would get a non-trivial performance penalty each
+        // time subnet6_select is called.
+        callout_handle->setArgument("subnet6collection", CfgMgr::instance().getSubnets6());
+
+        // Call user (and server-side) callouts
+        HooksManager::callCallouts(hook_index_subnet6_select_, *callout_handle);
+
+        // Callouts decided to skip this step. This means that no subnet will be
+        // selected. Packet processing will continue, but it will be severly limited
+        // (i.e. only global options will be assigned)
+        if (callout_handle->getSkip()) {
+            LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_SUBNET6_SELECT_SKIP);
+            return (Subnet6Ptr());
+        }
+
+        // Use whatever subnet was specified by the callout
+        callout_handle->getArgument("subnet6", subnet);
+    }
+
     return (subnet);
 }
 
@@ -619,7 +740,8 @@ Dhcpv6Srv::assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer) {
         switch (opt->second->getType()) {
         case D6O_IA_NA: {
             OptionPtr answer_opt = assignIA_NA(subnet, duid, question,
-                                   boost::dynamic_pointer_cast<Option6IA>(opt->second));
+                                               boost::dynamic_pointer_cast<Option6IA>(opt->second),
+                                               question);
             if (answer_opt) {
                 answer->addOption(answer_opt);
             }
@@ -633,7 +755,7 @@ Dhcpv6Srv::assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer) {
 
 OptionPtr
 Dhcpv6Srv::assignIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
-                       Pkt6Ptr question, boost::shared_ptr<Option6IA> ia) {
+                       Pkt6Ptr question, boost::shared_ptr<Option6IA> ia, const Pkt6Ptr& query) {
     // If there is no subnet selected for handling this IA_NA, the only thing to do left is
     // to say that we are sorry, but the user won't get an address. As a convenience, we
     // use a different status text to indicate that (compare to the same status code,
@@ -677,12 +799,15 @@ Dhcpv6Srv::assignIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
         fake_allocation = true;
     }
 
+    CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
     // Use allocation engine to pick a lease for this client. Allocation engine
     // will try to honour the hint, but it is just a hint - some other address
     // may be used instead. If fake_allocation is set to false, the lease will
     // be inserted into the LeaseMgr as well.
     Lease6Ptr lease = alloc_engine_->allocateAddress6(subnet, duid, ia->getIAID(),
-                                                      hint, fake_allocation);
+                                                      hint, fake_allocation,
+                                                      callout_handle);
 
     // Create IA_NA that we will put in the response.
     // Do not use OptionDefinition to create option's instance so
@@ -1103,5 +1228,74 @@ Dhcpv6Srv::processInfRequest(const Pkt6Ptr& infRequest) {
     return reply;
 }
 
+isc::hooks::CalloutHandlePtr Dhcpv6Srv::getCalloutHandle(const Pkt6Ptr& pkt) {
+    // This method returns a CalloutHandle for a given packet. It is guaranteed
+    // to return the same callout_handle (so user library contexts are
+    // preserved). This method works well if the server processes one packet
+    // at a time. Once the server architecture is extended to cover parallel
+    // packets processing (e.g. delayed-ack, some form of buffering etc.), this
+    // method has to be extended (e.g. store callouts in a map and use pkt as
+    // a key). Additional code would be required to release the callout handle
+    // once the server finished processing.
+
+    CalloutHandlePtr callout_handle;
+    static Pkt6Ptr old_pointer;
+
+    if (!callout_handle ||
+        old_pointer != pkt) {
+        // This is the first packet or a different packet than previously
+        // passed to getCalloutHandle()
+
+        // Remember the pointer to this packet
+        old_pointer = pkt;
+
+        callout_handle = HooksManager::getHooksManager().createCalloutHandle();
+    }
+
+    return (callout_handle);
+}
+
+void
+Dhcpv6Srv::openActiveSockets(const uint16_t port) {
+    IfaceMgr::instance().closeSockets();
+
+    // Get the reference to the collection of interfaces. This reference should be
+    // valid as long as the program is run because IfaceMgr is a singleton.
+    // Therefore we can safely iterate over instances of all interfaces and modify
+    // their flags. Here we modify flags which indicate wheter socket should be
+    // open for a particular interface or not.
+    const IfaceMgr::IfaceCollection& ifaces = IfaceMgr::instance().getIfaces();
+    for (IfaceMgr::IfaceCollection::const_iterator iface = ifaces.begin();
+         iface != ifaces.end(); ++iface) {
+        Iface* iface_ptr = IfaceMgr::instance().getIface(iface->getName());
+        if (iface_ptr == NULL) {
+            isc_throw(isc::Unexpected, "Interface Manager returned NULL"
+                      << " instance of the interface when DHCPv6 server was"
+                      << " trying to reopen sockets after reconfiguration");
+        }
+        if (CfgMgr::instance().isActiveIface(iface->getName())) {
+            iface_ptr->inactive4_ = false;
+            LOG_INFO(dhcp6_logger, DHCP6_ACTIVATE_INTERFACE)
+                .arg(iface->getFullName());
+
+        } else {
+            // For deactivating interface, it should be sufficient to log it
+            // on the debug level because it is more useful to know what
+            // interface is activated which is logged on the info level.
+            LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC,
+                      DHCP6_DEACTIVATE_INTERFACE).arg(iface->getName());
+            iface_ptr->inactive6_ = true;
+
+        }
+    }
+    // Let's reopen active sockets. openSockets6 will check internally whether
+    // sockets are marked active or inactive.
+    // @todo Optimization: we should not reopen all sockets but rather select
+    // those that have been affected by the new configuration.
+    if (!IfaceMgr::instance().openSockets6(port)) {
+        LOG_WARN(dhcp6_logger, DHCP6_NO_SOCKETS_OPEN);
+    }
+}
+
 };
 };

+ 57 - 1
src/bin/dhcp6/dhcp6_srv.h

@@ -23,6 +23,7 @@
 #include <dhcp/pkt6.h>
 #include <dhcpsrv/alloc_engine.h>
 #include <dhcpsrv/subnet.h>
+#include <hooks/callout_handle.h>
 
 #include <boost/noncopyable.hpp>
 
@@ -87,6 +88,32 @@ public:
     /// @brief Instructs the server to shut down.
     void shutdown();
 
+    /// @brief Get UDP port on which server should listen.
+    ///
+    /// Typically, server listens on UDP port 547. Other ports are only
+    /// used for testing purposes.
+    ///
+    /// This accessor must be public because sockets are reopened from the
+    /// static configuration callback handler. This callback handler invokes
+    /// @c ControlledDhcpv4Srv::openActiveSockets which requires port parameter
+    /// which has to be retrieved from the @c ControlledDhcpv4Srv object.
+    /// They are retrieved using this public function.
+    ///
+    /// @return UDP port on which server should listen.
+    uint16_t getPort() const {
+        return (port_);
+    }
+
+    /// @brief Open sockets which are marked as active in @c CfgMgr.
+    ///
+    /// This function reopens sockets according to the current settings in the
+    /// Configuration Manager. It holds the list of the interfaces which server
+    /// should listen on. This function will open sockets on these interfaces
+    /// only. This function is not exception safe.
+    ///
+    /// @param port UDP port on which server should listen.
+    static void openActiveSockets(const uint16_t port);
+
 protected:
 
     /// @brief verifies if specified packet meets RFC requirements
@@ -189,7 +216,8 @@ protected:
     OptionPtr assignIA_NA(const isc::dhcp::Subnet6Ptr& subnet,
                           const isc::dhcp::DuidPtr& duid,
                           isc::dhcp::Pkt6Ptr question,
-                          boost::shared_ptr<Option6IA> ia);
+                          boost::shared_ptr<Option6IA> ia,
+                          const Pkt6Ptr& query);
 
     /// @brief Renews specific IA_NA option
     ///
@@ -321,6 +349,19 @@ protected:
     /// @return string representation
     static std::string duidToString(const OptionPtr& opt);
 
+
+    /// @brief dummy wrapper around IfaceMgr::receive6
+    ///
+    /// This method is useful for testing purposes, where its replacement
+    /// simulates reception of a packet. For that purpose it is protected.
+    virtual Pkt6Ptr receivePacket(int timeout);
+
+    /// @brief dummy wrapper around IfaceMgr::send()
+    ///
+    /// This method is useful for testing purposes, where its replacement
+    /// simulates transmission of a packet. For that purpose it is protected.
+    virtual void sendPacket(const Pkt6Ptr& pkt);
+
 private:
     /// @brief Allocation Engine.
     /// Pointer to the allocation engine that we are currently using
@@ -334,6 +375,21 @@ private:
     /// Indicates if shutdown is in progress. Setting it to true will
     /// initiate server shutdown procedure.
     volatile bool shutdown_;
+
+    /// @brief returns callout handle for specified packet
+    ///
+    /// @param pkt packet for which the handle should be returned
+    ///
+    /// @return a callout handle to be used in hooks related to said packet
+    isc::hooks::CalloutHandlePtr getCalloutHandle(const Pkt6Ptr& pkt);
+
+    /// Indexes for registered hook points
+    int hook_index_pkt6_receive_;
+    int hook_index_subnet6_select_;
+    int hook_index_pkt6_send_;
+
+    /// UDP port number on which server listens.
+    uint16_t port_;
 };
 
 }; // namespace isc::dhcp

+ 1 - 1
src/bin/dhcp6/tests/Makefile.am

@@ -77,10 +77,10 @@ dhcp6_unittests_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
+dhcp6_unittests_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
-
 endif
 
 noinst_PROGRAMS = $(TESTS)

+ 99 - 20
src/bin/dhcp6/tests/config_parser_unittest.cc

@@ -75,6 +75,9 @@ public:
                           << " while the test assumes that it doesn't, to execute"
                           << " some negative scenarios. Can't continue this test.";
         }
+
+        // Reset configuration for each test.
+        resetConfiguration();
     }
 
     ~Dhcp6ParserTest() {
@@ -146,7 +149,7 @@ public:
                                        std::string>& params)
     {
         std::ostringstream stream;
-        stream << "{ \"interface\": [ \"all\" ],"
+        stream << "{ \"interfaces\": [ \"*\" ],"
             "\"preferred-lifetime\": 3000,"
             "\"rebind-timer\": 2000, "
             "\"renew-timer\": 1000, "
@@ -240,7 +243,7 @@ public:
     /// test to make sure that contents of the database do not affect the
     /// results of subsequent tests.
     void resetConfiguration() {
-        string config = "{ \"interface\": [ \"all\" ],"
+        string config = "{ \"interfaces\": [ \"all\" ],"
             "\"hooks-libraries\": [ ],"
             "\"preferred-lifetime\": 3000,"
             "\"rebind-timer\": 2000, "
@@ -251,6 +254,11 @@ public:
             "\"option-data\": [ ] }";
         static_cast<void>(executeConfiguration(config,
                                                "reset configuration database"));
+        // The default setting is to listen on all interfaces. In order to
+        // properly test interface configuration we disable listening on
+        // all interfaces before each test and later check that this setting
+        // has been overriden by the configuration used in the test.
+        CfgMgr::instance().deleteActiveIfaces();
     }
 
     /// @brief Test invalid option parameter value.
@@ -415,7 +423,7 @@ TEST_F(Dhcp6ParserTest, emptySubnet) {
     ConstElementPtr status;
 
     EXPECT_NO_THROW(status = configureDhcp6Server(srv_,
-                    Element::fromJSON("{ \"interface\": [ \"all\" ],"
+                    Element::fromJSON("{ \"interfaces\": [ \"*\" ],"
                                       "\"preferred-lifetime\": 3000,"
                                       "\"rebind-timer\": 2000, "
                                       "\"renew-timer\": 1000, "
@@ -434,7 +442,7 @@ TEST_F(Dhcp6ParserTest, subnetGlobalDefaults) {
 
     ConstElementPtr status;
 
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"preferred-lifetime\": 3000,"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
@@ -468,7 +476,7 @@ TEST_F(Dhcp6ParserTest, subnetLocal) {
 
     ConstElementPtr status;
 
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"preferred-lifetime\": 3000,"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
@@ -506,7 +514,7 @@ TEST_F(Dhcp6ParserTest, subnetInterface) {
 
     // There should be at least one interface
 
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"preferred-lifetime\": 3000,"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
@@ -539,7 +547,7 @@ TEST_F(Dhcp6ParserTest, subnetInterfaceBogus) {
 
     // There should be at least one interface
 
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"preferred-lifetime\": 3000,"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
@@ -570,7 +578,7 @@ TEST_F(Dhcp6ParserTest, interfaceGlobal) {
 
     ConstElementPtr status;
 
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"preferred-lifetime\": 3000,"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
@@ -640,7 +648,7 @@ TEST_F(Dhcp6ParserTest, subnetInterfaceId) {
 // parameter.
 TEST_F(Dhcp6ParserTest, interfaceIdGlobal) {
 
-    const string config = "{ \"interface\": [ \"all\" ],"
+    const string config = "{ \"interfaces\": [ \"*\" ],"
         "\"preferred-lifetime\": 3000,"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
@@ -695,7 +703,7 @@ TEST_F(Dhcp6ParserTest, poolOutOfSubnet) {
 
     ConstElementPtr status;
 
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"preferred-lifetime\": 3000,"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
@@ -723,7 +731,7 @@ TEST_F(Dhcp6ParserTest, poolPrefixLen) {
 
     ConstElementPtr x;
 
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"preferred-lifetime\": 3000,"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
@@ -1243,7 +1251,7 @@ TEST_F(Dhcp6ParserTest, optionStandardDefOverride) {
 // configuration does not include options configuration.
 TEST_F(Dhcp6ParserTest, optionDataDefaults) {
     ConstElementPtr x;
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"preferred-lifetime\": 3000,"
         "\"rebind-timer\": 2000,"
         "\"renew-timer\": 1000,"
@@ -1325,7 +1333,7 @@ TEST_F(Dhcp6ParserTest, optionDataTwoSpaces) {
     // The definition is not required for the option that
     // belongs to the 'dhcp6' option space as it is the
     // standard option.
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000,"
         "\"renew-timer\": 1000,"
         "\"option-data\": [ {"
@@ -1403,7 +1411,7 @@ TEST_F(Dhcp6ParserTest, optionDataEncapsulate) {
     // at the very end (when all other parameters are configured).
 
     // Starting stage 1. Configure sub-options and their definitions.
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000,"
         "\"renew-timer\": 1000,"
         "\"option-data\": [ {"
@@ -1452,7 +1460,7 @@ TEST_F(Dhcp6ParserTest, optionDataEncapsulate) {
     // the configuration from the stage 2 is repeated because BIND
     // configuration manager sends whole configuration for the lists
     // where at least one element is being modified or added.
-    config = "{ \"interface\": [ \"all\" ],"
+    config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000,"
         "\"renew-timer\": 1000,"
         "\"option-data\": [ {"
@@ -1546,7 +1554,7 @@ TEST_F(Dhcp6ParserTest, optionDataEncapsulate) {
 // for multiple subnets.
 TEST_F(Dhcp6ParserTest, optionDataInMultipleSubnets) {
     ConstElementPtr x;
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"preferred-lifetime\": 3000,"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
@@ -1789,7 +1797,7 @@ TEST_F(Dhcp6ParserTest, stdOptionDataEncapsulate) {
     // In the first stahe we create definitions of suboptions
     // that we will add to the base option.
     // Let's create some dummy options: foo and foo2.
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000,"
         "\"renew-timer\": 1000,"
         "\"option-data\": [ {"
@@ -1842,7 +1850,7 @@ TEST_F(Dhcp6ParserTest, stdOptionDataEncapsulate) {
     // We add our dummy options to this option space and thus
     // they should be included as sub-options in the 'vendor-opts'
     // option.
-    config = "{ \"interface\": [ \"all\" ],"
+    config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000,"
         "\"renew-timer\": 1000,"
         "\"option-data\": [ {"
@@ -1951,7 +1959,7 @@ buildHooksLibrariesConfig(const std::vector<std::string>& libraries) {
 
     // Create the first part of the configuration string.
     string config =
-        "{ \"interface\": [ \"all\" ],"
+        "{ \"interfaces\": [ \"all\" ],"
             "\"hooks-libraries\": [";
 
     // Append the libraries (separated by commas if needed)
@@ -2070,7 +2078,7 @@ TEST_F(Dhcp6ParserTest, LibrariesSpecified) {
     string config = buildHooksLibrariesConfig(CALLOUT_LIBRARY_1,
                                               CALLOUT_LIBRARY_2);
     ASSERT_TRUE(executeConfiguration(config,
-                                     "loading two valid libraries"));
+                                     "load two valid libraries"));
 
     // Expect two libraries to be loaded in the correct order (load marker file
     // is present, no unload marker file).
@@ -2095,6 +2103,77 @@ TEST_F(Dhcp6ParserTest, LibrariesSpecified) {
 //   ASSERT_TRUE(libraries.empty());
 
 
+// This test verifies that it is possible to select subset of interfaces on
+// which server should listen.
+TEST_F(Dhcp6ParserTest, selectedInterfaces) {
+
+    // Make sure there is no garbage interface configuration in the CfgMgr.
+    ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth0"));
+    ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth1"));
+    ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth2"));
+
+    ConstElementPtr status;
+
+    string config = "{ \"interfaces\": [ \"eth0\", \"eth1\" ],"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"valid-lifetime\": 4000 }";
+
+
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+
+    // returned value must be 1 (values error)
+    // as the pool does not belong to that subnet
+    ASSERT_TRUE(status);
+    comment_ = parseAnswer(rcode_, status);
+    EXPECT_EQ(0, rcode_);
+
+    // eth0 and eth1 were explicitly selected. eth2 was not.
+    EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth0"));
+    EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth1"));
+    EXPECT_FALSE(CfgMgr::instance().isActiveIface("eth2"));
+}
+
+// This test verifies that it is possible to configure the server to listen on
+// all interfaces.
+TEST_F(Dhcp6ParserTest, allInterfaces) {
+
+    // Make sure there is no garbage interface configuration in the CfgMgr.
+    ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth0"));
+    ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth1"));
+    ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth2"));
+
+    ConstElementPtr status;
+
+    // This configuration specifies two interfaces on which server should listen
+    // bu also includes keyword 'all'. This keyword switches server into the
+    // mode when it listens on all interfaces regardless of what interface names
+    // were specified in the "interfaces" parameter.
+    string config = "{ \"interfaces\": [ \"eth0\", \"eth1\", \"*\" ],"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"valid-lifetime\": 4000 }";
+
+
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+
+    // returned value must be 1 (values error)
+    // as the pool does not belong to that subnet
+    ASSERT_TRUE(status);
+    comment_ = parseAnswer(rcode_, status);
+    EXPECT_EQ(0, rcode_);
+
+    // All interfaces should be now active.
+    EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth0"));
+    EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth1"));
+    EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth2"));
+}
 
 
 };

+ 761 - 2
src/bin/dhcp6/tests/dhcp6_srv_unittest.cc

@@ -24,14 +24,18 @@
 #include <dhcp/option6_ia.h>
 #include <dhcp/option6_iaaddr.h>
 #include <dhcp/option_int_array.h>
+#include <dhcp/iface_mgr.h>
 #include <dhcp6/config_parser.h>
 #include <dhcp6/dhcp6_srv.h>
+#include <dhcp/dhcp6.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcpsrv/utils.h>
 #include <util/buffer.h>
 #include <util/range_utilities.h>
+#include <hooks/server_hooks.h>
+#include <hooks/hooks_manager.h>
 
 #include <boost/scoped_ptr.hpp>
 #include <gtest/gtest.h>
@@ -39,6 +43,7 @@
 #include <fstream>
 #include <iostream>
 #include <sstream>
+#include <list>
 
 using namespace isc;
 using namespace isc::asiolink;
@@ -46,6 +51,7 @@ using namespace isc::config;
 using namespace isc::data;
 using namespace isc::dhcp;
 using namespace isc::util;
+using namespace isc::hooks;
 using namespace std;
 
 // namespace has to be named, because friends are defined in Dhcpv6Srv class
@@ -61,6 +67,46 @@ public:
         LeaseMgrFactory::create(memfile);
     }
 
+    /// @brief fakes packet reception
+    /// @param timeout ignored
+    ///
+    /// The method receives all packets queued in receive
+    /// queue, one after another. Once the queue is empty,
+    /// it initiates the shutdown procedure.
+    ///
+    /// See fake_received_ field for description
+    virtual Pkt6Ptr receivePacket(int /*timeout*/) {
+
+        // If there is anything prepared as fake incoming
+        // traffic, use it
+        if (!fake_received_.empty()) {
+            Pkt6Ptr pkt = fake_received_.front();
+            fake_received_.pop_front();
+            return (pkt);
+        }
+
+        // If not, just trigger shutdown and
+        // return immediately
+        shutdown();
+        return (Pkt6Ptr());
+    }
+
+    /// @brief fake packet sending
+    ///
+    /// Pretend to send a packet, but instead just store
+    /// it in fake_send_ list where test can later inspect
+    /// server's response.
+    virtual void sendPacket(const Pkt6Ptr& pkt) {
+        fake_sent_.push_back(pkt);
+    }
+
+    /// @brief adds a packet to fake receive queue
+    ///
+    /// See fake_received_ field for description
+    void fakeReceive(const Pkt6Ptr& pkt) {
+        fake_received_.push_back(pkt);
+    }
+
     virtual ~NakedDhcpv6Srv() {
         // Close the lease database
         LeaseMgrFactory::destroy();
@@ -75,6 +121,16 @@ public:
     using Dhcpv6Srv::sanityCheck;
     using Dhcpv6Srv::loadServerID;
     using Dhcpv6Srv::writeServerID;
+
+    /// @brief packets we pretend to receive
+    ///
+    /// Instead of setting up sockets on interfaces that change between OSes, it
+    /// is much easier to fake packet reception. This is a list of packets that
+    /// we pretend to have received. You can schedule new packets to be received
+    /// using fakeReceive() and NakedDhcpv6Srv::receivePacket() methods.
+    list<Pkt6Ptr> fake_received_;
+
+    list<Pkt6Ptr> fake_sent_;
 };
 
 static const char* DUID_FILE = "server-id-test.txt";
@@ -87,6 +143,16 @@ public:
     NakedDhcpv6SrvTest() : rcode_(-1) {
         // it's ok if that fails. There should not be such a file anyway
         unlink(DUID_FILE);
+
+        const IfaceMgr::IfaceCollection& ifaces = IfaceMgr::instance().getIfaces();
+
+        // There must be some interface detected
+        if (ifaces.empty()) {
+            // We can't use ASSERT in constructor
+            ADD_FAILURE() << "No interfaces detected.";
+        }
+
+        valid_iface_ = ifaces.begin()->getName();
     }
 
     // Generate IA_NA option with specified parameters
@@ -228,6 +294,13 @@ public:
     virtual ~NakedDhcpv6SrvTest() {
         // Let's clean up if there is such a file.
         unlink(DUID_FILE);
+        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+                                                 "pkt6_receive");
+        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+                                                 "pkt6_send");
+        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+                                                 "subnet6_select");
+
     };
 
     // A DUID used in most tests (typically as client-id)
@@ -235,6 +308,9 @@ public:
 
     int rcode_;
     ConstElementPtr comment_;
+
+    // Name of a valid network interface
+    string valid_iface_;
 };
 
 // Provides suport for tests against a preconfigured subnet6
@@ -480,7 +556,7 @@ TEST_F(Dhcpv6SrvTest, DUID) {
 
     boost::scoped_ptr<Dhcpv6Srv> srv;
     ASSERT_NO_THROW( {
-        srv.reset(new Dhcpv6Srv(0));
+        srv.reset(new NakedDhcpv6Srv(0));
     });
 
     OptionPtr srvid = srv->getServerID();
@@ -555,7 +631,7 @@ TEST_F(Dhcpv6SrvTest, DUID) {
 // and the requested options are actually assigned.
 TEST_F(Dhcpv6SrvTest, advertiseOptions) {
     ConstElementPtr x;
-    string config = "{ \"interface\": [ \"all\" ],"
+    string config = "{ \"interfaces\": [ \"all\" ],"
         "\"preferred-lifetime\": 3000,"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
@@ -1756,6 +1832,689 @@ TEST_F(Dhcpv6SrvTest, ServerID) {
     EXPECT_EQ(duid1_text, text);
 }
 
+// Checks if hooks are implemented properly.
+TEST_F(Dhcpv6SrvTest, Hooks) {
+    NakedDhcpv6Srv srv(0);
+
+    // check if appropriate hooks are registered
+    int hook_index_pkt6_received = -1;
+    int hook_index_select_subnet = -1;
+    int hook_index_pkt6_send     = -1;
+
+    // check if appropriate indexes are set
+    EXPECT_NO_THROW(hook_index_pkt6_received = ServerHooks::getServerHooks()
+                    .getIndex("pkt6_receive"));
+    EXPECT_NO_THROW(hook_index_select_subnet = ServerHooks::getServerHooks()
+                    .getIndex("subnet6_select"));
+    EXPECT_NO_THROW(hook_index_pkt6_send     = ServerHooks::getServerHooks()
+                    .getIndex("pkt6_send"));
+
+    EXPECT_TRUE(hook_index_pkt6_received > 0);
+    EXPECT_TRUE(hook_index_select_subnet > 0);
+    EXPECT_TRUE(hook_index_pkt6_send > 0);
+}
+
+// This function returns buffer for very simple Solicit
+Pkt6* captureSimpleSolicit() {
+    Pkt6* pkt;
+    uint8_t data[] = {
+        1,  // type 1 = SOLICIT
+        0xca, 0xfe, 0x01, // trans-id = 0xcafe01
+        0, 1, // option type 1 (client-id)
+        0, 10, // option lenth 10
+        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // DUID
+        0, 3, // option type 3 (IA_NA)
+        0, 12, // option length 12
+        0, 0, 0, 1, // iaid = 1
+        0, 0, 0, 0, // T1 = 0
+        0, 0, 0, 0  // T2 = 0
+    };
+
+    pkt = new Pkt6(data, sizeof(data));
+    pkt->setRemotePort(546);
+    pkt->setRemoteAddr(IOAddress("fe80::1"));
+    pkt->setLocalPort(0);
+    pkt->setLocalAddr(IOAddress("ff02::1:2"));
+    pkt->setIndex(2);
+    pkt->setIface("eth0");
+
+    return (pkt);
+}
+
+/// @brief a class dedicated to Hooks testing in DHCPv6 server
+///
+/// This class has a number of static members, because each non-static
+/// method has implicit 'this' parameter, so it does not match callout
+/// signature and couldn't be registered. Furthermore, static methods
+/// can't modify non-static members (for obvious reasons), so many
+/// fields are declared static. It is still better to keep them as
+/// one class rather than unrelated collection of global objects.
+class HooksDhcpv6SrvTest : public Dhcpv6SrvTest {
+
+public:
+
+    /// @brief creates Dhcpv6Srv and prepares buffers for callouts
+    HooksDhcpv6SrvTest() {
+
+        // Allocate new DHCPv6 Server
+        srv_ = new NakedDhcpv6Srv(0);
+
+        // clear static buffers
+        resetCalloutBuffers();
+    }
+
+    /// @brief destructor (deletes Dhcpv6Srv)
+    ~HooksDhcpv6SrvTest() {
+        delete srv_;
+    }
+
+    /// @brief creates an option with specified option code
+    ///
+    /// This method is static, because it is used from callouts
+    /// that do not have a pointer to HooksDhcpv6SSrvTest object
+    ///
+    /// @param option_code code of option to be created
+    ///
+    /// @return pointer to create option object
+    static OptionPtr createOption(uint16_t option_code) {
+
+        char payload[] = {
+            0xa, 0xb, 0xc, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14
+        };
+
+        OptionBuffer tmp(payload, payload + sizeof(payload));
+        return OptionPtr(new Option(Option::V6, option_code, tmp));
+    }
+
+    /// test callback that stores received callout name and pkt6 value
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_receive_callout(CalloutHandle& callout_handle) {
+        callback_name_ = string("pkt6_receive");
+
+        callout_handle.getArgument("query6", callback_pkt6_);
+
+        callback_argument_names_ = callout_handle.getArgumentNames();
+        return (0);
+    }
+
+    /// test callback that changes client-id value
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_receive_change_clientid(CalloutHandle& callout_handle) {
+
+        Pkt6Ptr pkt;
+        callout_handle.getArgument("query6", pkt);
+
+        // get rid of the old client-id
+        pkt->delOption(D6O_CLIENTID);
+
+        // add a new option
+        pkt->addOption(createOption(D6O_CLIENTID));
+
+        // carry on as usual
+        return pkt6_receive_callout(callout_handle);
+    }
+
+    /// test callback that deletes client-id
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_receive_delete_clientid(CalloutHandle& callout_handle) {
+
+        Pkt6Ptr pkt;
+        callout_handle.getArgument("query6", pkt);
+
+        // get rid of the old client-id
+        pkt->delOption(D6O_CLIENTID);
+
+        // carry on as usual
+        return pkt6_receive_callout(callout_handle);
+    }
+
+    /// test callback that sets skip flag
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_receive_skip(CalloutHandle& callout_handle) {
+
+        Pkt6Ptr pkt;
+        callout_handle.getArgument("query6", pkt);
+
+        callout_handle.setSkip(true);
+
+        // carry on as usual
+        return pkt6_receive_callout(callout_handle);
+    }
+
+    /// Test callback that stores received callout name and pkt6 value
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_send_callout(CalloutHandle& callout_handle) {
+        callback_name_ = string("pkt6_send");
+
+        callout_handle.getArgument("response6", callback_pkt6_);
+
+        callback_argument_names_ = callout_handle.getArgumentNames();
+        return (0);
+    }
+
+    // Test callback that changes server-id
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_send_change_serverid(CalloutHandle& callout_handle) {
+
+        Pkt6Ptr pkt;
+        callout_handle.getArgument("response6", pkt);
+
+        // get rid of the old server-id
+        pkt->delOption(D6O_SERVERID);
+
+        // add a new option
+        pkt->addOption(createOption(D6O_SERVERID));
+
+        // carry on as usual
+        return pkt6_send_callout(callout_handle);
+    }
+
+    /// test callback that deletes server-id
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_send_delete_serverid(CalloutHandle& callout_handle) {
+
+        Pkt6Ptr pkt;
+        callout_handle.getArgument("response6", pkt);
+
+        // get rid of the old client-id
+        pkt->delOption(D6O_SERVERID);
+
+        // carry on as usual
+        return pkt6_send_callout(callout_handle);
+    }
+
+    /// Test callback that sets skip flag
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_send_skip(CalloutHandle& callout_handle) {
+
+        Pkt6Ptr pkt;
+        callout_handle.getArgument("response6", pkt);
+
+        callout_handle.setSkip(true);
+
+        // carry on as usual
+        return pkt6_send_callout(callout_handle);
+    }
+
+    /// Test callback that stores received callout name and subnet6 values
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    subnet6_select_callout(CalloutHandle& callout_handle) {
+        callback_name_ = string("subnet6_select");
+
+        callout_handle.getArgument("query6", callback_pkt6_);
+        callout_handle.getArgument("subnet6", callback_subnet6_);
+        callout_handle.getArgument("subnet6collection", callback_subnet6collection_);
+
+        callback_argument_names_ = callout_handle.getArgumentNames();
+        return (0);
+    }
+
+    /// Test callback that picks the other subnet if possible.
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    subnet6_select_different_subnet_callout(CalloutHandle& callout_handle) {
+
+        // Call the basic calllout to record all passed values
+        subnet6_select_callout(callout_handle);
+
+        const Subnet6Collection* subnets;
+        Subnet6Ptr subnet;
+        callout_handle.getArgument("subnet6", subnet);
+        callout_handle.getArgument("subnet6collection", subnets);
+
+        // Let's change to a different subnet
+        if (subnets->size() > 1) {
+            subnet = (*subnets)[1]; // Let's pick the other subnet
+            callout_handle.setArgument("subnet6", subnet);
+        }
+
+        return (0);
+    }
+
+    /// resets buffers used to store data received by callouts
+    void resetCalloutBuffers() {
+        callback_name_ = string("");
+        callback_pkt6_.reset();
+        callback_subnet6_.reset();
+        callback_subnet6collection_ = NULL;
+        callback_argument_names_.clear();
+    }
+
+    /// pointer to Dhcpv6Srv that is used in tests
+    NakedDhcpv6Srv* srv_;
+
+    // The following fields are used in testing pkt6_receive_callout
+
+    /// String name of the received callout
+    static string callback_name_;
+
+    /// Pkt6 structure returned in the callout
+    static Pkt6Ptr callback_pkt6_;
+
+    /// Pointer to a subnet received by callout
+    static Subnet6Ptr callback_subnet6_;
+
+    /// A list of all available subnets (received by callout)
+    static const Subnet6Collection* callback_subnet6collection_;
+
+    /// A list of all received arguments
+    static vector<string> callback_argument_names_;
+};
+
+// The following fields are used in testing pkt6_receive_callout.
+// See fields description in the class for details
+string HooksDhcpv6SrvTest::callback_name_;
+Pkt6Ptr HooksDhcpv6SrvTest::callback_pkt6_;
+Subnet6Ptr HooksDhcpv6SrvTest::callback_subnet6_;
+const Subnet6Collection* HooksDhcpv6SrvTest::callback_subnet6collection_;
+vector<string> HooksDhcpv6SrvTest::callback_argument_names_;
+
+
+// Checks if callouts installed on pkt6_received are indeed called and the
+// all necessary parameters are passed.
+//
+// Note that the test name does not follow test naming convention,
+// but the proper hook name is "pkt6_receive".
+TEST_F(HooksDhcpv6SrvTest, simple_pkt6_receive) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_receive", pkt6_receive_callout));
+
+    // Let's create a simple SOLICIT
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // check that the callback called is indeed the one we installed
+    EXPECT_EQ("pkt6_receive", callback_name_);
+
+    // check that pkt6 argument passing was successful and returned proper value
+    EXPECT_TRUE(callback_pkt6_.get() == sol.get());
+
+    // Check that all expected parameters are there
+    vector<string> expected_argument_names;
+    expected_argument_names.push_back(string("query6"));
+
+    EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+}
+
+// Checks if callouts installed on pkt6_received is able to change
+// the values and the parameters are indeed used by the server.
+TEST_F(HooksDhcpv6SrvTest, valueChange_pkt6_receive) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_receive", pkt6_receive_change_clientid));
+
+    // Let's create a simple SOLICIT
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // check that the server did send a reposonce
+    ASSERT_EQ(1, srv_->fake_sent_.size());
+
+    // Make sure that we received a response
+    Pkt6Ptr adv = srv_->fake_sent_.front();
+    ASSERT_TRUE(adv);
+
+    // Get client-id...
+    OptionPtr clientid = adv->getOption(D6O_CLIENTID);
+
+    // ... and check if it is the modified value
+    OptionPtr expected = createOption(D6O_CLIENTID);
+    EXPECT_TRUE(clientid->equal(expected));
+}
+
+// Checks if callouts installed on pkt6_received is able to delete
+// existing options and that change impacts server processing (mandatory
+// client-id option is deleted, so the packet is expected to be dropped)
+TEST_F(HooksDhcpv6SrvTest, deleteClientId_pkt6_receive) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_receive", pkt6_receive_delete_clientid));
+
+    // Let's create a simple SOLICIT
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // Check that the server dropped the packet and did not send a response
+    ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+// Checks if callouts installed on pkt6_received is able to set skip flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv6SrvTest, skip_pkt6_receive) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_receive", pkt6_receive_skip));
+
+    // Let's create a simple SOLICIT
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // check that the server dropped the packet and did not produce any response
+    ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+
+// Checks if callouts installed on pkt6_send are indeed called and the
+// all necessary parameters are passed.
+TEST_F(HooksDhcpv6SrvTest, simple_pkt6_send) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_send", pkt6_send_callout));
+
+    // Let's create a simple SOLICIT
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // Check that the callback called is indeed the one we installed
+    EXPECT_EQ("pkt6_send", callback_name_);
+
+    // Check that there is one packet sent
+    ASSERT_EQ(1, srv_->fake_sent_.size());
+    Pkt6Ptr adv = srv_->fake_sent_.front();
+
+    // Check that pkt6 argument passing was successful and returned proper value
+    EXPECT_TRUE(callback_pkt6_.get() == adv.get());
+
+    // Check that all expected parameters are there
+    vector<string> expected_argument_names;
+    expected_argument_names.push_back(string("response6"));
+    EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+}
+
+// Checks if callouts installed on pkt6_send is able to change
+// the values and the packet sent contains those changes
+TEST_F(HooksDhcpv6SrvTest, valueChange_pkt6_send) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_send", pkt6_send_change_serverid));
+
+    // Let's create a simple SOLICIT
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // check that the server did send a reposonce
+    ASSERT_EQ(1, srv_->fake_sent_.size());
+
+    // Make sure that we received a response
+    Pkt6Ptr adv = srv_->fake_sent_.front();
+    ASSERT_TRUE(adv);
+
+    // Get client-id...
+    OptionPtr clientid = adv->getOption(D6O_SERVERID);
+
+    // ... and check if it is the modified value
+    OptionPtr expected = createOption(D6O_SERVERID);
+    EXPECT_TRUE(clientid->equal(expected));
+}
+
+// Checks if callouts installed on pkt6_send is able to delete
+// existing options and that server applies those changes. In particular,
+// we are trying to send a packet without server-id. The packet should
+// be sent
+TEST_F(HooksDhcpv6SrvTest, deleteServerId_pkt6_send) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_send", pkt6_send_delete_serverid));
+
+    // Let's create a simple SOLICIT
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // Check that the server indeed sent a malformed ADVERTISE
+    ASSERT_EQ(1, srv_->fake_sent_.size());
+
+    // Get that ADVERTISE
+    Pkt6Ptr adv = srv_->fake_sent_.front();
+    ASSERT_TRUE(adv);
+
+    // Make sure that it does not have server-id
+    EXPECT_FALSE(adv->getOption(D6O_SERVERID));
+}
+
+// Checks if callouts installed on pkt6_skip is able to set skip flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv6SrvTest, skip_pkt6_send) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_send", pkt6_send_skip));
+
+    // Let's create a simple REQUEST
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // check that the server dropped the packet and did not produce any response
+    ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+// This test checks if subnet6_select callout is triggered and reports
+// valid parameters
+TEST_F(HooksDhcpv6SrvTest, subnet6_select) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "subnet6_select", subnet6_select_callout));
+
+    // Configure 2 subnets, both directly reachable over local interface
+    // (let's not complicate the matter with relays)
+    string config = "{ \"interfaces\": [ \"*\" ],"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ { "
+        "    \"pool\": [ \"2001:db8:1::/64\" ],"
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"interface\": \"" + valid_iface_ + "\" "
+        " }, {"
+        "    \"pool\": [ \"2001:db8:2::/64\" ],"
+        "    \"subnet\": \"2001:db8:2::/48\" "
+        " } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json = Element::fromJSON(config);
+    ConstElementPtr status;
+
+    // Configure the server and make sure the config is accepted
+    EXPECT_NO_THROW(status = configureDhcp6Server(*srv_, json));
+    ASSERT_TRUE(status);
+    comment_ = parseAnswer(rcode_, status);
+    ASSERT_EQ(0, rcode_);
+
+    // Prepare solicit packet. Server should select first subnet for it
+    Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+    sol->setRemoteAddr(IOAddress("fe80::abcd"));
+    sol->setIface(valid_iface_);
+    sol->addOption(generateIA(234, 1500, 3000));
+    OptionPtr clientid = generateClientId();
+    sol->addOption(clientid);
+
+    // Pass it to the server and get an advertise
+    Pkt6Ptr adv = srv_->processSolicit(sol);
+
+    // check if we get response at all
+    ASSERT_TRUE(adv);
+
+    // Check that the callback called is indeed the one we installed
+    EXPECT_EQ("subnet6_select", callback_name_);
+
+    // Check that pkt6 argument passing was successful and returned proper value
+    EXPECT_TRUE(callback_pkt6_.get() == sol.get());
+
+    const Subnet6Collection* exp_subnets = CfgMgr::instance().getSubnets6();
+
+    // The server is supposed to pick the first subnet, because of matching
+    // interface. Check that the value is reported properly.
+    ASSERT_TRUE(callback_subnet6_);
+    EXPECT_EQ(callback_subnet6_.get(), exp_subnets->front().get());
+
+    // Server is supposed to report two subnets
+    ASSERT_EQ(exp_subnets->size(), callback_subnet6collection_->size());
+
+    // Compare that the available subnets are reported as expected
+    EXPECT_TRUE((*exp_subnets)[0].get() == (*callback_subnet6collection_)[0].get());
+    EXPECT_TRUE((*exp_subnets)[1].get() == (*callback_subnet6collection_)[1].get());
+}
+
+// This test checks if callout installed on subnet6_select hook point can pick
+// a different subnet.
+TEST_F(HooksDhcpv6SrvTest, subnet_select_change) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "subnet6_select", subnet6_select_different_subnet_callout));
+
+    // Configure 2 subnets, both directly reachable over local interface
+    // (let's not complicate the matter with relays)
+    string config = "{ \"interfaces\": [ \"*\" ],"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ { "
+        "    \"pool\": [ \"2001:db8:1::/64\" ],"
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"interface\": \"" + valid_iface_ + "\" "
+        " }, {"
+        "    \"pool\": [ \"2001:db8:2::/64\" ],"
+        "    \"subnet\": \"2001:db8:2::/48\" "
+        " } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json = Element::fromJSON(config);
+    ConstElementPtr status;
+
+    // Configure the server and make sure the config is accepted
+    EXPECT_NO_THROW(status = configureDhcp6Server(*srv_, json));
+    ASSERT_TRUE(status);
+    comment_ = parseAnswer(rcode_, status);
+    ASSERT_EQ(0, rcode_);
+
+    // Prepare solicit packet. Server should select first subnet for it
+    Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+    sol->setRemoteAddr(IOAddress("fe80::abcd"));
+    sol->setIface(valid_iface_);
+    sol->addOption(generateIA(234, 1500, 3000));
+    OptionPtr clientid = generateClientId();
+    sol->addOption(clientid);
+
+    // Pass it to the server and get an advertise
+    Pkt6Ptr adv = srv_->processSolicit(sol);
+
+    // check if we get response at all
+    ASSERT_TRUE(adv);
+
+    // The response should have an address from second pool, so let's check it
+    OptionPtr tmp = adv->getOption(D6O_IA_NA);
+    ASSERT_TRUE(tmp);
+    boost::shared_ptr<Option6IA> ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
+    ASSERT_TRUE(ia);
+    tmp = ia->getOption(D6O_IAADDR);
+    ASSERT_TRUE(tmp);
+    boost::shared_ptr<Option6IAAddr> addr_opt =
+        boost::dynamic_pointer_cast<Option6IAAddr>(tmp);
+    ASSERT_TRUE(addr_opt);
+
+    // Get all subnets and use second subnet for verification
+    const Subnet6Collection* subnets = CfgMgr::instance().getSubnets6();
+    ASSERT_EQ(2, subnets->size());
+
+    // Advertised address must belong to the second pool (in subnet's range,
+    // in dynamic pool)
+    EXPECT_TRUE((*subnets)[1]->inRange(addr_opt->getAddress()));
+    EXPECT_TRUE((*subnets)[1]->inPool(addr_opt->getAddress()));
+}
+
+
 /// @todo: Add more negative tests for processX(), e.g. extend sanityCheck() test
 /// to call processX() methods.
 

+ 17 - 2
src/bin/memmgr/tests/memmgr_test.py

@@ -137,9 +137,24 @@ class TestMemmgr(unittest.TestCase):
         self.assertEqual(1, answer[0])
         self.assertIsNotNone(re.search('not a directory', answer[1]))
 
-        # Bad update: directory exists but is not readable.
-        os.mkdir(self.__test_mapped_file_dir, 0o500) # drop writable bit
+    @unittest.skipIf(os.getuid() == 0,
+                     'test cannot be run as root user')
+    def test_configure_bad_permissions(self):
+        self.__mgr._setup_ccsession()
+
+        # Pretend specified directories exist and writable
+        os.path.isdir = lambda x: True
+        os.access = lambda x, y: True
+
+        # Initial configuration.
+        self.assertEqual((0, None),
+                         parse_answer(self.__mgr._config_handler({})))
+
+        os.path.isdir = self.__orig_isdir
         os.access = self.__orig_os_access
+
+        # Bad update: directory exists but is not writable.
+        os.mkdir(self.__test_mapped_file_dir, 0o500) # drop writable bit
         user_cfg = {'mapped_file_dir': self.__test_mapped_file_dir}
         answer = parse_answer(self.__mgr._config_handler(user_cfg))
         self.assertEqual(1, answer[0])

+ 13 - 0
src/bin/usermgr/tests/b10-cmdctl-usermgr_test.py

@@ -399,6 +399,19 @@ Options:
                          'add', 'user1', 'pass1'
                        ])
 
+    @unittest.skipIf(os.getuid() == 0,
+                     'test cannot be run as root user')
+    def test_bad_file_permissions(self):
+        """
+        Check for graceful handling of bad file argument
+        """
+        # Create the test file
+        self.run_check(0, None, None,
+                       [ self.TOOL,
+                         '-f', self.OUTPUT_FILE,
+                         'add', 'user1', 'pass1'
+                       ])
+
         # Make it non-writable (don't worry about cleanup, the
         # file should be deleted after each test anyway
         os.chmod(self.OUTPUT_FILE, stat.S_IRUSR)

+ 6 - 3
src/lib/dhcp/iface_mgr.cc

@@ -51,7 +51,8 @@ IfaceMgr::instance() {
 Iface::Iface(const std::string& name, int ifindex)
     :name_(name), ifindex_(ifindex), mac_len_(0), hardware_type_(0),
      flag_loopback_(false), flag_up_(false), flag_running_(false),
-     flag_multicast_(false), flag_broadcast_(false), flags_(0)
+     flag_multicast_(false), flag_broadcast_(false), flags_(0),
+     inactive4_(false), inactive6_(false)
 {
     memset(mac_, 0, sizeof(mac_));
 }
@@ -295,7 +296,8 @@ bool IfaceMgr::openSockets4(const uint16_t port, const bool use_bcast) {
 
         if (iface->flag_loopback_ ||
             !iface->flag_up_ ||
-            !iface->flag_running_) {
+            !iface->flag_running_ ||
+            iface->inactive4_) {
             continue;
         }
 
@@ -361,7 +363,8 @@ bool IfaceMgr::openSockets6(const uint16_t port) {
 
         if (iface->flag_loopback_ ||
             !iface->flag_up_ ||
-            !iface->flag_running_) {
+            !iface->flag_running_ ||
+            iface->inactive6_) {
             continue;
         }
 

+ 8 - 0
src/lib/dhcp/iface_mgr.h

@@ -309,6 +309,14 @@ public:
     /// Interface flags (this value is as is returned by OS,
     /// it may mean different things on different OSes).
     uint32_t flags_;
+
+    /// Indicates that IPv4 sockets should (true) or should not (false)
+    /// be opened on this interface.
+    bool inactive4_;
+
+    /// Indicates that IPv6 sockets should (true) or should not (false)
+    /// be opened on this interface.
+    bool inactive6_;
 };
 
 /// @brief Handles network interfaces, transmission and reception.

+ 2 - 0
src/lib/dhcpsrv/Makefile.am

@@ -63,6 +63,8 @@ libb10_dhcpsrv_la_LIBADD  += $(top_builddir)/src/lib/hooks/libb10-hooks.la
 libb10_dhcpsrv_la_LIBADD  += $(top_builddir)/src/lib/log/libb10-log.la
 libb10_dhcpsrv_la_LIBADD  += $(top_builddir)/src/lib/util/libb10-util.la
 libb10_dhcpsrv_la_LIBADD  += $(top_builddir)/src/lib/cc/libb10-cc.la
+libb10_dhcpsrv_la_LIBADD  += $(top_builddir)/src/lib/hooks/libb10-hooks.la
+
 libb10_dhcpsrv_la_LDFLAGS  = -no-undefined -version-info 3:0:0
 if HAVE_MYSQL
 libb10_dhcpsrv_la_LDFLAGS += $(MYSQL_LIBS)

+ 191 - 10
src/lib/dhcpsrv/alloc_engine.cc

@@ -16,11 +16,37 @@
 #include <dhcpsrv/dhcpsrv_log.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 
+#include <hooks/server_hooks.h>
+#include <hooks/hooks_manager.h>
+
 #include <cstring>
 #include <vector>
 #include <string.h>
 
 using namespace isc::asiolink;
+using namespace isc::hooks;
+
+namespace {
+
+/// Structure that holds registered hook indexes
+struct AllocEngineHooks {
+    int hook_index_lease4_select_; ///< index for "lease4_receive" hook point
+    int hook_index_lease6_select_; ///< index for "lease6_receive" hook point
+
+    /// Constructor that registers hook points for AllocationEngine
+    AllocEngineHooks() {
+        hook_index_lease4_select_ = HooksManager::registerHook("lease4_select");
+        hook_index_lease6_select_ = HooksManager::registerHook("lease6_select");
+    }
+};
+
+// Declare a Hooks object. As this is outside any function or method, it
+// will be instantiated (and the constructor run) when the module is loaded.
+// As a result, the hook indexes will be defined before any method in this
+// module is called.
+AllocEngineHooks Hooks;
+
+}; // anonymous namespace
 
 namespace isc {
 namespace dhcp {
@@ -161,6 +187,10 @@ AllocEngine::AllocEngine(AllocType engine_type, unsigned int attempts)
     default:
         isc_throw(BadValue, "Invalid/unsupported allocation algorithm");
     }
+
+    // Register hook points
+    hook_index_lease4_select_ = Hooks.hook_index_lease4_select_;
+    hook_index_lease6_select_ = Hooks.hook_index_lease6_select_;
 }
 
 Lease6Ptr
@@ -168,7 +198,8 @@ AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
                               const DuidPtr& duid,
                               uint32_t iaid,
                               const IOAddress& hint,
-                              bool fake_allocation /* = false */ ) {
+                              bool fake_allocation,
+                              const isc::hooks::CalloutHandlePtr& callout_handle) {
 
     try {
         // That check is not necessary. We create allocator in AllocEngine
@@ -201,7 +232,8 @@ AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
                 /// implemented
 
                 // the hint is valid and not currently used, let's create a lease for it
-                Lease6Ptr lease = createLease6(subnet, duid, iaid, hint, fake_allocation);
+                Lease6Ptr lease = createLease6(subnet, duid, iaid, hint, callout_handle,
+                                               fake_allocation);
 
                 // It can happen that the lease allocation failed (we could have lost
                 // the race condition. That means that the hint is lo longer usable and
@@ -212,7 +244,7 @@ AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
             } else {
                 if (existing->expired()) {
                     return (reuseExpiredLease(existing, subnet, duid, iaid,
-                                              fake_allocation));
+                                              callout_handle, fake_allocation));
                 }
 
             }
@@ -246,7 +278,7 @@ AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
                 // there's no existing lease for selected candidate, so it is
                 // free. Let's allocate it.
                 Lease6Ptr lease = createLease6(subnet, duid, iaid, candidate,
-                                              fake_allocation);
+                                               callout_handle, fake_allocation);
                 if (lease) {
                     return (lease);
                 }
@@ -257,7 +289,7 @@ AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
             } else {
                 if (existing->expired()) {
                     return (reuseExpiredLease(existing, subnet, duid, iaid,
-                                              fake_allocation));
+                                              callout_handle, fake_allocation));
                 }
             }
 
@@ -283,7 +315,8 @@ AllocEngine::allocateAddress4(const SubnetPtr& subnet,
                               const ClientIdPtr& clientid,
                               const HWAddrPtr& hwaddr,
                               const IOAddress& hint,
-                              bool fake_allocation /* = false */ ) {
+                              bool fake_allocation,
+                              const isc::hooks::CalloutHandlePtr& callout_handle) {
 
     try {
         // Allocator is always created in AllocEngine constructor and there is
@@ -336,7 +369,8 @@ AllocEngine::allocateAddress4(const SubnetPtr& subnet,
                 /// implemented
 
                 // The hint is valid and not currently used, let's create a lease for it
-                Lease4Ptr lease = createLease4(subnet, clientid, hwaddr, hint, fake_allocation);
+                Lease4Ptr lease = createLease4(subnet, clientid, hwaddr, hint,
+                                               callout_handle, fake_allocation);
 
                 // It can happen that the lease allocation failed (we could have lost
                 // the race condition. That means that the hint is lo longer usable and
@@ -347,7 +381,7 @@ AllocEngine::allocateAddress4(const SubnetPtr& subnet,
             } else {
                 if (existing->expired()) {
                     return (reuseExpiredLease(existing, subnet, clientid, hwaddr,
-                                              fake_allocation));
+                                              callout_handle, fake_allocation));
                 }
 
             }
@@ -381,7 +415,7 @@ AllocEngine::allocateAddress4(const SubnetPtr& subnet,
                 // there's no existing lease for selected candidate, so it is
                 // free. Let's allocate it.
                 Lease4Ptr lease = createLease4(subnet, clientid, hwaddr, candidate,
-                                              fake_allocation);
+                                               callout_handle, fake_allocation);
                 if (lease) {
                     return (lease);
                 }
@@ -392,7 +426,7 @@ AllocEngine::allocateAddress4(const SubnetPtr& subnet,
             } else {
                 if (existing->expired()) {
                     return (reuseExpiredLease(existing, subnet, clientid, hwaddr,
-                                              fake_allocation));
+                                              callout_handle, fake_allocation));
                 }
             }
 
@@ -438,6 +472,7 @@ Lease6Ptr AllocEngine::reuseExpiredLease(Lease6Ptr& expired,
                                          const Subnet6Ptr& subnet,
                                          const DuidPtr& duid,
                                          uint32_t iaid,
+                                         const isc::hooks::CalloutHandlePtr& callout_handle,
                                          bool fake_allocation /*= false */ ) {
 
     if (!expired->expired()) {
@@ -461,6 +496,39 @@ Lease6Ptr AllocEngine::reuseExpiredLease(Lease6Ptr& expired,
     /// @todo: log here that the lease was reused (there's ticket #2524 for
     /// logging in libdhcpsrv)
 
+    // Let's execute all callouts registered for lease6_select
+    if (callout_handle &&
+        HooksManager::getHooksManager().calloutsPresent(hook_index_lease6_select_)) {
+
+        // Delete all previous arguments
+        callout_handle->deleteAllArguments();
+
+        // Pass necessary arguments
+        // Subnet from which we do the allocation
+        callout_handle->setArgument("subnet6", subnet);
+
+        // Is this solicit (fake = true) or request (fake = false)
+        callout_handle->setArgument("fake_allocation", fake_allocation);
+
+        // The lease that will be assigned to a client
+        callout_handle->setArgument("lease6", expired);
+
+        // Call the callouts
+        HooksManager::callCallouts(hook_index_lease6_select_, *callout_handle);
+
+        // Callouts decided to skip the action. This means that the lease is not
+        // assigned, so the client will get NoAddrAvail as a result. The lease
+        // won't be inserted into the database.
+        if (callout_handle->getSkip()) {
+            LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_HOOKS, DHCPSRV_HOOK_LEASE6_SELECT_SKIP);
+            return (Lease6Ptr());
+        }
+
+        // Let's use whatever callout returned. Hopefully it is the same lease
+        // we handled to it.
+        callout_handle->getArgument("lease6", expired);
+    }
+
     if (!fake_allocation) {
         // for REQUEST we do update the lease
         LeaseMgrFactory::instance().updateLease6(expired);
@@ -478,6 +546,7 @@ Lease4Ptr AllocEngine::reuseExpiredLease(Lease4Ptr& expired,
                                          const SubnetPtr& subnet,
                                          const ClientIdPtr& clientid,
                                          const HWAddrPtr& hwaddr,
+                                         const isc::hooks::CalloutHandlePtr& callout_handle,
                                          bool fake_allocation /*= false */ ) {
 
     if (!expired->expired()) {
@@ -500,6 +569,44 @@ Lease4Ptr AllocEngine::reuseExpiredLease(Lease4Ptr& expired,
     /// @todo: log here that the lease was reused (there's ticket #2524 for
     /// logging in libdhcpsrv)
 
+    // Let's execute all callouts registered for lease4_select
+    if (callout_handle &&
+        HooksManager::getHooksManager().calloutsPresent(hook_index_lease4_select_)) {
+
+        // Delete all previous arguments
+        callout_handle->deleteAllArguments();
+
+        // Pass necessary arguments
+
+        // Subnet from which we do the allocation. Convert the general subnet
+        // pointer to a pointer to a Subnet4.  Note that because we are using
+        // boost smart pointers here, we need to do the cast using the boost
+        // version of dynamic_pointer_cast.
+        Subnet4Ptr subnet4 = boost::dynamic_pointer_cast<Subnet4>(subnet);
+        callout_handle->setArgument("subnet4", subnet4);
+
+        // Is this solicit (fake = true) or request (fake = false)
+        callout_handle->setArgument("fake_allocation", fake_allocation);
+
+        // The lease that will be assigned to a client
+        callout_handle->setArgument("lease4", expired);
+
+        // Call the callouts
+        HooksManager::callCallouts(hook_index_lease6_select_, *callout_handle);
+
+        // Callouts decided to skip the action. This means that the lease is not
+        // assigned, so the client will get NoAddrAvail as a result. The lease
+        // won't be inserted into the database.
+        if (callout_handle->getSkip()) {
+            LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_HOOKS, DHCPSRV_HOOK_LEASE4_SELECT_SKIP);
+            return (Lease4Ptr());
+        }
+
+        // Let's use whatever callout returned. Hopefully it is the same lease
+        // we handled to it.
+        callout_handle->getArgument("lease4", expired);
+    }
+
     if (!fake_allocation) {
         // for REQUEST we do update the lease
         LeaseMgrFactory::instance().updateLease4(expired);
@@ -517,12 +624,45 @@ Lease6Ptr AllocEngine::createLease6(const Subnet6Ptr& subnet,
                                     const DuidPtr& duid,
                                     uint32_t iaid,
                                     const IOAddress& addr,
+                                    const isc::hooks::CalloutHandlePtr& callout_handle,
                                     bool fake_allocation /*= false */ ) {
 
     Lease6Ptr lease(new Lease6(Lease6::LEASE_IA_NA, addr, duid, iaid,
                                subnet->getPreferred(), subnet->getValid(),
                                subnet->getT1(), subnet->getT2(), subnet->getID()));
 
+    // Let's execute all callouts registered for lease6_select
+    if (callout_handle &&
+        HooksManager::getHooksManager().calloutsPresent(hook_index_lease6_select_)) {
+
+        // Delete all previous arguments
+        callout_handle->deleteAllArguments();
+
+        // Pass necessary arguments
+
+        // Subnet from which we do the allocation
+        callout_handle->setArgument("subnet6", subnet);
+
+        // Is this solicit (fake = true) or request (fake = false)
+        callout_handle->setArgument("fake_allocation", fake_allocation);
+        callout_handle->setArgument("lease6", lease);
+
+        // This is the first callout, so no need to clear any arguments
+        HooksManager::callCallouts(hook_index_lease6_select_, *callout_handle);
+
+        // Callouts decided to skip the action. This means that the lease is not
+        // assigned, so the client will get NoAddrAvail as a result. The lease
+        // won't be inserted into the database.
+        if (callout_handle->getSkip()) {
+            LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_HOOKS, DHCPSRV_HOOK_LEASE6_SELECT_SKIP);
+            return (Lease6Ptr());
+        }
+
+        // Let's use whatever callout returned. Hopefully it is the same lease
+        // we handled to it.
+        callout_handle->getArgument("lease6", lease);
+    }
+
     if (!fake_allocation) {
         // That is a real (REQUEST) allocation
         bool status = LeaseMgrFactory::instance().addLease(lease);
@@ -555,6 +695,7 @@ Lease4Ptr AllocEngine::createLease4(const SubnetPtr& subnet,
                                     const DuidPtr& clientid,
                                     const HWAddrPtr& hwaddr,
                                     const IOAddress& addr,
+                                    const isc::hooks::CalloutHandlePtr& callout_handle,
                                     bool fake_allocation /*= false */ ) {
     if (!hwaddr) {
         isc_throw(BadValue, "Can't create a lease with NULL HW address");
@@ -572,6 +713,46 @@ Lease4Ptr AllocEngine::createLease4(const SubnetPtr& subnet,
                                subnet->getT1(), subnet->getT2(), now,
                                subnet->getID()));
 
+    // Let's execute all callouts registered for lease4_select
+    if (callout_handle &&
+        HooksManager::getHooksManager().calloutsPresent(hook_index_lease4_select_)) {
+
+        // Delete all previous arguments
+        callout_handle->deleteAllArguments();
+
+        // Pass necessary arguments
+
+        // Subnet from which we do the allocation (That's as far as we can go
+        // with using SubnetPtr to point to Subnet4 object. Users should not
+        // be confused with dynamic_pointer_casts. They should get a concrete
+        // pointer (Subnet4Ptr) pointing to a Subnet4 object.
+        Subnet4Ptr subnet4 = boost::dynamic_pointer_cast<Subnet4>(subnet);
+        callout_handle->setArgument("subnet4", subnet4);
+
+        // Is this solicit (fake = true) or request (fake = false)
+        callout_handle->setArgument("fake_allocation", fake_allocation);
+
+        // Pass the intended lease as well
+        callout_handle->setArgument("lease4", lease);
+
+        // This is the first callout, so no need to clear any arguments
+        HooksManager::callCallouts(hook_index_lease4_select_, *callout_handle);
+
+        // Callouts decided to skip the action. This means that the lease is not
+        // assigned, so the client will get NoAddrAvail as a result. The lease
+        // won't be inserted into the database.
+        if (callout_handle->getSkip()) {
+            LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_HOOKS, DHCPSRV_HOOK_LEASE4_SELECT_SKIP);
+            return (Lease4Ptr());
+        }
+
+        // Let's use whatever callout returned. Hopefully it is the same lease
+        // we handled to it.
+        callout_handle->getArgument("lease4", lease);
+    }
+
+
+
     if (!fake_allocation) {
         // That is a real (REQUEST) allocation
         bool status = LeaseMgrFactory::instance().addLease(lease);

+ 28 - 2
src/lib/dhcpsrv/alloc_engine.h

@@ -20,6 +20,7 @@
 #include <dhcp/hwaddr.h>
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/lease_mgr.h>
+#include <hooks/callout_handle.h>
 
 #include <boost/shared_ptr.hpp>
 #include <boost/noncopyable.hpp>
@@ -193,13 +194,16 @@ protected:
     /// @param hint a hint that the client provided
     /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
     ///        an address for DISCOVER that is not really allocated (true)
+    /// @param callout_handle a callout handle (used in hooks). A lease callouts
+    ///        will be executed if this parameter is passed.
     /// @return Allocated IPv4 lease (or NULL if allocation failed)
     Lease4Ptr
     allocateAddress4(const SubnetPtr& subnet,
                      const ClientIdPtr& clientid,
                      const HWAddrPtr& hwaddr,
                      const isc::asiolink::IOAddress& hint,
-                     bool fake_allocation);
+                     bool fake_allocation,
+                     const isc::hooks::CalloutHandlePtr& callout_handle);
 
     /// @brief Renews a IPv4 lease
     ///
@@ -235,13 +239,17 @@ protected:
     /// @param hint a hint that the client provided
     /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
     ///        an address for SOLICIT that is not really allocated (true)
+    /// @param callout_handle a callout handle (used in hooks). A lease callouts
+    ///        will be executed if this parameter is passed.
+    ///
     /// @return Allocated IPv6 lease (or NULL if allocation failed)
     Lease6Ptr
     allocateAddress6(const Subnet6Ptr& subnet,
                      const DuidPtr& duid,
                      uint32_t iaid,
                      const isc::asiolink::IOAddress& hint,
-                     bool fake_allocation);
+                     bool fake_allocation,
+                     const isc::hooks::CalloutHandlePtr& callout_handle);
 
     /// @brief Destructor. Used during DHCPv6 service shutdown.
     virtual ~AllocEngine();
@@ -257,6 +265,9 @@ private:
     /// @param clientid client identifier
     /// @param hwaddr client's hardware address
     /// @param addr an address that was selected and is confirmed to be available
+    /// @param callout_handle a callout handle (used in hooks). A lease callouts
+    ///        will be executed if this parameter is passed (and there are callouts
+    ///        registered)
     /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
     ///        an address for DISCOVER that is not really allocated (true)
     /// @return allocated lease (or NULL in the unlikely case of the lease just
@@ -264,6 +275,7 @@ private:
     Lease4Ptr createLease4(const SubnetPtr& subnet, const DuidPtr& clientid,
                            const HWAddrPtr& hwaddr,
                            const isc::asiolink::IOAddress& addr,
+                           const isc::hooks::CalloutHandlePtr& callout_handle,
                            bool fake_allocation = false);
 
     /// @brief creates a lease and inserts it in LeaseMgr if necessary
@@ -276,12 +288,16 @@ private:
     /// @param duid client's DUID
     /// @param iaid IAID from the IA_NA container the client sent to us
     /// @param addr an address that was selected and is confirmed to be available
+    /// @param callout_handle a callout handle (used in hooks). A lease callouts
+    ///        will be executed if this parameter is passed (and there are callouts
+    ///        registered)
     /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
     ///        an address for SOLICIT that is not really allocated (true)
     /// @return allocated lease (or NULL in the unlikely case of the lease just
     ///        becomed unavailable)
     Lease6Ptr createLease6(const Subnet6Ptr& subnet, const DuidPtr& duid,
                            uint32_t iaid, const isc::asiolink::IOAddress& addr,
+                           const isc::hooks::CalloutHandlePtr& callout_handle,
                            bool fake_allocation = false);
 
     /// @brief Reuses expired IPv4 lease
@@ -294,6 +310,8 @@ private:
     /// @param subnet subnet the lease is allocated from
     /// @param clientid client identifier
     /// @param hwaddr client's hardware address
+    /// @param callout_handle a callout handle (used in hooks). A lease callouts
+    ///        will be executed if this parameter is passed.
     /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
     ///        an address for DISCOVER that is not really allocated (true)
     /// @return refreshed lease
@@ -301,6 +319,7 @@ private:
     Lease4Ptr reuseExpiredLease(Lease4Ptr& expired, const SubnetPtr& subnet,
                                 const ClientIdPtr& clientid,
                                 const HWAddrPtr& hwaddr,
+                                const isc::hooks::CalloutHandlePtr& callout_handle,
                                 bool fake_allocation = false);
 
     /// @brief Reuses expired IPv6 lease
@@ -313,12 +332,15 @@ private:
     /// @param subnet subnet the lease is allocated from
     /// @param duid client's DUID
     /// @param iaid IAID from the IA_NA container the client sent to us
+    /// @param callout_handle a callout handle (used in hooks). A lease callouts
+    ///        will be executed if this parameter is passed.
     /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
     ///        an address for SOLICIT that is not really allocated (true)
     /// @return refreshed lease
     /// @throw BadValue if trying to recycle lease that is still valid
     Lease6Ptr reuseExpiredLease(Lease6Ptr& expired, const Subnet6Ptr& subnet,
                                 const DuidPtr& duid, uint32_t iaid,
+                                const isc::hooks::CalloutHandlePtr& callout_handle,
                                 bool fake_allocation = false);
 
     /// @brief a pointer to currently used allocator
@@ -326,6 +348,10 @@ private:
 
     /// @brief number of attempts before we give up lease allocation (0=unlimited)
     unsigned int attempts_;
+
+    // hook name indexes (used in hooks callouts)
+    int hook_index_lease4_select_; ///< index for lease4_select hook
+    int hook_index_lease6_select_; ///< index for lease6_select hook
 };
 
 }; // namespace isc::dhcp

+ 54 - 1
src/lib/dhcpsrv/cfgmgr.cc

@@ -262,13 +262,66 @@ void CfgMgr::deleteSubnets6() {
     subnets6_.clear();
 }
 
+
 std::string CfgMgr::getDataDir() {
     return (datadir_);
 }
 
+void
+CfgMgr::addActiveIface(const std::string& iface) {
+    if (isIfaceListedActive(iface)) {
+        isc_throw(DuplicateListeningIface,
+                  "attempt to add duplicate interface '" << iface << "'"
+                  " to the set of interfaces on which server listens");
+    }
+    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE, DHCPSRV_CFGMGR_ADD_IFACE)
+        .arg(iface);
+    active_ifaces_.push_back(iface);
+}
+
+void
+CfgMgr::activateAllIfaces() {
+    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+              DHCPSRV_CFGMGR_ALL_IFACES_ACTIVE);
+    all_ifaces_active_ = true;
+}
+
+void
+CfgMgr::deleteActiveIfaces() {
+    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+              DHCPSRV_CFGMGR_CLEAR_ACTIVE_IFACES);
+    active_ifaces_.clear();
+    all_ifaces_active_ = false;
+}
+
+bool
+CfgMgr::isActiveIface(const std::string& iface) const {
+
+    // @todo Verify that the interface with the specified name is
+    // present in the system.
+
+    // If all interfaces are marked active, there is no need to check that
+    // the name of this interface has been explicitly listed.
+    if (all_ifaces_active_) {
+        return (true);
+    }
+    return (isIfaceListedActive(iface));
+}
+
+bool
+CfgMgr::isIfaceListedActive(const std::string& iface) const {
+    for (ActiveIfacesCollection::const_iterator it = active_ifaces_.begin();
+         it != active_ifaces_.end(); ++it) {
+        if (iface == *it) {
+            return (true);
+        }
+    }
+    return (false);
+}
 
 CfgMgr::CfgMgr()
-    :datadir_(DHCP_DATA_DIR) {
+    : datadir_(DHCP_DATA_DIR),
+      all_ifaces_active_(false) {
     // DHCP_DATA_DIR must be set set with -DDHCP_DATA_DIR="..." in Makefile.am
     // Note: the definition of DHCP_DATA_DIR needs to include quotation marks
     // See AM_CPPFLAGS definition in Makefile.am

+ 92 - 0
src/lib/dhcpsrv/cfgmgr.h

@@ -30,10 +30,21 @@
 #include <map>
 #include <string>
 #include <vector>
+#include <list>
 
 namespace isc {
 namespace dhcp {
 
+/// @brief Exception thrown when the same interface has been specified twice.
+///
+/// In particular, this exception is thrown when adding interface to the set
+/// of interfaces on which server is supposed to listen.
+class DuplicateListeningIface : public Exception {
+public:
+    DuplicateListeningIface(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
 
 /// @brief Configuration Manager
 ///
@@ -201,6 +212,26 @@ public:
     /// completely new?
     void deleteSubnets6();
 
+    /// @brief returns const reference to all subnets6
+    ///
+    /// This is used in a hook (subnet4_select), where the hook is able
+    /// to choose a different subnet. Server code has to offer a list
+    /// of possible choices (i.e. all subnets).
+    /// @return a pointer to const Subnet6 collection
+    const Subnet4Collection* getSubnets4() {
+        return (&subnets4_);
+    }
+
+    /// @brief returns const reference to all subnets6
+    ///
+    /// This is used in a hook (subnet6_select), where the hook is able
+    /// to choose a different subnet. Server code has to offer a list
+    /// of possible choices (i.e. all subnets).
+    /// @return a pointer to const Subnet6 collection
+    const Subnet6Collection* getSubnets6() {
+        return (&subnets6_);
+    }
+
     /// @brief get IPv4 subnet by address
     ///
     /// Finds a matching subnet, based on an address. This can be used
@@ -237,6 +268,43 @@ public:
     /// @return data directory
     std::string getDataDir();
 
+    /// @brief Adds the name of the interface to the set of interfaces on which
+    /// server should listen.
+    ///
+    /// @param iface A name of the interface being added to the listening set.
+    void addActiveIface(const std::string& iface);
+
+    /// @brief Sets the flag which indicates that server is supposed to listen
+    /// on all available interfaces.
+    ///
+    /// This function does not close or open sockets. It simply marks that
+    /// server should start to listen on all interfaces the next time sockets
+    /// are reopened. Server should examine this flag when it gets reconfigured
+    /// and configuration changes the interfaces that server should listen on.
+    void activateAllIfaces();
+
+    /// @brief Clear the collection of the interfaces that server should listen
+    /// on.
+    ///
+    /// Apart from clearing the list of interfaces specified with
+    /// @c CfgMgr::addListeningInterface, it also disables listening on all
+    /// interfaces if it has been enabled using
+    /// @c CfgMgr::activateAllInterfaces.
+    /// Likewise @c CfgMgr::activateAllIfaces, this function does not close or
+    /// open sockets. It marks all interfaces inactive for DHCP traffic.
+    /// Server should examine this new setting when it attempts to
+    /// reopen sockets (as a result of reconfiguration).
+    void deleteActiveIfaces();
+
+    /// @brief Check if specified interface should be used to listen to DHCP
+    /// traffic.
+    ///
+    /// @param iface A name of the interface to be checked.
+    ///
+    /// @return true if the specified interface belongs to the set of the
+    /// interfaces on which server is configured to listen.
+    bool isActiveIface(const std::string& iface) const;
+
 protected:
 
     /// @brief Protected constructor.
@@ -268,6 +336,20 @@ protected:
 
 private:
 
+    /// @brief Checks if the specified interface is listed as active.
+    ///
+    /// This function searches for the specified interface name on the list of
+    /// active interfaces: @c CfgMgr::active_ifaces_. It does not take into
+    /// account @c CfgMgr::all_ifaces_active_ flag. If this flag is set to true
+    /// but the specified interface does not belong to
+    /// @c CfgMgr::active_ifaces_, it will return false.
+    ///
+    /// @param iface interface name.
+    ///
+    /// @return true if specified interface belongs to
+    /// @c CfgMgr::active_ifaces_.
+    bool isIfaceListedActive(const std::string& iface) const;
+
     /// @brief A collection of option definitions.
     ///
     /// A collection of option definitions that can be accessed
@@ -283,6 +365,16 @@ private:
 
     /// @brief directory where data files (e.g. server-id) are stored
     std::string datadir_;
+
+    /// @name A collection of interface names on which server listens.
+    //@{
+    typedef std::list<std::string> ActiveIfacesCollection;
+    std::list<std::string> active_ifaces_;
+    //@}
+
+    /// A flag which indicates that server should listen on all available
+    /// interfaces.
+    bool all_ifaces_active_;
 };
 
 } // namespace isc::dhcp

+ 130 - 76
src/lib/dhcpsrv/dhcp_parsers.cc

@@ -36,6 +36,10 @@ using namespace isc::hooks;
 namespace isc {
 namespace dhcp {
 
+namespace {
+const char* ALL_IFACES_KEYWORD = "*";
+}
+
 // *********************** ParserContext  *************************
 
 ParserContext::ParserContext(Option::Universe universe):
@@ -56,17 +60,17 @@ ParserContext::ParserContext(const ParserContext& rhs):
         universe_(rhs.universe_) {
     }
 
-ParserContext& 
+ParserContext&
 ParserContext::operator=(const ParserContext& rhs) {
         if (this != &rhs) {
-            boolean_values_ = 
+            boolean_values_ =
                 BooleanStoragePtr(new BooleanStorage(*(rhs.boolean_values_)));
-            uint32_values_ = 
+            uint32_values_ =
                 Uint32StoragePtr(new Uint32Storage(*(rhs.uint32_values_)));
-            string_values_ = 
+            string_values_ =
                 StringStoragePtr(new StringStorage(*(rhs.string_values_)));
             options_ = OptionStoragePtr(new OptionStorage(*(rhs.options_)));
-            option_defs_ = 
+            option_defs_ =
                 OptionDefStoragePtr(new OptionDefStorage(*(rhs.option_defs_)));
             universe_ = rhs.universe_;
         }
@@ -80,14 +84,14 @@ DebugParser::DebugParser(const std::string& param_name)
     :param_name_(param_name) {
 }
 
-void 
+void
 DebugParser::build(ConstElementPtr new_config) {
     value_ = new_config;
     std::cout << "Build for token: [" << param_name_ << "] = ["
-        << value_->str() << "]" << std::endl; 
+        << value_->str() << "]" << std::endl;
 }
 
-void 
+void
 DebugParser::commit() {
     // Debug message. The whole DebugParser class is used only for parser
     // debugging, and is not used in production code. It is very convenient
@@ -105,7 +109,7 @@ template<> void ValueParser<bool>::build(isc::data::ConstElementPtr value) {
     try {
         value_ = value->boolValue();
     } catch (const isc::data::TypeError &) {
-        isc_throw(BadValue, " Wrong value type for " << param_name_ 
+        isc_throw(BadValue, " Wrong value type for " << param_name_
                   << " : build called with a non-boolean element.");
     }
 }
@@ -143,25 +147,75 @@ template <> void ValueParser<std::string>::build(ConstElementPtr value) {
 
 // ******************** InterfaceListConfigParser *************************
 
-InterfaceListConfigParser::InterfaceListConfigParser(const std::string& 
-                                                     param_name) {
-    if (param_name != "interface") {
+InterfaceListConfigParser::
+InterfaceListConfigParser(const std::string& param_name)
+    : activate_all_(false),
+      param_name_(param_name) {
+    if (param_name_ != "interfaces") {
         isc_throw(BadValue, "Internal error. Interface configuration "
             "parser called for the wrong parameter: " << param_name);
     }
 }
 
-void 
+void
 InterfaceListConfigParser::build(ConstElementPtr value) {
+    // First, we iterate over all specified entries and add it to the
+    // local container so as we can do some basic validation, e.g. eliminate
+    // duplicates.
     BOOST_FOREACH(ConstElementPtr iface, value->listValue()) {
-        interfaces_.push_back(iface->str());
+        std::string iface_name = iface->stringValue();
+        if (iface_name != ALL_IFACES_KEYWORD) {
+            // Let's eliminate duplicates. We could possibly allow duplicates,
+            // but if someone specified duplicated interface name it is likely
+            // that he mistyped the configuration. Failing here should draw his
+            // attention.
+            if (isIfaceAdded(iface_name)) {
+                isc_throw(isc::dhcp::DhcpConfigError, "duplicate interface"
+                          << " name '" << iface_name << "' specified in '"
+                          << param_name_ << "' configuration parameter");
+            }
+            // @todo check that this interface exists in the system!
+            // The IfaceMgr exposes mechanisms to check this.
+
+            // Add the interface name if ok.
+            interfaces_.push_back(iface_name);
+
+        } else {
+            activate_all_ = true;
+
+        }
     }
 }
 
-void 
+void
 InterfaceListConfigParser::commit() {
-    /// @todo: Implement per interface listening. Currently always listening
-    /// on all interfaces.
+    CfgMgr& cfg_mgr = CfgMgr::instance();
+    // Remove active interfaces and clear a flag which marks all interfaces
+    // active
+    cfg_mgr.deleteActiveIfaces();
+
+    if (activate_all_) {
+        // Activate all interfaces. There is not need to add their names
+        // explicitly.
+        cfg_mgr.activateAllIfaces();
+
+    } else {
+        // Explicitly add names of the interfaces which server should listen on.
+        BOOST_FOREACH(std::string iface, interfaces_) {
+            cfg_mgr.addActiveIface(iface);
+        }
+    }
+}
+
+bool
+InterfaceListConfigParser::isIfaceAdded(const std::string& iface) const {
+    for (IfaceListStorage::const_iterator it = interfaces_.begin();
+         it != interfaces_.end(); ++it) {
+        if (iface == *it) {
+            return (true);
+        }
+    }
+    return (false);
 }
 
 // ******************** HooksLibrariesParser *************************
@@ -231,9 +285,9 @@ HooksLibrariesParser::getLibraries(std::vector<std::string>& libraries,
 // **************************** OptionDataParser *************************
 OptionDataParser::OptionDataParser(const std::string&, OptionStoragePtr options,
                                   ParserContextPtr global_context)
-    : boolean_values_(new BooleanStorage()), 
-    string_values_(new StringStorage()), uint32_values_(new Uint32Storage()), 
-    options_(options), option_descriptor_(false), 
+    : boolean_values_(new BooleanStorage()),
+    string_values_(new StringStorage()), uint32_values_(new Uint32Storage()),
+    options_(options), option_descriptor_(false),
     global_context_(global_context) {
     if (!options_) {
         isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
@@ -246,22 +300,22 @@ OptionDataParser::OptionDataParser(const std::string&, OptionStoragePtr options,
     }
 }
 
-void 
+void
 OptionDataParser::build(ConstElementPtr option_data_entries) {
     BOOST_FOREACH(ConfigPair param, option_data_entries->mapValue()) {
         ParserPtr parser;
         if (param.first == "name" || param.first == "data" ||
             param.first == "space") {
-            StringParserPtr name_parser(new StringParser(param.first, 
-                                        string_values_)); 
+            StringParserPtr name_parser(new StringParser(param.first,
+                                        string_values_));
             parser = name_parser;
         } else if (param.first == "code") {
-            Uint32ParserPtr code_parser(new Uint32Parser(param.first, 
-                                       uint32_values_)); 
+            Uint32ParserPtr code_parser(new Uint32Parser(param.first,
+                                       uint32_values_));
             parser = code_parser;
         } else if (param.first == "csv-format") {
-            BooleanParserPtr value_parser(new BooleanParser(param.first, 
-                                         boolean_values_)); 
+            BooleanParserPtr value_parser(new BooleanParser(param.first,
+                                         boolean_values_));
             parser = value_parser;
         } else {
             isc_throw(DhcpConfigError,
@@ -283,12 +337,12 @@ OptionDataParser::build(ConstElementPtr option_data_entries) {
     createOption();
 }
 
-void 
+void
 OptionDataParser::commit() {
     if (!option_descriptor_.option) {
-        // Before we can commit the new option should be configured. If it is 
+        // Before we can commit the new option should be configured. If it is
         // not than somebody must have called commit() before build().
-        isc_throw(isc::InvalidOperation, 
+        isc_throw(isc::InvalidOperation,
             "parser logic error: no option has been configured and"
             " thus there is nothing to commit. Has build() been called?");
     }
@@ -312,7 +366,7 @@ OptionDataParser::commit() {
     options_->addItem(option_descriptor_, option_space_);
 }
 
-void 
+void
 OptionDataParser::createOption() {
     // Option code is held in the uint32_t storage but is supposed to
     // be uint16_t value. We need to check that value in the configuration
@@ -329,7 +383,7 @@ OptionDataParser::createOption() {
 
     // Check that the option name has been specified, is non-empty and does not
     // contain spaces
-    std::string option_name = string_values_->getParam("name"); 
+    std::string option_name = string_values_->getParam("name");
     if (option_name.empty()) {
         isc_throw(DhcpConfigError, "name of the option with code '"
                 << option_code << "' is empty");
@@ -338,7 +392,7 @@ OptionDataParser::createOption() {
                 << "', space character is not allowed");
     }
 
-    std::string option_space = string_values_->getParam("space"); 
+    std::string option_space = string_values_->getParam("space");
     if (!OptionSpace::validateName(option_space)) {
         isc_throw(DhcpConfigError, "invalid option space name '"
                 << option_space << "' specified for option '"
@@ -354,7 +408,7 @@ OptionDataParser::createOption() {
         // need to search for its definition among user-configured
         // options. They are expected to be in the global storage
         // already.
-        OptionDefContainerPtr defs = 
+        OptionDefContainerPtr defs =
             global_context_->option_defs_->getItems(option_space);
 
         // The getItems() should never return the NULL pointer. If there are
@@ -408,16 +462,16 @@ OptionDataParser::createOption() {
                       << " does not have a definition.");
         }
 
-        // @todo We have a limited set of option definitions intiialized at 
-        // the moment.  In the future we want to initialize option definitions 
-        // for all options.  Consequently an error will be issued if an option 
+        // @todo We have a limited set of option definitions intiialized at
+        // the moment.  In the future we want to initialize option definitions
+        // for all options.  Consequently an error will be issued if an option
         // definition does not exist for a particular option code. For now it is
         // ok to create generic option if definition does not exist.
-        OptionPtr option(new Option(global_context_->universe_, 
+        OptionPtr option(new Option(global_context_->universe_,
                         static_cast<uint16_t>(option_code), binary));
-        // The created option is stored in option_descriptor_ class member 
-        // until the commit stage when it is inserted into the main storage. 
-        // If an option with the same code exists in main storage already the 
+        // The created option is stored in option_descriptor_ class member
+        // until the commit stage when it is inserted into the main storage.
+        // If an option with the same code exists in main storage already the
         // old option is replaced.
         option_descriptor_.option = option;
         option_descriptor_.persistent = false;
@@ -439,9 +493,9 @@ OptionDataParser::createOption() {
         // an instance of our option.
         try {
             OptionPtr option = csv_format ?
-                def->optionFactory(global_context_->universe_, 
+                def->optionFactory(global_context_->universe_,
                                   option_code, data_tokens) :
-                def->optionFactory(global_context_->universe_, 
+                def->optionFactory(global_context_->universe_,
                                   option_code, binary);
             Subnet::OptionDescriptor desc(option, false);
             option_descriptor_.option = option;
@@ -459,10 +513,10 @@ OptionDataParser::createOption() {
 }
 
 // **************************** OptionDataListParser *************************
-OptionDataListParser::OptionDataListParser(const std::string&, 
+OptionDataListParser::OptionDataListParser(const std::string&,
     OptionStoragePtr options, ParserContextPtr global_context,
     OptionDataParserFactory* optionDataParserFactory)
-    : options_(options), local_options_(new OptionStorage()), 
+    : options_(options), local_options_(new OptionStorage()),
     global_context_(global_context),
     optionDataParserFactory_(optionDataParserFactory) {
     if (!options_) {
@@ -481,11 +535,11 @@ OptionDataListParser::OptionDataListParser(const std::string&,
     }
 }
 
-void 
+void
 OptionDataListParser::build(ConstElementPtr option_data_list) {
     BOOST_FOREACH(ConstElementPtr option_value, option_data_list->listValue()) {
-        boost::shared_ptr<OptionDataParser> 
-            parser((*optionDataParserFactory_)("option-data", 
+        boost::shared_ptr<OptionDataParser>
+            parser((*optionDataParserFactory_)("option-data",
                     local_options_, global_context_));
 
         // options_ member will hold instances of all options thus
@@ -497,7 +551,7 @@ OptionDataListParser::build(ConstElementPtr option_data_list) {
     }
 }
 
-void 
+void
 OptionDataListParser::commit() {
     BOOST_FOREACH(ParserPtr parser, parsers_) {
         parser->commit();
@@ -510,7 +564,7 @@ OptionDataListParser::commit() {
 }
 
 // ******************************** OptionDefParser ****************************
-OptionDefParser::OptionDefParser(const std::string&, 
+OptionDefParser::OptionDefParser(const std::string&,
                                 OptionDefStoragePtr storage)
     : storage_(storage), boolean_values_(new BooleanStorage()),
     string_values_(new StringStorage()), uint32_values_(new Uint32Storage()) {
@@ -520,23 +574,23 @@ OptionDefParser::OptionDefParser(const std::string&,
     }
 }
 
-void 
+void
 OptionDefParser::build(ConstElementPtr option_def) {
     // Parse the elements that make up the option definition.
      BOOST_FOREACH(ConfigPair param, option_def->mapValue()) {
         std::string entry(param.first);
         ParserPtr parser;
-        if (entry == "name" || entry == "type" || entry == "record-types" 
+        if (entry == "name" || entry == "type" || entry == "record-types"
             || entry == "space" || entry == "encapsulate") {
-            StringParserPtr str_parser(new StringParser(entry, 
+            StringParserPtr str_parser(new StringParser(entry,
                                        string_values_));
             parser = str_parser;
         } else if (entry == "code") {
-            Uint32ParserPtr code_parser(new Uint32Parser(entry, 
+            Uint32ParserPtr code_parser(new Uint32Parser(entry,
                                         uint32_values_));
             parser = code_parser;
         } else if (entry == "array") {
-            BooleanParserPtr array_parser(new BooleanParser(entry, 
+            BooleanParserPtr array_parser(new BooleanParser(entry,
                                          boolean_values_));
             parser = array_parser;
         } else {
@@ -568,7 +622,7 @@ OptionDefParser::build(ConstElementPtr option_def) {
     }
 }
 
-void 
+void
 OptionDefParser::commit() {
     if (storage_ && option_definition_ &&
         OptionSpace::validateName(option_space_name_)) {
@@ -576,7 +630,7 @@ OptionDefParser::commit() {
     }
 }
 
-void 
+void
 OptionDefParser::createOptionDef() {
     // Get the option space name and validate it.
     std::string space = string_values_->getParam("space");
@@ -656,7 +710,7 @@ OptionDefParser::createOptionDef() {
 }
 
 // ******************************** OptionDefListParser ************************
-OptionDefListParser::OptionDefListParser(const std::string&, 
+OptionDefListParser::OptionDefListParser(const std::string&,
     OptionDefStoragePtr storage) :storage_(storage) {
     if (!storage_) {
         isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
@@ -664,7 +718,7 @@ OptionDefListParser::OptionDefListParser(const std::string&,
     }
 }
 
-void 
+void
 OptionDefListParser::build(ConstElementPtr option_def_list) {
     // Clear existing items in the storage.
     // We are going to replace all of them.
@@ -683,7 +737,7 @@ OptionDefListParser::build(ConstElementPtr option_def_list) {
     }
 }
 
-void 
+void
 OptionDefListParser::commit() {
     CfgMgr& cfg_mgr = CfgMgr::instance();
     cfg_mgr.deleteOptionDefs();
@@ -715,7 +769,7 @@ PoolParser::PoolParser(const std::string&,  PoolStoragePtr pools)
     }
 }
 
-void 
+void
 PoolParser::build(ConstElementPtr pools_list) {
     BOOST_FOREACH(ConstElementPtr text_pool, pools_list->listValue()) {
         // That should be a single pool representation. It should contain
@@ -743,7 +797,7 @@ PoolParser::build(ConstElementPtr pools_list) {
                 // will result in interpreting the first digit as output
                 // value and throwing exception if length is written on two
                 // digits (because there are extra characters left over).
-    
+
                 // No checks for values over 128. Range correctness will
                 // be checked in Pool4 constructor.
                 len = boost::lexical_cast<int>(prefix_len);
@@ -775,7 +829,7 @@ PoolParser::build(ConstElementPtr pools_list) {
         }
 }
 
-void 
+void
 PoolParser::commit() {
     if (pools_) {
         // local_pools_ holds the values produced by the build function.
@@ -787,9 +841,9 @@ PoolParser::commit() {
 
 //****************************** SubnetConfigParser *************************
 
-SubnetConfigParser::SubnetConfigParser(const std::string&, 
-                                       ParserContextPtr global_context) 
-    : uint32_values_(new Uint32Storage()), string_values_(new StringStorage()), 
+SubnetConfigParser::SubnetConfigParser(const std::string&,
+                                       ParserContextPtr global_context)
+    : uint32_values_(new Uint32Storage()), string_values_(new StringStorage()),
     pools_(new PoolStorage()), options_(new OptionStorage()),
     global_context_(global_context) {
     // The first parameter should always be "subnet", but we don't check
@@ -800,7 +854,7 @@ SubnetConfigParser::SubnetConfigParser(const std::string&,
     }
 }
 
-void 
+void
 SubnetConfigParser::build(ConstElementPtr subnet) {
     BOOST_FOREACH(ConfigPair param, subnet->mapValue()) {
         ParserPtr parser(createSubnetConfigParser(param.first));
@@ -824,8 +878,8 @@ SubnetConfigParser::build(ConstElementPtr subnet) {
     createSubnet();
 }
 
-void 
-SubnetConfigParser::appendSubOptions(const std::string& option_space, 
+void
+SubnetConfigParser::appendSubOptions(const std::string& option_space,
                                      OptionPtr& option) {
     // Only non-NULL options are stored in option container.
     // If this option pointer is NULL this is a serious error.
@@ -879,7 +933,7 @@ SubnetConfigParser::appendSubOptions(const std::string& option_space,
     }
 }
 
-void 
+void
 SubnetConfigParser::createSubnet() {
     std::string subnet_txt;
     try {
@@ -910,11 +964,11 @@ SubnetConfigParser::createSubnet() {
     isc::asiolink::IOAddress addr(subnet_txt.substr(0, pos));
     uint8_t len = boost::lexical_cast<unsigned int>(subnet_txt.substr(pos + 1));
 
-    // Call the subclass's method to instantiate the subnet 
+    // Call the subclass's method to instantiate the subnet
     initSubnet(addr, len);
 
     // Add pools to it.
-    for (PoolStorage::iterator it = pools_->begin(); it != pools_->end(); 
+    for (PoolStorage::iterator it = pools_->begin(); it != pools_->end();
          ++it) {
         subnet_->addPool(*it);
     }
@@ -989,7 +1043,7 @@ SubnetConfigParser::createSubnet() {
             // values we don't add option from the global storage
             // if there is one already.
             Subnet::OptionDescriptor existing_desc =
-                    subnet_->getOptionDescriptor(option_space, 
+                    subnet_->getOptionDescriptor(option_space,
                                                 desc.option->getType());
             if (!existing_desc.option) {
                 // Add sub-options (if any).
@@ -1000,15 +1054,15 @@ SubnetConfigParser::createSubnet() {
     }
 }
 
-isc::dhcp::Triplet<uint32_t> 
+isc::dhcp::Triplet<uint32_t>
 SubnetConfigParser::getParam(const std::string& name) {
     uint32_t value = 0;
     try {
-        // look for local value 
+        // look for local value
         value = uint32_values_->getParam(name);
     } catch (const DhcpConfigError &) {
         try {
-            // no local, use global value 
+            // no local, use global value
             value = global_context_->uint32_values_->getParam(name);
         } catch (const DhcpConfigError &) {
             isc_throw(DhcpConfigError, "Mandatory parameter " << name

+ 85 - 71
src/lib/dhcpsrv/dhcp_parsers.h

@@ -49,9 +49,9 @@ typedef boost::shared_ptr<OptionStorage> OptionStoragePtr;
 
 /// @brief A template class that stores named elements of a given data type.
 ///
-/// This template class is provides data value storage for configuration
-/// parameters of a given data type.  The values are stored by parameter name
-/// and as instances of type "ValueType". 
+/// This template class is provides data value storage for configuration parameters
+/// of a given data type.  The values are stored by parameter name and as instances
+/// of type "ValueType".
 ///
 /// @param ValueType is the data type of the elements to store.
 template<typename ValueType>
@@ -60,7 +60,7 @@ class ValueStorage {
         /// @brief  Stores the the parameter and its value in the store.
         ///
         /// If the parameter does not exist in the store, then it will be added,
-        /// otherwise its data value will be updated with the given value. 
+        /// otherwise its data value will be updated with the given value.
         ///
         /// @param name is the name of the paramater to store.
         /// @param value is the data value to store.
@@ -74,10 +74,10 @@ class ValueStorage {
         /// @param name is the name of the parameter for which the data
         /// value is desired.
         ///
-        /// @return The paramater's data value of type <ValueType>.
+        /// @return The paramater's data value of type @c ValueType.
         /// @throw DhcpConfigError if the parameter is not found.
         ValueType getParam(const std::string& name) const {
-            typename std::map<std::string, ValueType>::const_iterator param 
+            typename std::map<std::string, ValueType>::const_iterator param
                 = values_.find(name);
 
             if (param == values_.end()) {
@@ -90,8 +90,8 @@ class ValueStorage {
 
         /// @brief  Remove the parameter from the store.
         ///
-        /// Deletes the entry for the given parameter from the store if it 
-        /// exists. 
+        /// Deletes the entry for the given parameter from the store if it
+        /// exists.
         ///
         /// @param name is the name of the paramater to delete.
         void delParam(const std::string& name) {
@@ -111,7 +111,7 @@ class ValueStorage {
 };
 
 
-/// @brief a collection of elements that store uint32 values 
+/// @brief a collection of elements that store uint32 values
 typedef ValueStorage<uint32_t> Uint32Storage;
 typedef boost::shared_ptr<Uint32Storage> Uint32StoragePtr;
 
@@ -131,9 +131,9 @@ typedef boost::shared_ptr<BooleanStorage> BooleanStoragePtr;
 class ParserContext {
 public:
     /// @brief Constructor
-    /// 
+    ///
     /// @param universe is the Option::Universe value of this
-    /// context. 
+    /// context.
     ParserContext(Option::Universe universe);
 
     /// @brief Copy constructor
@@ -178,12 +178,12 @@ public:
 /// @brief Pointer to various parser context.
 typedef boost::shared_ptr<ParserContext> ParserContextPtr;
 
-/// @brief Simple data-type parser template class 
+/// @brief Simple data-type parser template class
 ///
 /// This is the template class for simple data-type parsers. It supports
-/// parsing a configuration parameter with specific data-type for its 
-/// possible values. It provides a common constructor, commit, and templated 
-/// data storage.  The "build" method implementation must be provided by a 
+/// parsing a configuration parameter with specific data-type for its
+/// possible values. It provides a common constructor, commit, and templated
+/// data storage.  The "build" method implementation must be provided by a
 /// declaring type.
 /// @param ValueType is the data type of the configuration paramater value
 /// the parser should handle.
@@ -199,7 +199,7 @@ public:
     /// @throw isc::dhcp::DhcpConfigError if a provided parameter's
     /// name is empty.
     /// @throw isc::dhcp::DhcpConfigError if storage is null.
-    ValueParser(const std::string& param_name, 
+    ValueParser(const std::string& param_name,
         boost::shared_ptr<ValueStorage<ValueType> > storage)
         : storage_(storage), param_name_(param_name), value_() {
         // Empty parameter name is invalid.
@@ -216,12 +216,12 @@ public:
     }
 
 
-    /// @brief Parse a given element into a value of type <ValueType>
+    /// @brief Parse a given element into a value of type @c ValueType
     ///
     /// @param value a value to be parsed.
     ///
     /// @throw isc::BadValue Typically the implementing type will throw
-    /// a BadValue exception when given an invalid Element to parse. 
+    /// a BadValue exception when given an invalid Element to parse.
     void build(isc::data::ConstElementPtr value);
 
     /// @brief Put a parsed value to the storage.
@@ -230,7 +230,7 @@ public:
         // its value. If it doesn't we insert a new element.
         storage_->setParam(param_name_, value_);
     }
-    
+ 
 private:
     /// Pointer to the storage where committed value is stored.
     boost::shared_ptr<ValueStorage<ValueType> > storage_;
@@ -319,8 +319,23 @@ public:
     virtual void commit();
 
 private:
+    /// @brief Check that specified interface exists in
+    /// @c InterfaceListConfigParser::interfaces_.
+    ///
+    /// @param iface A name of the interface.
+    ///
+    /// @return true if specified interface name was found.
+    bool isIfaceAdded(const std::string& iface) const;
+
     /// contains list of network interfaces
-    std::vector<std::string> interfaces_;
+    typedef std::list<std::string> IfaceListStorage;
+    IfaceListStorage interfaces_;
+
+    // Should server listen on all interfaces.
+    bool activate_all_;
+
+    // Parsed parameter name
+    std::string param_name_;
 };
 
 /// @brief parser for hooks library list
@@ -419,11 +434,11 @@ public:
     /// @param dummy first argument is ignored, all Parser constructors
     /// accept string as first argument.
     /// @param options is the option storage in which to store the parsed option
-    /// upon "commit". 
-    /// @param global_context is a pointer to the global context which 
-    /// stores global scope parameters, options, option defintions. 
+    /// upon "commit".
+    /// @param global_context is a pointer to the global context which
+    /// stores global scope parameters, options, option defintions.
     /// @throw isc::dhcp::DhcpConfigError if options or global_context are null.
-    OptionDataParser(const std::string&, OptionStoragePtr options, 
+    OptionDataParser(const std::string& dummy, OptionStoragePtr options,
                     ParserContextPtr global_context);
 
     /// @brief Parses the single option data.
@@ -443,31 +458,31 @@ public:
 
     /// @brief Commits option value.
     ///
-    /// This function adds a new option to the storage or replaces an existing 
+    /// This function adds a new option to the storage or replaces an existing
     /// option with the same code.
     ///
-    /// @throw isc::InvalidOperation if failed to set pointer to storage or 
+    /// @throw isc::InvalidOperation if failed to set pointer to storage or
     /// failed
     /// to call build() prior to commit. If that happens data in the storage
     /// remain un-modified.
     virtual void commit();
 
-    /// @brief virtual destructor to ensure orderly destruction of derivations. 
+    /// @brief virtual destructor to ensure orderly destruction of derivations.
     virtual ~OptionDataParser(){};
 
 protected:
     /// @brief Finds an option definition within the server's option space
-    /// 
-    /// Given an option space and an option code, find the correpsonding 
+    ///
+    /// Given an option space and an option code, find the correpsonding
     /// option defintion within the server's option defintion storage. This
     /// method is pure virtual requiring derivations to manage which option
     /// space(s) is valid for search.
     ///
-    /// @param option_space name of the parameter option space 
-    /// @param option_code numeric value of the parameter to find 
-    /// @return OptionDefintionPtr of the option defintion or an 
+    /// @param option_space name of the parameter option space
+    /// @param option_code numeric value of the parameter to find
+    /// @return OptionDefintionPtr of the option defintion or an
     /// empty OptionDefinitionPtr if not found.
-    /// @throw DhcpConfigError if the option space requested is not valid 
+    /// @throw DhcpConfigError if the option space requested is not valid
     /// for this server.
     virtual OptionDefinitionPtr findServerSpaceOptionDefinition (
             std::string& option_space, uint32_t option_code) = 0;
@@ -507,13 +522,13 @@ private:
     /// Option space name where the option belongs to.
     std::string option_space_;
 
-    /// Parsing context which contains global values, options and option 
+    /// Parsing context which contains global values, options and option
     /// definitions.
     ParserContextPtr global_context_;
 };
 
 ///@brief Function pointer for OptionDataParser factory methods
-typedef OptionDataParser *OptionDataParserFactory(const std::string&, 
+typedef OptionDataParser *OptionDataParserFactory(const std::string&,
                      OptionStoragePtr options, ParserContextPtr global_context);
 
 /// @brief Parser for option data values within a subnet.
@@ -526,15 +541,15 @@ class OptionDataListParser : public DhcpConfigParser {
 public:
     /// @brief Constructor.
     ///
-    /// @param string& nominally would be param name, this is always ignored.
+    /// @param dummy nominally would be param name, this is always ignored.
     /// @param options parsed option storage for options in this list
-    /// @param global_context is a pointer to the global context which 
-    /// stores global scope parameters, options, option defintions. 
-    /// @param optionDataParserFactory factory method for creating individual 
-    /// option parsers 
+    /// @param global_context is a pointer to the global context which
+    /// stores global scope parameters, options, option defintions.
+    /// @param optionDataParserFactory factory method for creating individual
+    /// option parsers
     /// @throw isc::dhcp::DhcpConfigError if options or global_context are null.
-    OptionDataListParser(const std::string&, OptionStoragePtr options, 
-                        ParserContextPtr global_context, 
+    OptionDataListParser(const std::string& dummy, OptionStoragePtr options,
+                        ParserContextPtr global_context,
                         OptionDataParserFactory *optionDataParserFactory);
 
     /// @brief Parses entries that define options' data for a subnet.
@@ -564,7 +579,7 @@ private:
     /// Collection of parsers;
     ParserCollection parsers_;
 
-    /// Parsing context which contains global values, options and option 
+    /// Parsing context which contains global values, options and option
     /// definitions.
     ParserContextPtr global_context_;
 
@@ -582,10 +597,10 @@ public:
     ///
     /// @param dummy first argument is ignored, all Parser constructors
     /// accept string as first argument.
-    /// @param storage is the definition storage in which to store the parsed 
-    /// definition upon "commit". 
+    /// @param storage is the definition storage in which to store the parsed
+    /// definition upon "commit".
     /// @throw isc::dhcp::DhcpConfigError if storage is null.
-    OptionDefParser(const std::string&, OptionDefStoragePtr storage);
+    OptionDefParser(const std::string& dummy, OptionDefStoragePtr storage);
 
     /// @brief Parses an entry that describes single option definition.
     ///
@@ -633,10 +648,10 @@ public:
     ///
     /// @param dummy first argument is ignored, all Parser constructors
     /// accept string as first argument.
-    /// @param storage is the definition storage in which to store the parsed 
-    /// definitions in this list 
+    /// @param storage is the definition storage in which to store the parsed
+    /// definitions in this list
     /// @throw isc::dhcp::DhcpConfigError if storage is null.
-    OptionDefListParser(const std::string&, OptionDefStoragePtr storage);
+    OptionDefListParser(const std::string& dummy, OptionDefStoragePtr storage);
 
     /// @brief Parse configuration entries.
     ///
@@ -653,7 +668,7 @@ public:
 
 private:
     /// @brief storage for option definitions.
-    OptionDefStoragePtr storage_; 
+    OptionDefStoragePtr storage_;
 };
 
 /// @brief a collection of pools
@@ -674,14 +689,13 @@ class PoolParser : public DhcpConfigParser {
 public:
 
     /// @brief constructor.
-   
-
+    ///
     /// @param dummy first argument is ignored, all Parser constructors
     /// accept string as first argument.
-    /// @param pools is the storage in which to store the parsed pool 
-    /// upon "commit". 
+    /// @param pools is the storage in which to store the parsed pool
+    /// upon "commit".
     /// @throw isc::dhcp::DhcpConfigError if storage is null.
-    PoolParser(const std::string&,  PoolStoragePtr pools);
+    PoolParser(const std::string& dummy, PoolStoragePtr pools);
 
     /// @brief parses the actual list
     ///
@@ -701,9 +715,9 @@ protected:
     ///
     /// @param addr is the IP  prefix of the pool.
     /// @param len is the prefix length.
-    /// @param ignored dummy parameter to provide symmetry between 
-    /// @return returns a PoolPtr to the new Pool object. 
-    virtual PoolPtr poolMaker(isc::asiolink::IOAddress &addr, uint32_t len, 
+    /// @param ptype is the type of pool to create.
+    /// @return returns a PoolPtr to the new Pool object.
+    virtual PoolPtr poolMaker(isc::asiolink::IOAddress &addr, uint32_t len,
                            int32_t ptype=0) = 0;
 
     /// @brief Creates a Pool object given starting and ending IP addresses.
@@ -712,7 +726,7 @@ protected:
     /// @param max is the last IP address in the pool.
     /// @param ptype is the type of pool to create (not used by all derivations)
     /// @return returns a PoolPtr to the new Pool object.
-    virtual PoolPtr poolMaker(isc::asiolink::IOAddress &min, 
+    virtual PoolPtr poolMaker(isc::asiolink::IOAddress &min,
                            isc::asiolink::IOAddress &max, int32_t ptype=0) = 0;
 
     /// @brief pointer to the actual Pools storage
@@ -741,7 +755,7 @@ public:
     /// @param subnet pointer to the content of subnet definition
     ///
     /// @throw isc::DhcpConfigError if subnet configuration parsing failed.
-    virtual void build(isc::data::ConstElementPtr subnet); 
+    virtual void build(isc::data::ConstElementPtr subnet);
 
     /// @brief Adds the created subnet to a server's configuration.
     virtual void commit() = 0;
@@ -758,7 +772,7 @@ protected:
                                             const std::string& config_id) = 0;
 
     /// @brief Determines if the given option space name and code describe
-    /// a standard option for the  server. 
+    /// a standard option for the  server.
     ///
     /// @param option_space is the name of the option space to consider
     /// @param code is the numeric option code to consider
@@ -774,20 +788,20 @@ protected:
                                                              uint32_t code) = 0;
 
     /// @brief Issues a server specific warning regarding duplicate subnet
-    /// options. 
-    /// 
+    /// options.
+    ///
     /// @param code is the numeric option code of the duplicate option
-    /// @param addr is the subnet address 
+    /// @param addr is the subnet address
     /// @todo a means to know the correct logger and perhaps a common
     /// message would allow this method to be emitted by the base class.
-    virtual void duplicate_option_warning(uint32_t code, 
+    virtual void duplicate_option_warning(uint32_t code,
         isc::asiolink::IOAddress& addr) = 0;
 
-    /// @brief Instantiates the subnet based on a given IP prefix and prefix 
-    /// length.  
-    /// 
+    /// @brief Instantiates the subnet based on a given IP prefix and prefix
+    /// length.
+    ///
     /// @param addr is the IP prefix of the subnet.
-    /// @param len is the prefix length 
+    /// @param len is the prefix length
     virtual void initSubnet(isc::asiolink::IOAddress addr, uint8_t len) = 0;
 
     /// @brief Returns value for a given parameter (after using inheritance)
@@ -811,7 +825,7 @@ private:
 
     /// @brief Create a new subnet using a data from child parsers.
     ///
-    /// @throw isc::dhcp::DhcpConfigError if subnet configuration parsing 
+    /// @throw isc::dhcp::DhcpConfigError if subnet configuration parsing
     /// failed.
     void createSubnet();
 
@@ -835,7 +849,7 @@ protected:
     /// Pointer to the created subnet object.
     isc::dhcp::SubnetPtr subnet_;
 
-    /// Parsing context which contains global values, options and option 
+    /// Parsing context which contains global values, options and option
     /// definitions.
     ParserContextPtr global_context_;
 };

+ 3 - 0
src/lib/dhcpsrv/dhcpsrv_log.h

@@ -50,6 +50,9 @@ const int DHCPSRV_DBG_TRACE_DETAIL = DBGLVL_TRACE_DETAIL;
 /// Record detailed (and verbose) data on the server.
 const int DHCPSRV_DBG_TRACE_DETAIL_DATA = DBGLVL_TRACE_DETAIL_DATA;
 
+// Trace hook related operations
+const int DHCPSRV_DBG_HOOKS = DBGLVL_TRACE_BASIC;
+
 ///@}
 
 

+ 26 - 0
src/lib/dhcpsrv/dhcpsrv_messages.mes

@@ -54,6 +54,10 @@ consider reducing the lease lifetime.  In this way, addresses allocated
 to clients that are no longer active on the network will become available
 available sooner.
 
+% DHCPSRV_CFGMGR_ADD_IFACE adding listening interface %1
+A debug message issued when new interface is being added to the collection of
+interfaces on which server listens to DHCP messages.
+
 % DHCPSRV_CFGMGR_ADD_SUBNET4 adding subnet %1
 A debug message reported when the DHCP configuration manager is adding the
 specified IPv4 subnet to its database.
@@ -62,6 +66,16 @@ specified IPv4 subnet to its database.
 A debug message reported when the DHCP configuration manager is adding the
 specified IPv6 subnet to its database.
 
+% DHCPSRV_CFGMGR_ALL_IFACES_ACTIVE enabling listening on all interfaces
+A debug message issued when server is being configured to listen on all
+interfaces.
+
+% DHCPSRV_CFGMGR_CLEAR_ACTIVE_IFACES stop listening on all interfaces
+A debug message issued when configuration manager clears the internal list
+of active interfaces. This doesn't prevent the server from listening to
+the DHCP traffic through open sockets, but will rather be used by Interface
+Manager to select active interfaces when sockets are re-opened.
+
 % DHCPSRV_CFGMGR_DELETE_SUBNET4 deleting all IPv4 subnets
 A debug message noting that the DHCP configuration manager has deleted all IPv4
 subnets in its database.
@@ -121,6 +135,18 @@ the database access parameters are changed: in the latter case, the
 server closes the currently open database, and opens a database using
 the new parameters.
 
+% DHCPSRV_HOOK_LEASE4_SELECT_SKIP Lease4 creation was skipped, because of callout skip flag.
+This debug message is printed when a callout installed on lease4_select
+hook point sets the skip flag. It means that the server was told that
+no lease4 should be assigned. The server will not put that lease in its
+database and the client will get a NAK packet.
+
+% DHCPSRV_HOOK_LEASE6_SELECT_SKIP Lease6 (non-temporary) creation was skipped, because of callout skip flag.
+This debug message is printed when a callout installed on lease6_select
+hook point sets the skip flag. It means that the server was told that
+no lease6 should be assigned. The server will not put that lease in its
+database and the client will get a NoAddrsAvail for that IA_NA option.
+
 % DHCPSRV_INVALID_ACCESS invalid database access string: %1
 This is logged when an attempt has been made to parse a database access string
 and the attempt ended in error.  The access string in question - which

+ 471 - 27
src/lib/dhcpsrv/tests/alloc_engine_unittest.cc

@@ -25,18 +25,24 @@
 
 #include <dhcpsrv/tests/test_utils.h>
 
+#include <hooks/server_hooks.h>
+#include <hooks/callout_manager.h>
+#include <hooks/hooks_manager.h>
+
 #include <boost/shared_ptr.hpp>
 #include <boost/scoped_ptr.hpp>
 #include <gtest/gtest.h>
 
 #include <iostream>
 #include <sstream>
+#include <algorithm>
 #include <set>
 #include <time.h>
 
 using namespace std;
 using namespace isc;
 using namespace isc::asiolink;
+using namespace isc::hooks;
 using namespace isc::dhcp;
 using namespace isc::dhcp::test;
 
@@ -105,7 +111,7 @@ public:
         // @todo: check cltt
      }
 
-    ~AllocEngine6Test() {
+    virtual ~AllocEngine6Test() {
         factory_.destroy();
     }
 
@@ -173,7 +179,7 @@ public:
         // @todo: check cltt
      }
 
-    ~AllocEngine4Test() {
+    virtual ~AllocEngine4Test() {
         factory_.destroy();
     }
 
@@ -203,7 +209,7 @@ TEST_F(AllocEngine6Test, simpleAlloc6) {
     ASSERT_TRUE(engine);
 
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
-                                               false);
+                                               false, CalloutHandlePtr());
 
     // Check that we got a lease
     ASSERT_TRUE(lease);
@@ -226,7 +232,7 @@ TEST_F(AllocEngine6Test, fakeAlloc6) {
     ASSERT_TRUE(engine);
 
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
-                                               true);
+                                               true, CalloutHandlePtr());
 
     // Check that we got a lease
     ASSERT_TRUE(lease);
@@ -248,7 +254,7 @@ TEST_F(AllocEngine6Test, allocWithValidHint6) {
 
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_,
                                                IOAddress("2001:db8:1::15"),
-                                               false);
+                                               false, CalloutHandlePtr());
 
     // Check that we got a lease
     ASSERT_TRUE(lease);
@@ -286,7 +292,7 @@ TEST_F(AllocEngine6Test, allocWithUsedHint6) {
     // twice.
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_,
                                                IOAddress("2001:db8:1::1f"),
-                                               false);
+                                               false, CalloutHandlePtr());
     // Check that we got a lease
     ASSERT_TRUE(lease);
 
@@ -319,7 +325,7 @@ TEST_F(AllocEngine6Test, allocBogusHint6) {
     // with the normal allocation
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_,
                                                IOAddress("3000::abc"),
-                                               false);
+                                               false, CalloutHandlePtr());
     // Check that we got a lease
     ASSERT_TRUE(lease);
 
@@ -345,12 +351,12 @@ TEST_F(AllocEngine6Test, allocateAddress6Nulls) {
 
     // Allocations without subnet are not allowed
     Lease6Ptr lease = engine->allocateAddress6(Subnet6Ptr(), duid_, iaid_,
-                                               IOAddress("::"), false);
+                                               IOAddress("::"), false, CalloutHandlePtr());
     ASSERT_FALSE(lease);
 
     // Allocations without DUID are not allowed either
     lease = engine->allocateAddress6(subnet_, DuidPtr(), iaid_,
-                                     IOAddress("::"), false);
+                                     IOAddress("::"), false, CalloutHandlePtr());
     ASSERT_FALSE(lease);
 }
 
@@ -439,7 +445,7 @@ TEST_F(AllocEngine6Test, smallPool6) {
     cfg_mgr.addSubnet6(subnet_);
 
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
-                                               false);
+                                               false, CalloutHandlePtr());
 
     // Check that we got that single lease
     ASSERT_TRUE(lease);
@@ -485,7 +491,7 @@ TEST_F(AllocEngine6Test, outOfAddresses6) {
     // There is just a single address in the pool and allocated it to someone
     // else, so the allocation should fail
     Lease6Ptr lease2 = engine->allocateAddress6(subnet_, duid_, iaid_,
-                                                IOAddress("::"), false);
+                                                IOAddress("::"), false, CalloutHandlePtr());
     EXPECT_FALSE(lease2);
 }
 
@@ -519,7 +525,7 @@ TEST_F(AllocEngine6Test, solicitReuseExpiredLease6) {
 
     // CASE 1: Asking for any address
     lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
-                                     true);
+                                     true, CalloutHandlePtr());
     // Check that we got that single lease
     ASSERT_TRUE(lease);
     EXPECT_EQ(addr.toText(), lease->addr_.toText());
@@ -529,7 +535,7 @@ TEST_F(AllocEngine6Test, solicitReuseExpiredLease6) {
 
     // CASE 2: Asking specifically for this address
     lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress(addr.toText()),
-                                     true);
+                                     true, CalloutHandlePtr());
     // Check that we got that single lease
     ASSERT_TRUE(lease);
     EXPECT_EQ(addr.toText(), lease->addr_.toText());
@@ -563,7 +569,8 @@ TEST_F(AllocEngine6Test, requestReuseExpiredLease6) {
 
     // A client comes along, asking specifically for this address
     lease = engine->allocateAddress6(subnet_, duid_, iaid_,
-                                     IOAddress(addr.toText()), false);
+                                     IOAddress(addr.toText()), false,
+                                     CalloutHandlePtr());
 
     // Check that he got that single lease
     ASSERT_TRUE(lease);
@@ -586,7 +593,8 @@ TEST_F(AllocEngine4Test, simpleAlloc4) {
     ASSERT_TRUE(engine);
 
     Lease4Ptr lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
-                                               IOAddress("0.0.0.0"), false);
+                                               IOAddress("0.0.0.0"), false,
+                                               CalloutHandlePtr());
 
     // Check that we got a lease
     ASSERT_TRUE(lease);
@@ -609,7 +617,8 @@ TEST_F(AllocEngine4Test, fakeAlloc4) {
     ASSERT_TRUE(engine);
 
     Lease4Ptr lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
-                                               IOAddress("0.0.0.0"), true);
+                                               IOAddress("0.0.0.0"), true,
+                                               CalloutHandlePtr());
 
     // Check that we got a lease
     ASSERT_TRUE(lease);
@@ -632,7 +641,7 @@ TEST_F(AllocEngine4Test, allocWithValidHint4) {
 
     Lease4Ptr lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
                                                IOAddress("192.0.2.105"),
-                                               false);
+                                               false, CalloutHandlePtr());
 
     // Check that we got a lease
     ASSERT_TRUE(lease);
@@ -672,7 +681,7 @@ TEST_F(AllocEngine4Test, allocWithUsedHint4) {
     // twice.
     Lease4Ptr lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
                                                IOAddress("192.0.2.106"),
-                                               false);
+                                               false, CalloutHandlePtr());
     // Check that we got a lease
     ASSERT_TRUE(lease);
 
@@ -706,7 +715,7 @@ TEST_F(AllocEngine4Test, allocBogusHint4) {
     // with the normal allocation
     Lease4Ptr lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
                                                IOAddress("10.1.1.1"),
-                                               false);
+                                               false, CalloutHandlePtr());
     // Check that we got a lease
     ASSERT_TRUE(lease);
 
@@ -733,18 +742,19 @@ TEST_F(AllocEngine4Test, allocateAddress4Nulls) {
 
     // Allocations without subnet are not allowed
     Lease4Ptr lease = engine->allocateAddress4(SubnetPtr(), clientid_, hwaddr_,
-                                               IOAddress("0.0.0.0"), false);
+                                               IOAddress("0.0.0.0"), false,
+                                               CalloutHandlePtr());
     EXPECT_FALSE(lease);
 
     // Allocations without HW address are not allowed
     lease = engine->allocateAddress4(subnet_, clientid_, HWAddrPtr(),
-                                     IOAddress("0.0.0.0"), false);
+                                     IOAddress("0.0.0.0"), false, CalloutHandlePtr());
     EXPECT_FALSE(lease);
 
     // Allocations without client-id are allowed
     clientid_ = ClientIdPtr();
     lease = engine->allocateAddress4(subnet_, ClientIdPtr(), hwaddr_,
-                                     IOAddress("0.0.0.0"), false);
+                                     IOAddress("0.0.0.0"), false, CalloutHandlePtr());
     // Check that we got a lease
     ASSERT_TRUE(lease);
 
@@ -848,7 +858,7 @@ TEST_F(AllocEngine4Test, smallPool4) {
     cfg_mgr.addSubnet4(subnet_);
 
     Lease4Ptr lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
-                                               false);
+                                               false, CalloutHandlePtr());
 
     // Check that we got that single lease
     ASSERT_TRUE(lease);
@@ -896,7 +906,8 @@ TEST_F(AllocEngine4Test, outOfAddresses4) {
     // else, so the allocation should fail
 
     Lease4Ptr lease2 = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
-                                                IOAddress("0.0.0.0"), false);
+                                                IOAddress("0.0.0.0"), false,
+                                                CalloutHandlePtr());
     EXPECT_FALSE(lease2);
 }
 
@@ -929,7 +940,7 @@ TEST_F(AllocEngine4Test, discoverReuseExpiredLease4) {
 
     // CASE 1: Asking for any address
     lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
-                                     true);
+                                     true, CalloutHandlePtr());
     // Check that we got that single lease
     ASSERT_TRUE(lease);
     EXPECT_EQ(addr.toText(), lease->addr_.toText());
@@ -939,7 +950,7 @@ TEST_F(AllocEngine4Test, discoverReuseExpiredLease4) {
 
     // CASE 2: Asking specifically for this address
     lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_, IOAddress(addr.toText()),
-                                     true);
+                                     true, CalloutHandlePtr());
     // Check that we got that single lease
     ASSERT_TRUE(lease);
     EXPECT_EQ(addr.toText(), lease->addr_.toText());
@@ -966,7 +977,8 @@ TEST_F(AllocEngine4Test, requestReuseExpiredLease4) {
 
     // A client comes along, asking specifically for this address
     lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
-                                     IOAddress(addr.toText()), false);
+                                     IOAddress(addr.toText()), false,
+                                     CalloutHandlePtr());
 
     // Check that he got that single lease
     ASSERT_TRUE(lease);
@@ -980,6 +992,8 @@ TEST_F(AllocEngine4Test, requestReuseExpiredLease4) {
     detailCompareLease(lease, from_mgr);
 }
 
+/// @todo write renewLease6
+
 // This test checks if a lease is really renewed when renewLease4 method is
 // called
 TEST_F(AllocEngine4Test, renewLease4) {
@@ -1020,4 +1034,434 @@ TEST_F(AllocEngine4Test, renewLease4) {
     detailCompareLease(lease, from_mgr);
 }
 
+/// @brief helper class used in Hooks testing in AllocEngine6
+///
+/// It features a couple of callout functions and buffers to store
+/// the data that is accessible via callouts.
+class HookAllocEngine6Test : public AllocEngine6Test {
+public:
+    HookAllocEngine6Test() {
+        resetCalloutBuffers();
+    }
+
+    virtual ~HookAllocEngine6Test() {
+        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+            "lease6_select");
+    }
+
+    /// @brief clears out buffers, so callouts can store received arguments
+    void resetCalloutBuffers() {
+        callback_name_ = string("");
+        callback_subnet6_.reset();
+        callback_fake_allocation_ = false;
+        callback_lease6_.reset();
+        callback_argument_names_.clear();
+        callback_addr_original_ = IOAddress("::");
+        callback_addr_updated_ = IOAddress("::");
+    }
+
+    /// callback that stores received callout name and received values
+    static int
+    lease6_select_callout(CalloutHandle& callout_handle) {
+
+        callback_name_ = string("lease6_select");
+
+        callout_handle.getArgument("subnet6", callback_subnet6_);
+        callout_handle.getArgument("fake_allocation", callback_fake_allocation_);
+        callout_handle.getArgument("lease6", callback_lease6_);
+
+        callback_addr_original_ = callback_lease6_->addr_;
+
+        callback_argument_names_ = callout_handle.getArgumentNames();
+        return (0);
+    }
+
+    /// callback that overrides the lease with different values
+    static int
+    lease6_select_different_callout(CalloutHandle& callout_handle) {
+
+        // Let's call the basic callout, so it can record all parameters
+        lease6_select_callout(callout_handle);
+
+        // Now we need to tweak the least a bit
+        Lease6Ptr lease;
+        callout_handle.getArgument("lease6", lease);
+        callback_addr_updated_ = addr_override_;
+        lease->addr_ = callback_addr_updated_;
+        lease->t1_ = t1_override_;
+        lease->t2_ = t2_override_;
+        lease->preferred_lft_ = pref_override_;
+        lease->valid_lft_ = valid_override_;
+
+        return (0);
+    }
+
+    // Values to be used in callout to override lease6 content
+    static const IOAddress addr_override_;
+    static const uint32_t t1_override_;
+    static const uint32_t t2_override_;
+    static const uint32_t pref_override_;
+    static const uint32_t valid_override_;
+
+    // Callback will store original and overridden values here
+    static IOAddress callback_addr_original_;
+    static IOAddress callback_addr_updated_;
+
+    // Buffers (callback will store received values here)
+    static string callback_name_;
+    static Subnet6Ptr callback_subnet6_;
+    static Lease6Ptr callback_lease6_;
+    static bool callback_fake_allocation_;
+    static vector<string> callback_argument_names_;
+};
+
+// For some reason intialization within a class makes the linker confused.
+// linker complains about undefined references if they are defined within
+// the class declaration.
+const IOAddress HookAllocEngine6Test::addr_override_("2001:db8::abcd");
+const uint32_t HookAllocEngine6Test::t1_override_ = 6000;
+const uint32_t HookAllocEngine6Test::t2_override_ = 7000;
+const uint32_t HookAllocEngine6Test::pref_override_ = 8000;
+const uint32_t HookAllocEngine6Test::valid_override_ = 9000;
+
+IOAddress HookAllocEngine6Test::callback_addr_original_("::");
+IOAddress HookAllocEngine6Test::callback_addr_updated_("::");
+
+string HookAllocEngine6Test::callback_name_;
+Subnet6Ptr HookAllocEngine6Test::callback_subnet6_;
+Lease6Ptr HookAllocEngine6Test::callback_lease6_;
+bool HookAllocEngine6Test::callback_fake_allocation_;
+vector<string> HookAllocEngine6Test::callback_argument_names_;
+
+// This test checks if the lease6_select callout is executed and expected
+// parameters as passed.
+TEST_F(HookAllocEngine6Test, lease6_select) {
+
+    // Note: The following order is working as expected:
+    // 1. create AllocEngine (that register hook points)
+    // 2. call loadLibraries()
+    //
+    // This order, however, causes segfault in HooksManager
+    // 1. call loadLibraries()
+    // 2. create AllocEngine (that register hook points)
+
+    // Create allocation engine (hook names are registered in its ctor)
+    boost::scoped_ptr<AllocEngine> engine;
+    ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+    ASSERT_TRUE(engine);
+
+    // Initialize Hooks Manager
+    vector<string> libraries; // no libraries at this time
+    HooksManager::loadLibraries(libraries);
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "lease6_select", lease6_select_callout));
+
+    CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+
+    Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
+                                               false, callout_handle);
+    // Check that we got a lease
+    ASSERT_TRUE(lease);
+
+    // Do all checks on the lease
+    checkLease6(lease);
+
+    // Check that the lease is indeed in LeaseMgr
+    Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->addr_);
+    ASSERT_TRUE(from_mgr);
+
+    // Check that callouts were indeed called
+    EXPECT_EQ("lease6_select", callback_name_);
+
+    // Now check that the lease in LeaseMgr has the same parameters
+    ASSERT_TRUE(callback_lease6_);
+    detailCompareLease(callback_lease6_, from_mgr);
+
+    ASSERT_TRUE(callback_subnet6_);
+    EXPECT_EQ(subnet_->toText(), callback_subnet6_->toText());
+
+    EXPECT_FALSE(callback_fake_allocation_);
+
+    // Check if all expected parameters are reported. It's a bit tricky, because
+    // order may be different. If the test starts failing, because someone tweaked
+    // hooks engine, we'll have to implement proper vector matching (ignoring order)
+    vector<string> expected_argument_names;
+    expected_argument_names.push_back("fake_allocation");
+    expected_argument_names.push_back("lease6");
+    expected_argument_names.push_back("subnet6");
+
+    sort(callback_argument_names_.begin(), callback_argument_names_.end());
+    sort(expected_argument_names.begin(), expected_argument_names.end());
+
+    EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+}
+
+// This test checks if lease6_select callout is able to override the values
+// in a lease6.
+TEST_F(HookAllocEngine6Test, change_lease6_select) {
+
+    // Make sure that the overridden values are different than the ones from
+    // subnet originally used to create the lease
+    ASSERT_NE(t1_override_, subnet_->getT1());
+    ASSERT_NE(t2_override_, subnet_->getT2());
+    ASSERT_NE(pref_override_, subnet_->getPreferred());
+    ASSERT_NE(valid_override_, subnet_->getValid());
+    ASSERT_FALSE(subnet_->inRange(addr_override_));
+
+    // Create allocation engine (hook names are registered in its ctor)
+    boost::scoped_ptr<AllocEngine> engine;
+    ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+    ASSERT_TRUE(engine);
+
+    // Initialize Hooks Manager
+    vector<string> libraries; // no libraries at this time
+    HooksManager::loadLibraries(libraries);
+
+    // Install a callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "lease6_select", lease6_select_different_callout));
+
+    // Normally, dhcpv6_srv would passed the handle when calling allocateAddress6,
+    // but in tests we need to create it on our own.
+    CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+
+    // Call allocateAddress6. Callouts should be triggered here.
+    Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
+                                               false, callout_handle);
+    // Check that we got a lease
+    ASSERT_TRUE(lease);
+
+    // See if the values overridden by callout are there
+    EXPECT_TRUE(lease->addr_.equals(addr_override_));
+    EXPECT_EQ(t1_override_, lease->t1_);
+    EXPECT_EQ(t2_override_, lease->t2_);
+    EXPECT_EQ(pref_override_, lease->preferred_lft_);
+    EXPECT_EQ(valid_override_, lease->valid_lft_);
+
+    // Now check if the lease is in the database
+    Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->addr_);
+    ASSERT_TRUE(from_mgr);
+
+    // Check if values in the database are overridden
+    EXPECT_TRUE(from_mgr->addr_.equals(addr_override_));
+    EXPECT_EQ(t1_override_, from_mgr->t1_);
+    EXPECT_EQ(t2_override_, from_mgr->t2_);
+    EXPECT_EQ(pref_override_, from_mgr->preferred_lft_);
+    EXPECT_EQ(valid_override_, from_mgr->valid_lft_);
+}
+
+
+/// @brief helper class used in Hooks testing in AllocEngine4
+///
+/// It features a couple of callout functions and buffers to store
+/// the data that is accessible via callouts.
+class HookAllocEngine4Test : public AllocEngine4Test {
+public:
+    HookAllocEngine4Test() {
+        resetCalloutBuffers();
+    }
+
+    virtual ~HookAllocEngine4Test() {
+        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+            "lease4_select");
+    }
+
+    /// @brief clears out buffers, so callouts can store received arguments
+    void resetCalloutBuffers() {
+        callback_name_ = string("");
+        callback_subnet4_.reset();
+        callback_fake_allocation_ = false;
+        callback_lease4_.reset();
+        callback_argument_names_.clear();
+        callback_addr_original_ = IOAddress("::");
+        callback_addr_updated_ = IOAddress("::");
+    }
+
+    /// callback that stores received callout name and received values
+    static int
+    lease4_select_callout(CalloutHandle& callout_handle) {
+
+        callback_name_ = string("lease4_select");
+
+        callout_handle.getArgument("subnet4", callback_subnet4_);
+        callout_handle.getArgument("fake_allocation", callback_fake_allocation_);
+        callout_handle.getArgument("lease4", callback_lease4_);
+
+        callback_addr_original_ = callback_lease4_->addr_;
+
+        callback_argument_names_ = callout_handle.getArgumentNames();
+        return (0);
+    }
+
+    /// callback that overrides the lease with different values
+    static int
+    lease4_select_different_callout(CalloutHandle& callout_handle) {
+
+        // Let's call the basic callout, so it can record all parameters
+        lease4_select_callout(callout_handle);
+
+        // Now we need to tweak the least a bit
+        Lease4Ptr lease;
+        callout_handle.getArgument("lease4", lease);
+        callback_addr_updated_ = addr_override_;
+        lease->addr_ = callback_addr_updated_;
+        lease->t1_ = t1_override_;
+        lease->t2_ = t2_override_;
+        lease->valid_lft_ = valid_override_;
+
+        return (0);
+    }
+
+    // Values to be used in callout to override lease4 content
+    static const IOAddress addr_override_;
+    static const uint32_t t1_override_;
+    static const uint32_t t2_override_;
+    static const uint32_t valid_override_;
+
+    // Callback will store original and overridden values here
+    static IOAddress callback_addr_original_;
+    static IOAddress callback_addr_updated_;
+
+    // Buffers (callback will store received values here)
+    static string callback_name_;
+    static Subnet4Ptr callback_subnet4_;
+    static Lease4Ptr callback_lease4_;
+    static bool callback_fake_allocation_;
+    static vector<string> callback_argument_names_;
+};
+
+// For some reason intialization within a class makes the linker confused.
+// linker complains about undefined references if they are defined within
+// the class declaration.
+const IOAddress HookAllocEngine4Test::addr_override_("192.0.3.1");
+const uint32_t HookAllocEngine4Test::t1_override_ = 4000;
+const uint32_t HookAllocEngine4Test::t2_override_ = 7000;
+const uint32_t HookAllocEngine4Test::valid_override_ = 9000;
+
+IOAddress HookAllocEngine4Test::callback_addr_original_("::");
+IOAddress HookAllocEngine4Test::callback_addr_updated_("::");
+
+string HookAllocEngine4Test::callback_name_;
+Subnet4Ptr HookAllocEngine4Test::callback_subnet4_;
+Lease4Ptr HookAllocEngine4Test::callback_lease4_;
+bool HookAllocEngine4Test::callback_fake_allocation_;
+vector<string> HookAllocEngine4Test::callback_argument_names_;
+
+// This test checks if the lease4_select callout is executed and expected
+// parameters as passed.
+TEST_F(HookAllocEngine4Test, lease4_select) {
+
+    // Note: The following order is working as expected:
+    // 1. create AllocEngine (that register hook points)
+    // 2. call loadLibraries()
+    //
+    // This order, however, causes segfault in HooksManager
+    // 1. call loadLibraries()
+    // 2. create AllocEngine (that register hook points)
+
+    // Create allocation engine (hook names are registered in its ctor)
+    boost::scoped_ptr<AllocEngine> engine;
+    ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+    ASSERT_TRUE(engine);
+
+    // Initialize Hooks Manager
+    vector<string> libraries; // no libraries at this time
+    HooksManager::loadLibraries(libraries);
+
+    // Install pkt4_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "lease4_select", lease4_select_callout));
+
+    CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+
+    Lease4Ptr lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
+                                               IOAddress("0.0.0.0"),
+                                               false, callout_handle);
+    // Check that we got a lease
+    ASSERT_TRUE(lease);
+
+    // Do all checks on the lease
+    checkLease4(lease);
+
+    // Check that the lease is indeed in LeaseMgr
+    Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+    ASSERT_TRUE(from_mgr);
+
+    // Check that callouts were indeed called
+    EXPECT_EQ("lease4_select", callback_name_);
+
+    // Now check that the lease in LeaseMgr has the same parameters
+    ASSERT_TRUE(callback_lease4_);
+    detailCompareLease(callback_lease4_, from_mgr);
+
+    ASSERT_TRUE(callback_subnet4_);
+    EXPECT_EQ(subnet_->toText(), callback_subnet4_->toText());
+
+    EXPECT_EQ(callback_fake_allocation_, false);
+
+    // Check if all expected parameters are reported. It's a bit tricky, because
+    // order may be different. If the test starts failing, because someone tweaked
+    // hooks engine, we'll have to implement proper vector matching (ignoring order)
+    vector<string> expected_argument_names;
+    expected_argument_names.push_back("fake_allocation");
+    expected_argument_names.push_back("lease4");
+    expected_argument_names.push_back("subnet4");
+    EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+}
+
+// This test checks if lease4_select callout is able to override the values
+// in a lease4.
+TEST_F(HookAllocEngine4Test, change_lease4_select) {
+
+    // Make sure that the overridden values are different than the ones from
+    // subnet originally used to create the lease
+    ASSERT_NE(t1_override_, subnet_->getT1());
+    ASSERT_NE(t2_override_, subnet_->getT2());
+    ASSERT_NE(valid_override_, subnet_->getValid());
+    ASSERT_FALSE(subnet_->inRange(addr_override_));
+
+    // Create allocation engine (hook names are registered in its ctor)
+    boost::scoped_ptr<AllocEngine> engine;
+    ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+    ASSERT_TRUE(engine);
+
+    // Initialize Hooks Manager
+    vector<string> libraries; // no libraries at this time
+    HooksManager::loadLibraries(libraries);
+
+    // Install a callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "lease4_select", lease4_select_different_callout));
+
+    // Normally, dhcpv4_srv would passed the handle when calling allocateAddress4,
+    // but in tests we need to create it on our own.
+    CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+
+    // Call allocateAddress4. Callouts should be triggered here.
+    Lease4Ptr lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
+                                               false, callout_handle);
+    // Check that we got a lease
+    ASSERT_TRUE(lease);
+
+    // See if the values overridden by callout are there
+    EXPECT_TRUE(lease->addr_.equals(addr_override_));
+    EXPECT_EQ(t1_override_, lease->t1_);
+    EXPECT_EQ(t2_override_, lease->t2_);
+    EXPECT_EQ(valid_override_, lease->valid_lft_);
+
+    // Now check if the lease is in the database
+    Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+    ASSERT_TRUE(from_mgr);
+
+    // Check if values in the database are overridden
+    EXPECT_TRUE(from_mgr->addr_.equals(addr_override_));
+    EXPECT_EQ(t1_override_, from_mgr->t1_);
+    EXPECT_EQ(t2_override_, from_mgr->t2_);
+    EXPECT_EQ(valid_override_, from_mgr->valid_lft_);
+}
+
+
+
 }; // End of anonymous namespace

+ 45 - 0
src/lib/dhcpsrv/tests/cfgmgr_unittest.cc

@@ -165,6 +165,7 @@ public:
         CfgMgr::instance().deleteSubnets4();
         CfgMgr::instance().deleteSubnets6();
         CfgMgr::instance().deleteOptionDefs();
+        CfgMgr::instance().deleteActiveIfaces();
     }
 
     /// @brief generates interface-id option based on provided text
@@ -573,6 +574,50 @@ TEST_F(CfgMgrTest, optionSpace6) {
     // @todo decide if a duplicate vendor space is allowed.
 }
 
+// This test verifies that it is possible to specify interfaces that server
+// should listen on.
+TEST_F(CfgMgrTest, addActiveIface) {
+    CfgMgr& cfg_mgr = CfgMgr::instance();
+
+    cfg_mgr.addActiveIface("eth0");
+    cfg_mgr.addActiveIface("eth1");
+
+    EXPECT_TRUE(cfg_mgr.isActiveIface("eth0"));
+    EXPECT_TRUE(cfg_mgr.isActiveIface("eth1"));
+    EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
+
+    cfg_mgr.deleteActiveIfaces();
+
+    EXPECT_FALSE(cfg_mgr.isActiveIface("eth0"));
+    EXPECT_FALSE(cfg_mgr.isActiveIface("eth1"));
+    EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
+}
+
+// This test verifies that it is possible to set the flag which configures the
+// server to listen on all interfaces.
+TEST_F(CfgMgrTest, activateAllIfaces) {
+    CfgMgr& cfg_mgr = CfgMgr::instance();
+
+    cfg_mgr.addActiveIface("eth0");
+    cfg_mgr.addActiveIface("eth1");
+
+    ASSERT_TRUE(cfg_mgr.isActiveIface("eth0"));
+    ASSERT_TRUE(cfg_mgr.isActiveIface("eth1"));
+    ASSERT_FALSE(cfg_mgr.isActiveIface("eth2"));
+
+    cfg_mgr.activateAllIfaces();
+
+    EXPECT_TRUE(cfg_mgr.isActiveIface("eth0"));
+    EXPECT_TRUE(cfg_mgr.isActiveIface("eth1"));
+    EXPECT_TRUE(cfg_mgr.isActiveIface("eth2"));
+
+    cfg_mgr.deleteActiveIfaces();
+
+    EXPECT_FALSE(cfg_mgr.isActiveIface("eth0"));
+    EXPECT_FALSE(cfg_mgr.isActiveIface("eth1"));
+    EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
+}
+
 // No specific tests for getSubnet6. That method (2 overloaded versions) is tested
 // in Dhcpv6SrvTest.selectSubnetAddr and Dhcpv6SrvTest.selectSubnetIface
 // (see src/bin/dhcp6/tests/dhcp6_srv_unittest.cc)

+ 71 - 39
src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc

@@ -46,12 +46,13 @@ public:
     /// @brief Constructor
     ///
     DhcpParserTest() {
+        CfgMgr::instance().deleteActiveIfaces();
     }
 };
 
 
 /// @brief Check BooleanParser basic functionality.
-/// 
+///
 /// Verifies that the parser:
 /// 1. Does not allow empty for storage.
 /// 2. Rejects a non-boolean element.
@@ -98,7 +99,7 @@ TEST_F(DhcpParserTest, booleanParserTest) {
 }
 
 /// @brief Check StringParser basic functionality
-/// 
+///
 /// Verifies that the parser:
 /// 1. Does not allow empty for storage.
 /// 2. Builds with a nont string value.
@@ -138,7 +139,7 @@ TEST_F(DhcpParserTest, stringParserTest) {
 }
 
 /// @brief Check Uint32Parser basic functionality
-/// 
+///
 /// Verifies that the parser:
 /// 1. Does not allow empty for storage.
 /// 2. Rejects a non-integer element.
@@ -167,8 +168,8 @@ TEST_F(DhcpParserTest, uint32ParserTest) {
     ElementPtr int_element = Element::create(-1);
     EXPECT_THROW(parser.build(int_element), isc::BadValue);
 
-    // Verify that parser with rejects too large a value provided we are on 
-    // 64-bit platform. 
+    // Verify that parser with rejects too large a value provided we are on
+    // 64-bit platform.
     if (sizeof(long) > sizeof(uint32_t)) {
         long max = (long)(std::numeric_limits<uint32_t>::max()) + 1;
         int_element->setValue(max);
@@ -201,30 +202,61 @@ TEST_F(DhcpParserTest, uint32ParserTest) {
 ///
 /// Verifies that the parser:
 /// 1. Does not allow empty for storage.
-/// 2. Does not allow name other than "interface"
-///
-/// InterfaceListParser doesn't do very much, this test will need to 
-/// expand once it does.
+/// 2. Does not allow name other than "interfaces"
+/// 3. Parses list of interfaces and adds them to CfgMgr
+/// 4. Parses wildcard interface name and sets a CfgMgr flag which indicates
+/// that server will listen on all interfaces.
 TEST_F(DhcpParserTest, interfaceListParserTest) {
 
-    const std::string name = "interface";
+    const std::string name = "interfaces";
 
     // Verify that parser constructor fails if parameter name isn't "interface"
     EXPECT_THROW(InterfaceListConfigParser("bogus_name"), isc::BadValue);
 
-    InterfaceListConfigParser parser(name);
+    boost::scoped_ptr<InterfaceListConfigParser>
+        parser(new InterfaceListConfigParser(name));
     ElementPtr list_element = Element::createList();
     list_element->add(Element::create("eth0"));
     list_element->add(Element::create("eth1"));
+
+    // Make sure there are no interfaces added yet.
+    ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth0"));
+    ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth1"));
+
+    // This should parse the configuration and add eth0 and eth1 to the list
+    // of interfaces that server should listen on.
+    parser->build(list_element);
+    parser->commit();
+
+    // Use CfgMgr instance to check if eth0 and eth1 was added, and that
+    // eth2 was not added.
+    CfgMgr& cfg_mgr = CfgMgr::instance();
+    EXPECT_TRUE(cfg_mgr.isActiveIface("eth0"));
+    EXPECT_TRUE(cfg_mgr.isActiveIface("eth1"));
+    EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
+
+    // Add keyword all to the configuration. This should activate all
+    // interfaces, including eth2, even though it has not been explicitly
+    // added.
+    list_element->add(Element::create("*"));
+
+    // Reset parser's state.
+    parser.reset(new InterfaceListConfigParser(name));
+    parser->build(list_element);
+    parser->commit();
+
+    EXPECT_TRUE(cfg_mgr.isActiveIface("eth0"));
+    EXPECT_TRUE(cfg_mgr.isActiveIface("eth1"));
+    EXPECT_TRUE(cfg_mgr.isActiveIface("eth2"));
 }
 
-/// @brief Test Implementation of abstract OptionDataParser class. Allows 
-/// testing basic option parsing.   
+/// @brief Test Implementation of abstract OptionDataParser class. Allows
+/// testing basic option parsing.
 class UtestOptionDataParser : public OptionDataParser {
 public:
 
-    UtestOptionDataParser(const std::string&, 
-        OptionStoragePtr options, ParserContextPtr global_context) 
+    UtestOptionDataParser(const std::string&,
+        OptionStoragePtr options, ParserContextPtr global_context)
         :OptionDataParser("", options, global_context) {
     }
 
@@ -238,12 +270,12 @@ protected:
     virtual OptionDefinitionPtr findServerSpaceOptionDefinition (
                 std::string&, uint32_t) {
         OptionDefinitionPtr def;
-        // always return empty 
+        // always return empty
         return (def);
     }
 };
 
-/// @brief Test Fixture class which provides basic structure for testing 
+/// @brief Test Fixture class which provides basic structure for testing
 /// configuration parsing.  This is essentially the same structure provided
 /// by dhcp servers.
 class ParseConfigTest : public ::testing::Test {
@@ -257,15 +289,15 @@ public:
         reset_context();
     }
 
-    /// @brief Parses a configuration.   
+    /// @brief Parses a configuration.
     ///
     /// Parse the given configuration, populating the context storage with
-    /// the parsed elements.  
-    /// 
+    /// the parsed elements.
+    ///
     /// @param config_set is the set of elements to parse.
     /// @return returns an ConstElementPtr containing the numeric result
     /// code and outcome comment.
-    isc::data::ConstElementPtr parseElementSet(isc::data::ConstElementPtr 
+    isc::data::ConstElementPtr parseElementSet(isc::data::ConstElementPtr
                                            config_set) {
         // Answer will hold the result.
         ConstElementPtr answer;
@@ -297,7 +329,7 @@ public:
             }
 
             // The option values parser is the next one to be run.
-            std::map<std::string, ConstElementPtr>::const_iterator 
+            std::map<std::string, ConstElementPtr>::const_iterator
                                 option_config = values_map.find("option-data");
             if (option_config != values_map.end()) {
                 option_parser->build(option_config->second);
@@ -360,10 +392,10 @@ public:
     /// @brief Convenience method for parsing a configuration 
     /// 
     /// Given a configuration string, convert it into Elements
-    /// and parse them. 
+    /// and parse them.
     /// @param config is the configuration string to parse
     ///
-    /// @return retuns 0 if the configuration parsed successfully, 
+    /// @return retuns 0 if the configuration parsed successfully,
     /// non-zero otherwise failure.
     int parseConfiguration(const std::string& config) {    
         int rcode_ = 1;
@@ -380,17 +412,17 @@ public:
         return (rcode_);
     }
 
-    /// @brief Find an option definition for a given space and code within 
+    /// @brief Find an option definition for a given space and code within
     /// the parser context.
     /// @param space is the space name of the desired option.
     /// @param code is the numeric "type" of the desired option.
     /// @return returns an OptionDefinitionPtr which points to the found
     /// definition or is empty.
-    /// ASSERT_ tests don't work inside functions that return values 
+    /// ASSERT_ tests don't work inside functions that return values
     OptionDefinitionPtr getOptionDef(std::string space, uint32_t code)
     {
         OptionDefinitionPtr def;
-        OptionDefContainerPtr defs = 
+        OptionDefContainerPtr defs =
                             parser_context_->option_defs_->getItems(space);
         // Should always be able to get definitions list even if it is empty.
         EXPECT_TRUE(defs);
@@ -404,41 +436,41 @@ public:
                 def = *(idx.begin());
             }
         }
-        return (def); 
+        return (def);
     }
 
-    /// @brief Find an option for a given space and code within the parser 
+    /// @brief Find an option for a given space and code within the parser
     /// context.
     /// @param space is the space name of the desired option.
     /// @param code is the numeric "type" of the desired option.
     /// @return returns an OptionPtr which points to the found
     /// option or is empty.
-    /// ASSERT_ tests don't work inside functions that return values 
+    /// ASSERT_ tests don't work inside functions that return values
     OptionPtr getOptionPtr(std::string space, uint32_t code)
     {
         OptionPtr option_ptr;
-        Subnet::OptionContainerPtr options = 
+        Subnet::OptionContainerPtr options =
                             parser_context_->options_->getItems(space);
         // Should always be able to get options list even if it is empty.
         EXPECT_TRUE(options);
         if (options) {
             // Attempt to find desired option.
             const Subnet::OptionContainerTypeIndex& idx = options->get<1>();
-            const Subnet::OptionContainerTypeRange& range = 
+            const Subnet::OptionContainerTypeRange& range =
                                                         idx.equal_range(code);
             int cnt = std::distance(range.first, range.second);
             EXPECT_EQ(1, cnt);
             if (cnt == 1) {
-                Subnet::OptionDescriptor desc = *(idx.begin()); 
-                option_ptr = desc.option; 
+                Subnet::OptionDescriptor desc = *(idx.begin());
+                option_ptr = desc.option;
                 EXPECT_TRUE(option_ptr);
             }
         }
 
-        return (option_ptr); 
+        return (option_ptr);
     }
 
-    /// @brief Wipes the contents of the context to allowing another parsing 
+    /// @brief Wipes the contents of the context to allowing another parsing
     /// during a given test if needed.
     void reset_context(){
         // Note set context universe to V6 as it has to be something.
@@ -464,7 +496,7 @@ public:
 };
 
 /// @brief Check Basic parsing of option definitions.
-/// 
+///
 /// Note that this tests basic operation of the OptionDefinitionListParser and
 /// OptionDefinitionParser.  It uses a simple configuration consisting of one
 /// one definition and verifies that it is parsed and committed to storage
@@ -490,7 +522,7 @@ TEST_F(ParseConfigTest, basicOptionDefTest) {
 
 
     // Verify that the option definition can be retrieved.
-    OptionDefinitionPtr def = getOptionDef("isc", 100); 
+    OptionDefinitionPtr def = getOptionDef("isc", 100);
     ASSERT_TRUE(def);
 
     // Verify that the option definition is correct.
@@ -502,7 +534,7 @@ TEST_F(ParseConfigTest, basicOptionDefTest) {
 }
 
 /// @brief Check Basic parsing of options.
-/// 
+///
 /// Note that this tests basic operation of the OptionDataListParser and
 /// OptionDataParser.  It uses a simple configuration consisting of one
 /// one definition and matching option data.  It verifies that the option

+ 13 - 12
src/lib/hooks/callout_manager.cc

@@ -29,6 +29,19 @@ using namespace std;
 namespace isc {
 namespace hooks {
 
+// Constructor
+CalloutManager::CalloutManager(int num_libraries)
+    : current_hook_(-1), current_library_(-1),
+      hook_vector_(ServerHooks::getServerHooks().getCount()),
+      library_handle_(this), pre_library_handle_(this, 0),
+      post_library_handle_(this, INT_MAX), num_libraries_(num_libraries)
+{
+    if (num_libraries < 0) {
+        isc_throw(isc::BadValue, "number of libraries passed to the "
+                  "CalloutManager must be >= 0");
+    }
+}
+
 // Check that the index of a library is valid.  It can range from 1 - n
 // (n is the number of libraries), 0 (pre-user library callouts), or INT_MAX
 // (post-user library callouts).  It can also be -1 to indicate an invalid
@@ -46,18 +59,6 @@ CalloutManager::checkLibraryIndex(int library_index) const {
               num_libraries_ << ")");
 }
 
-// Set the number of libraries handled by the CalloutManager.
-
-void
-CalloutManager::setNumLibraries(int num_libraries) {
-    if (num_libraries < 0) {
-        isc_throw(isc::BadValue, "number of libraries passed to the "
-                  "CalloutManager must be >= 0");
-    }
-
-    num_libraries_ = num_libraries;
-}
-
 // Register a callout for the current library.
 
 void

+ 2 - 29
src/lib/hooks/callout_manager.h

@@ -132,19 +132,8 @@ public:
     ///
     /// @param num_libraries Number of loaded libraries.
     ///
-    /// @throw isc::BadValue if the number of libraries is less than or equal
-    ///        to 0, or if the pointer to the server hooks object is empty.
-    CalloutManager(int num_libraries = 0)
-        : current_hook_(-1), current_library_(-1),
-          hook_vector_(ServerHooks::getServerHooks().getCount()),
-          library_handle_(this), pre_library_handle_(this, 0),
-          post_library_handle_(this, INT_MAX), num_libraries_(num_libraries)
-    {
-        // Check that the number of libraries is OK.  (This does a redundant
-        // set of the number of libraries, but it's only a single assignment
-        // and avoids the need for a separate "check" method.
-        setNumLibraries(num_libraries);
-    }
+    /// @throw isc::BadValue if the number of libraries is less than 0,
+    CalloutManager(int num_libraries = 0);
 
     /// @brief Register a callout on a hook for the current library
     ///
@@ -224,22 +213,6 @@ public:
         return (current_hook_);
     }
 
-    /// @brief Set number of libraries
-    ///
-    /// Sets the number of libraries.  Although the value is passed to the
-    /// constructor, in some cases that is only an estimate and the number
-    /// can only be determined after the CalloutManager is created.
-    ///
-    /// @note If the number if libraries is reset, it must be done *before*
-    ///       any callouts are registered.
-    ///
-    /// @param num_libraries Number of libraries served by this CalloutManager.
-    ///
-    /// @throw BadValue Number of libraries must be >= 0.
-    /// @throw LibraryCountChanged Number of libraries has been changed after
-    ///        callouts have been registered.
-    void setNumLibraries(int num_libraries);
-
     /// @brief Get number of libraries
     ///
     /// Returns the number of libraries that this CalloutManager is expected

+ 29 - 2
src/lib/hooks/hooks_manager.cc

@@ -81,8 +81,15 @@ HooksManager::loadLibrariesInternal(const std::vector<std::string>& libraries) {
     lm_collection_.reset(new LibraryManagerCollection(libraries));
     bool status = lm_collection_->loadLibraries();
 
-    // ... and obtain the callout manager for them.
-    callout_manager_ = lm_collection_->getCalloutManager();
+    if (status) {
+        // ... and obtain the callout manager for them if successful.
+        callout_manager_ = lm_collection_->getCalloutManager();
+    } else {
+        // Unable to load libraries, reset to state before this function was
+        // called.
+        lm_collection_.reset();
+        callout_manager_.reset();
+    }
 
     return (status);
 }
@@ -124,6 +131,19 @@ HooksManager::createCalloutHandle() {
     return (getHooksManager().createCalloutHandleInternal());
 }
 
+// Get the list of the names of loaded libraries.
+
+std::vector<std::string>
+HooksManager::getLibraryNamesInternal() const {
+    return (lm_collection_ ? lm_collection_->getLibraryNames()
+                           : std::vector<std::string>());
+}
+
+std::vector<std::string>
+HooksManager::getLibraryNames() {
+    return (getHooksManager().getLibraryNamesInternal());
+}
+
 // Perform conditional initialization if nothing is loaded.
 
 void
@@ -169,5 +189,12 @@ HooksManager::postCalloutsLibraryHandle() {
     return (getHooksManager().postCalloutsLibraryHandleInternal());
 }
 
+// Validate libraries
+
+std::vector<std::string>
+HooksManager::validateLibraries(const std::vector<std::string>& libraries) {
+    return (LibraryManagerCollection::validateLibraries(libraries));
+}
+
 } // namespace util
 } // namespace isc

+ 40 - 0
src/lib/hooks/hooks_manager.h

@@ -178,6 +178,30 @@ public:
     ///         registered.
     static int registerHook(const std::string& name);
 
+    /// @brief Return list of loaded libraries
+    ///
+    /// Returns the names of the loaded libraries.
+    ///
+    /// @return List of loaded library names.
+    static std::vector<std::string> getLibraryNames();
+
+    /// @brief Validate library list
+    ///
+    /// For each library passed to it, checks that the library can be opened
+    /// and that the "version" function is present and gives the right answer.
+    /// Each library is closed afterwards.
+    ///
+    /// This is used during the configuration parsing - when the list of hooks
+    /// libraries is changed, each of the new libraries is checked before the
+    /// change is committed.
+    ///
+    /// @param List of libraries to be validated.
+    ///
+    /// @return An empty vector if all libraries validated.  Otherwise it
+    ///         holds the names of the libraries that failed validation.
+    static std::vector<std::string> validateLibraries(
+                       const std::vector<std::string>& libraries);
+
     /// Index numbers for pre-defined hooks.
     static const int CONTEXT_CREATE = ServerHooks::CONTEXT_CREATE;
     static const int CONTEXT_DESTROY = ServerHooks::CONTEXT_DESTROY;
@@ -195,6 +219,17 @@ private:
     /// but actually do the work on the singleton instance of the HooksManager.
     /// See the descriptions of the static methods for more details.
 
+    /// @brief Validate library list
+    ///
+    /// @param List of libraries to be validated.
+    ///
+    /// @return An empty string if all libraries validated.  Otherwise it is
+    ///         the name of the first library that failed validation.  The
+    ///         configuration code can return this to bindctl as an indication
+    ///         of the problem.
+    std::string validateLibrariesInternal(
+                       const std::vector<std::string>& libraries) const;
+
     /// @brief Load and reload libraries
     ///
     /// @param libraries List of libraries to be loaded.  The order is
@@ -241,6 +276,11 @@ private:
     ///         registration.
     LibraryHandle& postCalloutsLibraryHandleInternal();
 
+    /// @brief Return list of loaded libraries
+    ///
+    /// @return List of loaded library names.
+    std::vector<std::string> getLibraryNamesInternal() const;
+
     //@}
 
     /// @brief Initialization to No Libraries

+ 48 - 53
src/lib/hooks/hooks_messages.mes

@@ -14,47 +14,57 @@
 
 $NAMESPACE isc::hooks
 
+% HOOKS_ALL_CALLOUTS_DEREGISTERED hook library at index %1 removed all callouts on hook %2
+A debug message issued when all callouts on the specified hook registered
+by the library with the given index were removed.  This is similar to
+the HOOKS_CALLOUTS_REMOVED message (and the two are likely to be seen
+together), but is issued at a lower-level in the hook framework.
+
+% HOOKS_CALLOUTS_REMOVED callouts removed from hook %1 for library %2
+This is a debug message issued during library unloading.  It notes that
+one of more callouts registered by that library have been removed from
+the specified hook.  This is similar to the HOOKS_DEREGISTER_ALL_CALLOUTS
+message (and the two are likely to be seen together), but is issued at a
+higher-level in the hook framework.
+
 % HOOKS_CALLOUT_CALLED hooks library with index %1 has called a callout on hook %2 that has address %3
 Only output at a high debugging level, this message indicates that
 a callout on the named hook registered by the library with the given
 index (in the list of loaded libraries) has been called and returned a
 success state.  The address of the callout is given in the message
 
+% HOOKS_CALLOUT_DEREGISTERED hook library at index %1 deregistered a callout on hook %2
+A debug message issued when all instances of a particular callouts on
+the hook identified in the message that were registered by the library
+with the given index have been removed.
+
 % HOOKS_CALLOUT_ERROR error returned by callout on hook %1 registered by library with index %2 (callout address %3)
 If a callout returns an error status when called, this error message
 is issued.  It identifies the hook to which the callout is attached, the
 index of the library (in the list of loaded libraries) that registered
 it and the address of the callout.  The error is otherwise ignored.
 
-% HOOKS_CALLOUTS_REMOVED callouts removed from hook %1 for library %2
-This is a debug message issued during library unloading.  It notes that
-one of more callouts registered by that library have been removed from
-the specified hook.  This is similar to the HOOKS_DEREGISTER_ALL_CALLOUTS
-message (and the two are likely to be seen together), but is issued at a
-higher-level in the hook framework.
-
-% HOOKS_CLOSE_ERROR failed to close hook library %1: %2
-BIND 10 has failed to close the named hook library for the stated reason.
-Although this is an error, this should not affect the running system
-other than as a loss of resources.  If this error persists, you should
-restart BIND 10.
-
 % HOOKS_CALLOUT_EXCEPTION exception thrown by callout on hook %1 registered by library with index %2 (callout address %3)
 If a callout throws an exception when called, this error message is
 issued.  It identifies the hook to which the callout is attached, the
 index of the library (in the list of loaded libraries) that registered
 it and the address of the callout.  The error is otherwise ignored.
 
-% HOOKS_ALL_CALLOUTS_DEREGISTERED hook library at index %1 removed all callouts on hook %2
-A debug message issued when all callouts on the specified hook registered
-by the library with the given index were removed.  This is similar to
-the HOOKS_CALLOUTS_REMOVED message (and the two are likely to be seen
-together), but is issued at a lower-level in the hook framework.
+% HOOKS_CALLOUT_REGISTRATION hooks library with index %1 registering callout for hook '%2'
+This is a debug message, output when a library (whose index in the list
+of libraries (being) loaded is given) registers a callout.
 
-% HOOKS_CALLOUT_DEREGISTERED hook library at index %1 deregistered a callout on hook %2
-A debug message issued when all instances of a particular callouts on
-the hook identified in the message that were registered by the library
-with the given index have been removed.
+% HOOKS_CLOSE_ERROR failed to close hook library %1: %2
+BIND 10 has failed to close the named hook library for the stated reason.
+Although this is an error, this should not affect the running system
+other than as a loss of resources.  If this error persists, you should
+restart BIND 10.
+
+% HOOKS_HOOK_LIST_RESET the list of hooks has been reset
+This is a message indicating that the list of hooks has been reset.
+While this is usual when running the BIND 10 test suite, it should not be
+seen when running BIND 10 in a producion environment.  If this appears,
+please report a bug through the usual channels.
 
 % HOOKS_INCORRECT_VERSION hook library %1 is at version %2, require version %3
 BIND 10 has detected that the named hook library has been built against
@@ -69,18 +79,24 @@ library should fix the problem in most cases.
 This information message is issued when a user-supplied hooks library
 has been successfully loaded.
 
+% HOOKS_LIBRARY_LOADING loading hooks library %1
+This is a debug message output just before the specified library is loaded.
+If the action is successfully, it will be followed by the
+HOOKS_LIBRARY_LOADED informational message.
+
 % HOOKS_LIBRARY_UNLOADED hooks library %1 successfully unloaded
 This information message is issued when a user-supplied hooks library
 has been successfully unloaded.
 
+% HOOKS_LIBRARY_UNLOADING unloading library %1
+This is a debug message called when the specified library is
+being unloaded.  If all is successful, it will be followed by the
+HOOKS_LIBRARY_UNLOADED informational message.
+
 % HOOKS_LIBRARY_VERSION hooks library %1 reports its version as %2
 A debug  message issued when the version check on the hooks library
 has succeeded.
 
-% HOOKS_LOAD_SUCCESS 'load' function in hook library %1 returned success
-This is a debug message issued when the "load" function has been found
-in a hook library and has been successfully called.
-
 % HOOKS_LOAD_ERROR 'load' function in hook library %1 returned error %2
 A "load" function was found in the library named in the message and
 was called.  The function returned a non-zero status (also given in
@@ -93,10 +109,9 @@ was called.  The function threw an exception (an error indication)
 during execution, which is an error condition.  The library has been
 unloaded and no callouts from it will be installed.
 
-% HOOKS_LIBRARY_LOADING loading hooks library %1
-This is a debug message output just before the specified library is loaded.
-If the action is successfully, it will be followed by the
-HOOKS_LIBRARY_LOADED informational message.
+% HOOKS_LOAD_SUCCESS 'load' function in hook library %1 returned success
+This is a debug message issued when the "load" function has been found
+in a hook library and has been successfully called.
 
 % HOOKS_NO_LOAD no 'load' function found in hook library %1
 This is a debug message saying that the specified library was loaded
@@ -120,30 +135,11 @@ BIND 10 failed to open the specified hook library for the stated
 reason. The library has not been loaded.  BIND 10 will continue to
 function, but without the services offered by the library.
 
-% HOOKS_CALLOUT_REGISTRATION hooks library with index %1 registering callout for hook '%2'
-This is a debug message, output when a library (whose index in the list
-of libraries (being) loaded is given) registers a callout.
-
-% HOOKS_HOOK_REGISTERED hook %1 was registered
-This is a debug message indicating that a hook of the specified name
-was registered by a BIND 10 server.  The server doing the logging is
-indicated by the full name of the logger used to log this message.
-
 % HOOKS_STD_CALLOUT_REGISTERED hooks library %1 registered standard callout for hook %2 at address %3
 This is a debug message, output when the library loading function has
 located a standard callout (a callout with the same name as a hook point)
 and registered it.  The address of the callout is indicated.
 
-% HOOKS_HOOK_LIST_RESET the list of hooks has been reset
-This is a message indicating that the list of hooks has been reset.
-While this is usual when running the BIND 10 test suite, it should not be
-seen when running BIND 10 in a producion environment.  If this appears,
-please report a bug through the usual channels.
-
-% HOOKS_UNLOAD_SUCCESS 'unload' function in hook library %1 returned success
-This is a debug message issued when an "unload" function has been found
-in a hook library during the unload process, called, and returned success.
-
 % HOOKS_UNLOAD_ERROR 'unload' function in hook library %1 returned error %2
 During the unloading of a library, an "unload" function was found.
 It was called, but returned an error (non-zero) status, resulting in
@@ -156,10 +152,9 @@ called, but in the process generated an exception (an error indication).
 The unload process continued after this message and the library has
 been unloaded.
 
-% HOOKS_LIBRARY_UNLOADING unloading library %1
-This is a debug message called when the specified library is
-being unloaded.  If all is successful, it will be followed by the
-HOOKS_LIBRARY_UNLOADED informational message.
+% HOOKS_UNLOAD_SUCCESS 'unload' function in hook library %1 returned success
+This is a debug message issued when an "unload" function has been found
+in a hook library during the unload process, called, and returned success.
 
 % HOOKS_VERSION_EXCEPTION 'version' function in hook library %1 threw an exception
 This error message is issued if the version() function in the specified

+ 88 - 27
src/lib/hooks/library_manager.cc

@@ -31,6 +31,42 @@ namespace isc {
 namespace hooks {
 
 
+// Constructor (used by external agency)
+LibraryManager::LibraryManager(const std::string& name, int index,
+                               const boost::shared_ptr<CalloutManager>& manager)
+        : dl_handle_(NULL), index_(index), manager_(manager),
+          library_name_(name)
+{
+    if (!manager) {
+        isc_throw(NoCalloutManager, "must specify a CalloutManager when "
+                  "instantiating a LibraryManager object");
+    }
+}
+
+// Constructor (used by "validate" for library validation).  Note that this
+// sets "manager_" to not point to anything, which means that methods such as
+// registerStandardCallout() will fail, probably with a segmentation fault.
+// There are no checks for this condition in those methods: this constructor
+// is declared "private", so can only be executed by a method in this class.
+// The only method to do so is "validateLibrary", which takes care not to call
+// methods requiring a non-NULL manager.
+LibraryManager::LibraryManager(const std::string& name)
+        : dl_handle_(NULL), index_(-1), manager_(), library_name_(name)
+{}
+
+// Destructor.  
+LibraryManager::~LibraryManager() {
+    if (manager_) {
+        // LibraryManager instantiated to load a library, so ensure that
+        // it is unloaded before exiting.
+        static_cast<void>(unloadLibrary());
+    } else {
+        // LibraryManager instantiated to validate a library, so just ensure
+        // that it is closed before exiting.
+        static_cast<void>(closeLibrary());
+    }
+}
+
 // Open the library
 
 bool
@@ -118,8 +154,9 @@ LibraryManager::registerStandardCallouts() {
             // Found a symbol, so register it.
             manager_->getLibraryHandle().registerCallout(hook_names[i],
                                                          pc.calloutPtr());
-            LOG_DEBUG(hooks_logger, HOOKS_DBG_CALLS, HOOKS_STD_CALLOUT_REGISTERED)
-                .arg(library_name_).arg(hook_names[i]).arg(dlsym_ptr);
+            LOG_DEBUG(hooks_logger, HOOKS_DBG_CALLS,
+                      HOOKS_STD_CALLOUT_REGISTERED).arg(library_name_)
+                      .arg(hook_names[i]).arg(dlsym_ptr);
 
         }
     }
@@ -251,41 +288,65 @@ LibraryManager::loadLibrary() {
 }
 
 // The library unloading function.  Call the unload() function (if present),
-// remove callouts from the callout manager, then close the library.
+// remove callouts from the callout manager, then close the library.  This is
+// only run if the library is still loaded and is a no-op if the library is
+// not open.
 
 bool
 LibraryManager::unloadLibrary() {
-    LOG_DEBUG(hooks_logger, HOOKS_DBG_TRACE, HOOKS_LIBRARY_UNLOADING)
-        .arg(library_name_);
+    bool result = true;
+    if (dl_handle_ != NULL) {
+        LOG_DEBUG(hooks_logger, HOOKS_DBG_TRACE, HOOKS_LIBRARY_UNLOADING)
+            .arg(library_name_);
 
-    // Call the unload() function if present.  Note that this is done first -
-    // operations take place in the reverse order to which they were done when
-    // the library was loaded.
-    bool result = runUnload();
+        // Call the unload() function if present.  Note that this is done first
+        // - operations take place in the reverse order to which they were done
+        // when the library was loaded.
+        result = runUnload();
+
+        // Regardless of status, remove all callouts associated with this
+        // library on all hooks.
+        vector<string> hooks = ServerHooks::getServerHooks().getHookNames();
+        manager_->setLibraryIndex(index_);
+        for (int i = 0; i < hooks.size(); ++i) {
+            bool removed = manager_->deregisterAllCallouts(hooks[i]);
+            if (removed) {
+                LOG_DEBUG(hooks_logger, HOOKS_DBG_CALLS, HOOKS_CALLOUTS_REMOVED)
+                    .arg(hooks[i]).arg(library_name_);
+            }
+        }
 
-    // Regardless of status, remove all callouts associated with this library
-    // on all hooks.
-    vector<string> hooks = ServerHooks::getServerHooks().getHookNames();
-    manager_->setLibraryIndex(index_);
-    for (int i = 0; i < hooks.size(); ++i) {
-        bool removed = manager_->deregisterAllCallouts(hooks[i]);
-        if (removed) {
-            LOG_DEBUG(hooks_logger, HOOKS_DBG_CALLS, HOOKS_CALLOUTS_REMOVED)
-                .arg(hooks[i]).arg(library_name_);
+        // ... and close the library.
+        result = closeLibrary() && result;
+        if (result) {
+
+            // Issue the informational message only if the library was unloaded
+            // with no problems.  If there was an issue, an error message would
+            // have been issued.
+            LOG_INFO(hooks_logger, HOOKS_LIBRARY_UNLOADED).arg(library_name_);
         }
     }
+    return (result);
+}
 
-    // ... and close the library.
-    result = closeLibrary() && result;
-    if (result) {
+// Validate the library.  We must be able to open it, and the version function
+// must both exist and return the right number.  Note that this is a static
+// method.
 
-        // Issue the informational message only if the library was unloaded
-        // with no problems.  If there was an issue, an error message would
-        // have been issued.
-        LOG_INFO(hooks_logger, HOOKS_LIBRARY_UNLOADED).arg(library_name_);
-    }
+bool
+LibraryManager::validateLibrary(const std::string& name) {
+    // Instantiate a library manager for the validation.  We use the private
+    // constructor as we don't supply a CalloutManager.
+    LibraryManager manager(name);
 
-    return (result);
+    // Try to open it and, if we succeed, check the version.
+    bool validated = manager.openLibrary() && manager.checkVersion();
+
+    // Regardless of whether the version checked out, close the library. (This
+    // is a no-op if the library failed to open.)
+    static_cast<void>(manager.closeLibrary());
+
+    return (validated);
 }
 
 } // namespace hooks

+ 50 - 9
src/lib/hooks/library_manager.h

@@ -15,6 +15,8 @@
 #ifndef LIBRARY_MANAGER_H
 #define LIBRARY_MANAGER_H
 
+#include <exceptions/exceptions.h>
+
 #include <boost/shared_ptr.hpp>
 
 #include <string>
@@ -22,13 +24,25 @@
 namespace isc {
 namespace hooks {
 
+/// @brief No Callout Manager
+///
+/// Thrown if a library manager is instantiated by an external agency without
+/// specifying a CalloutManager object.
+class NoCalloutManager : public Exception {
+public:
+    NoCalloutManager(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) {}
+};
+
 class CalloutManager;
 class LibraryHandle;
 class LibraryManager;
 
 /// @brief Library manager
 ///
-/// This class handles the loading and unloading of a specific library.
+/// This class handles the loading and unloading of a specific library.  It also
+/// provides a static method for checking that a library is valid (this is used
+/// in configuration parsing).
 ///
 /// On loading, it opens the library using dlopen and checks the version (set
 /// with the "version" method.  If all is OK, it iterates through the list of
@@ -58,22 +72,28 @@ class LibraryManager;
 ///       suspends processing of new requests until all existing ones have
 ///       been serviced and all packet/context structures destroyed before
 ///       reloading the libraries.
+///
+/// When validating a library, only the fact that the library can be opened and
+/// version() exists and returns the correct number is checked.  The library
+/// is closed after the validation.
 
 class LibraryManager {
 public:
     /// @brief Constructor
     ///
-    /// Stores the library name.  The actual loading is done in loadLibrary().
+    /// This constructor is used by external agencies (i.e. the
+    /// LibraryManagerCollection) when instantiating a LibraryManager.  It
+    /// stores the library name - the actual actual loading is done in
+    /// loadLibrary().
     ///
     /// @param name Name of the library to load.  This should be an absolute
     ///        path name.
     /// @param index Index of this library
     /// @param manager CalloutManager object
+    ///
+    /// @throw NoCalloutManager Thrown if the manager argument is NULL.
     LibraryManager(const std::string& name, int index,
-                   const boost::shared_ptr<CalloutManager>& manager)
-        : dl_handle_(NULL), index_(index), manager_(manager),
-          library_name_(name)
-    {}
+                   const boost::shared_ptr<CalloutManager>& manager);
 
     /// @brief Destructor
     ///
@@ -81,9 +101,19 @@ public:
     /// feature to ensure closure in the case of an exception destroying this
     /// object.  However, see the caveat in the class header about when it is
     /// safe to unload libraries.
-    ~LibraryManager() {
-        static_cast<void>(unloadLibrary());
-    }
+    ~LibraryManager();
+
+    /// @brief Validate library
+    ///
+    /// A static method that is used to validate a library.  Validation checks
+    /// that the library can be opened, that "version" exists, and that it
+    /// returns the right number.
+    ///
+    /// @param name Name of the library to validate
+    ///
+    /// @return true if the library validated, false if not.  If the library
+    /// fails to validate, the reason for the failure is logged.
+    static bool validateLibrary(const std::string& name);
 
     /// @brief Loads a library
     ///
@@ -176,6 +206,17 @@ protected:
     bool runUnload();
 
 private:
+    /// @brief Validating constructor
+    ///
+    /// Constructor used when the LibraryManager is instantiated to validate
+    /// a library (i.e. by the "validateLibrary" static method).
+    ///
+    /// @param name Name of the library to load.  This should be an absolute
+    ///        path name.
+    LibraryManager(const std::string& name);
+
+    // Member variables
+
     void*       dl_handle_;     ///< Handle returned by dlopen
     int         index_;         ///< Index associated with this library
     boost::shared_ptr<CalloutManager> manager_;

+ 28 - 13
src/lib/hooks/library_manager_collection.cc

@@ -73,24 +73,18 @@ LibraryManagerCollection::loadLibraries() {
                                    callout_manager_));
 
         // Load the library.  On success, add it to the list of loaded
-        // libraries.  On failure, an error will have been logged and the
-        // library closed.
+        // libraries.  On failure, unload all currently loaded libraries,
+        // leaving the object in the state it was in before loadLibraries was
+        // called.
         if (manager->loadLibrary()) {
             lib_managers_.push_back(manager);
+        } else {
+            static_cast<void>(unloadLibraries());
+            return (false);
         }
     }
 
-    // Update the CalloutManager's idea of the number of libraries it is
-    // handling.
-    callout_manager_->setNumLibraries(lib_managers_.size());
-
-    // Get an indication of whether all libraries loaded successfully.
-    bool status = (library_names_.size() == lib_managers_.size());
-
-    // Don't need the library names any more, so free up the space.
-    library_names_.clear();
-
-    return (status);
+    return (true);
 }
 
 // Unload the libraries.
@@ -110,5 +104,26 @@ LibraryManagerCollection::unloadLibraries() {
     callout_manager_.reset();
 }
 
+// Return number of loaded libraries.
+int
+LibraryManagerCollection::getLoadedLibraryCount() const {
+    return (lib_managers_.size());
+}
+
+// Validate the libraries.
+std::vector<std::string>
+LibraryManagerCollection::validateLibraries(
+                          const std::vector<std::string>& libraries) {
+
+    std::vector<std::string> failures;
+    for (int i = 0; i < libraries.size(); ++i) {
+        if (!LibraryManager::validateLibrary(libraries[i])) {
+            failures.push_back(libraries[i]);
+        }
+    }
+
+    return (failures);
+}
+
 } // namespace hooks
 } // namespace isc

+ 41 - 4
src/lib/hooks/library_manager_collection.h

@@ -69,13 +69,17 @@ class LibraryManager;
 /// code.  However, the link with the CalloutHandle does at least mean that
 /// authors of server code do not need to be so careful about when they destroy
 /// CalloutHandles.
+///
+/// The collection object also provides a utility function to validate a set
+/// of libraries.  The function checks that each library exists, can be opened,
+/// that the "version" function exists and return the right number.
 
 class LibraryManagerCollection {
 public:
     /// @brief Constructor
     ///
-    /// @param List of libraries that this collection will manage.  The order
-    ///        of the libraries is important.
+    /// @param libraries List of libraries that this collection will manage.
+    ///        The order of the libraries is important.
     LibraryManagerCollection(const std::vector<std::string>& libraries)
         : library_names_(libraries)
     {}
@@ -91,8 +95,11 @@ public:
     ///
     /// Loads the libraries.  This creates the LibraryManager associated with
     /// each library and calls its loadLibrary() method.  If a library fails
-    /// to load, the fact is noted but attempts are made to load the remaining
-    /// libraries.
+    /// to load, the loading is abandoned and all libraries loaded so far
+    /// are unloaded.
+    ///
+    /// @return true if all libraries loaded, false if one or more failed t
+    ////        load.
     bool loadLibraries();
 
     /// @brief Get callout manager
@@ -107,6 +114,36 @@ public:
     ///        construction and the time loadLibraries() is called.
     boost::shared_ptr<CalloutManager> getCalloutManager() const;
 
+    /// @brief Get library names
+    ///
+    /// Returns the list of library names.  If called before loadLibraries(),
+    /// the list is the list of names to be loaded; if called afterwards, it
+    /// is the list of libraries that have been loaded.
+    std::vector<std::string> getLibraryNames() const {
+        return (library_names_);
+    }
+
+    /// @brief Get number of loaded libraries
+    ///
+    /// Mainly for testing, this returns the number of libraries that are
+    /// loaded.
+    ///
+    /// @return Number of libraries that are loaded.
+    int getLoadedLibraryCount() const;
+
+    /// @brief Validate libraries
+    ///
+    /// Utility function to validate libraries.  It checks that the libraries
+    /// exist, can be opened, that a "version" function is present in them, and
+    /// that it returns the right number.  All errors are logged.
+    ///
+    /// @param libraries List of libraries to validate
+    ///
+    /// @return Vector of libraries that faled to validate, or an empty vector
+    ///         if all validated.
+    static std::vector<std::string>
+    validateLibraries(const std::vector<std::string>& libraries);
+
 protected:
     /// @brief Unload libraries
     ///

+ 21 - 6
src/lib/hooks/server_hooks.cc

@@ -27,9 +27,18 @@ namespace hooks {
 
 // Constructor - register the pre-defined hooks and check that the indexes
 // assigned to them are as expected.
+//
+// Note that there are no logging messages here or in registerHooks().  The
+// recommended way to initialize hook names is to use static initialization.
+// Here, a static object is declared in a file outside of any function or
+// method.  As a result, it is instantiated and its constructor run before the
+// program starts.  By putting calls to ServerHooks::registerHook() in there,
+// hooks names are already registered when the program runs.  However, at that
+// point, the logging system is not initialized, so messages are unable to
+// be output.
 
 ServerHooks::ServerHooks() {
-    reset();
+    initialize();
 }
 
 // Register a hook.  The index assigned to the hook is the current number
@@ -54,17 +63,14 @@ ServerHooks::registerHook(const string& name) {
     // Element was inserted, so add to the inverse hooks collection.
     inverse_hooks_[index] = name;
 
-    // Log it if debug is enabled
-    LOG_DEBUG(hooks_logger, HOOKS_DBG_TRACE, HOOKS_HOOK_REGISTERED).arg(name);
-
     // ... and return numeric index.
     return (index);
 }
 
-// Reset ServerHooks object to initial state.
+// Set ServerHooks object to initial state.
 
 void
-ServerHooks::reset() {
+ServerHooks::initialize() {
 
     // Clear out the name->index and index->name maps.
     hooks_.clear();
@@ -82,6 +88,15 @@ ServerHooks::reset() {
                   ". context_destroy: expected = " << CONTEXT_DESTROY <<
                   ", actual = " << destroy);
     }
+}
+
+// Reset ServerHooks object to initial state.
+
+void
+ServerHooks::reset() {
+
+    // Clear all hooks then initialize the pre-defined ones.
+    initialize();
 
     // Log a warning - although this is done during testing, it should never be
     // seen in a production system.

+ 13 - 3
src/lib/hooks/server_hooks.h

@@ -74,8 +74,8 @@ public:
     ///
     /// Resets the collection of hooks to the initial state, with just the
     /// context_create and context_destroy hooks set.  This used during
-    /// construction. It is also used during testing to reset the global
-    /// ServerHooks object.
+    /// testing to reset the global ServerHooks object; it should never be
+    /// used in production.
     ///
     /// @throws isc::Unexpected if the registration of the pre-defined hooks
     ///         fails in some way.
@@ -149,7 +149,7 @@ private:
     /// for the packet has completed.  They allow the server code to allocate
     /// and destroy per-packet context.
     ///
-    /// Constructor is declared private to enforce the singleton nature of
+    /// The constructor is declared private to enforce the singleton nature of
     /// the object.  A reference to the singleton is obtainable through the
     /// getServerHooks() static method.
     ///
@@ -157,6 +157,16 @@ private:
     ///         fails in some way.
     ServerHooks();
 
+    /// @brief Initialize hooks
+    ///
+    /// Sets the collection of hooks to the initial state, with just the
+    /// context_create and context_destroy hooks set.  This is used during
+    /// construction.
+    ///
+    /// @throws isc::Unexpected if the registration of the pre-defined hooks
+    ///         fails in some way.
+    void initialize();
+
     /// Useful typedefs.
     typedef std::map<std::string, int> HookCollection;
     typedef std::map<int, std::string> InverseHookCollection;

+ 0 - 4
src/lib/hooks/tests/callout_manager_unittest.cc

@@ -199,10 +199,6 @@ TEST_F(CalloutManagerTest, NumberOfLibraries) {
 
     EXPECT_NO_THROW(cm.reset(new CalloutManager(42)));
     EXPECT_EQ(42, cm->getNumLibraries());
-
-    // Check that setting the number of libraries alterns the number reported.
-    EXPECT_NO_THROW(cm->setNumLibraries(27));
-    EXPECT_EQ(27, cm->getNumLibraries());
 }
 
 // Check that we can only set the current library index to the correct values.

+ 97 - 27
src/lib/hooks/tests/hooks_manager_unittest.cc

@@ -157,33 +157,6 @@ TEST_F(HooksManagerTest, LoadLibrariesWithError) {
     // Load the libraries.  We expect a failure return because one of the
     // libraries fails to load.
     EXPECT_FALSE(HooksManager::loadLibraries(library_names));
-
-    // Execute the callouts.  The first library implements the calculation.
-    //
-    // r3 = (7 * d1 - d2) * d3
-    //
-    // The last-loaded library implements the calculation
-    //
-    // r3 = (10 + d1) * d2 - d3
-    //
-    // Putting the processing for each library together in the appropriate
-    // order, we get:
-    //
-    // r3 = ((10 * d1 + d1) - d2) * d2 * d3 - d3
-    {
-        SCOPED_TRACE("Calculation with libraries loaded");
-        executeCallCallouts(10, 3, 33, 2, 62, 3, 183);
-    }
-
-    // Try unloading the libraries.
-    EXPECT_NO_THROW(HooksManager::unloadLibraries());
-
-    // Re-execute the calculation - callouts can be called but as nothing
-    // happens, the result should always be -1.
-    {
-        SCOPED_TRACE("Calculation with libraries not loaded");
-        executeCallCallouts(-1, 3, -1, 22, -1, 83, -1);
-    }
 }
 
 // Test that we can unload a set of libraries while we have a CalloutHandle
@@ -450,5 +423,102 @@ TEST_F(HooksManagerTest, RegisterHooks) {
     EXPECT_EQ(string("gamma"), names[4]);
 }
 
+// Check that we can get the names of the libraries.
+
+TEST_F(HooksManagerTest, LibraryNames) {
+
+    // Set up the list of libraries to be loaded.
+    std::vector<std::string> library_names;
+    library_names.push_back(std::string(FULL_CALLOUT_LIBRARY));
+    library_names.push_back(std::string(BASIC_CALLOUT_LIBRARY));
+
+    // Check the names before the libraries are loaded.
+    std::vector<std::string> loaded_names = HooksManager::getLibraryNames();
+    EXPECT_TRUE(loaded_names.empty());
+
+    // Load the libraries and check the names again.
+    EXPECT_TRUE(HooksManager::loadLibraries(library_names));
+    loaded_names = HooksManager::getLibraryNames();
+    EXPECT_TRUE(library_names == loaded_names);
+
+    // Unload the libraries and check again.
+    EXPECT_NO_THROW(HooksManager::unloadLibraries());
+    loaded_names = HooksManager::getLibraryNames();
+    EXPECT_TRUE(loaded_names.empty());
+}
+
+// Test the library validation function.
+
+TEST_F(HooksManagerTest, validateLibraries) {
+    // Vector of libraries that failed validation
+    std::vector<std::string> failed;
+
+    // Test different vectors of libraries.
+
+    // No libraries should return a success.
+    std::vector<std::string> libraries;
+
+    failed = HooksManager::validateLibraries(libraries);
+    EXPECT_TRUE(failed.empty());
+
+    // Single valid library should validate.
+    libraries.clear();
+    libraries.push_back(BASIC_CALLOUT_LIBRARY);
+
+    failed = HooksManager::validateLibraries(libraries);
+    EXPECT_TRUE(failed.empty());
+
+    // Multiple valid libraries should succeed.
+    libraries.clear();
+    libraries.push_back(BASIC_CALLOUT_LIBRARY);
+    libraries.push_back(FULL_CALLOUT_LIBRARY);
+    libraries.push_back(UNLOAD_CALLOUT_LIBRARY);
+
+    failed = HooksManager::validateLibraries(libraries);
+    EXPECT_TRUE(failed.empty());
+
+    // Single invalid library should fail.
+    libraries.clear();
+    libraries.push_back(NOT_PRESENT_LIBRARY);
+
+    failed = HooksManager::validateLibraries(libraries);
+    EXPECT_TRUE(failed == libraries);
+
+    // Multiple invalid libraries should fail.
+    libraries.clear();
+    libraries.push_back(INCORRECT_VERSION_LIBRARY);
+    libraries.push_back(NO_VERSION_LIBRARY);
+    libraries.push_back(FRAMEWORK_EXCEPTION_LIBRARY);
+
+    failed = HooksManager::validateLibraries(libraries);
+    EXPECT_TRUE(failed == libraries);
+
+    // Combination of valid and invalid (first one valid) should fail.
+    libraries.clear();
+    libraries.push_back(FULL_CALLOUT_LIBRARY);
+    libraries.push_back(INCORRECT_VERSION_LIBRARY);
+    libraries.push_back(NO_VERSION_LIBRARY);
+
+    std::vector<std::string> expected_failures;
+    expected_failures.push_back(INCORRECT_VERSION_LIBRARY);
+    expected_failures.push_back(NO_VERSION_LIBRARY);
+
+    failed = HooksManager::validateLibraries(libraries);
+    EXPECT_TRUE(failed == expected_failures);
+
+    // Combination of valid and invalid (first one invalid) should fail.
+    libraries.clear();
+    libraries.push_back(NO_VERSION_LIBRARY);
+    libraries.push_back(FULL_CALLOUT_LIBRARY);
+    libraries.push_back(INCORRECT_VERSION_LIBRARY);
+
+    expected_failures.clear();
+    expected_failures.push_back(NO_VERSION_LIBRARY);
+    expected_failures.push_back(INCORRECT_VERSION_LIBRARY);
+
+    failed = HooksManager::validateLibraries(libraries);
+    EXPECT_TRUE(failed == expected_failures);
+}
+
 
 } // Anonymous namespace

+ 105 - 38
src/lib/hooks/tests/library_manager_collection_unittest.cc

@@ -76,8 +76,7 @@ TEST_F(LibraryManagerCollectionTest, LoadLibraries) {
 
     // Load the libraries.
     EXPECT_TRUE(lm_collection.loadLibraries());
-    boost::shared_ptr<CalloutManager> manager =
-                                      lm_collection.getCalloutManager();
+    EXPECT_EQ(2, lm_collection.getLoadedLibraryCount());
 
     // Execute the callouts.  The first library implements the calculation.
     //
@@ -91,6 +90,8 @@ TEST_F(LibraryManagerCollectionTest, LoadLibraries) {
     // order, we get:
     //
     // r3 = ((10 * d1 + d1) - d2) * d2 * d3 - d3
+    boost::shared_ptr<CalloutManager> manager =
+                                      lm_collection.getCalloutManager();
     {
         SCOPED_TRACE("Doing calculation with libraries loaded");
         executeCallCallouts(manager, 10, 3, 33, 2, 62, 3, 183);
@@ -98,6 +99,7 @@ TEST_F(LibraryManagerCollectionTest, LoadLibraries) {
 
     // Try unloading the libraries.
     EXPECT_NO_THROW(lm_collection.unloadLibraries());
+    EXPECT_EQ(0, lm_collection.getLoadedLibraryCount());
 
     // Re-execute the calculation - callouts can be called but as nothing
     // happens, the result should always be -1.
@@ -108,8 +110,7 @@ TEST_F(LibraryManagerCollectionTest, LoadLibraries) {
 }
 
 // This is effectively the same test as above, but with a library generating
-// an error when loaded. It is expected that the failing library will not be
-// loaded, but others will be.
+// an error when loaded. It is expected that no libraries will be loaded.
 
 TEST_F(LibraryManagerCollectionTest, LoadLibrariesWithError) {
 
@@ -126,38 +127,9 @@ TEST_F(LibraryManagerCollectionTest, LoadLibrariesWithError) {
     // Load the libraries.  We expect a failure status to be returned as
     // one of the libraries failed to load.
     EXPECT_FALSE(lm_collection.loadLibraries());
-    boost::shared_ptr<CalloutManager> manager =
-                                      lm_collection.getCalloutManager();
-
-    // Expect only two libraries were loaded.
-    EXPECT_EQ(2, manager->getNumLibraries());
-
-    // Execute the callouts.  The first library implements the calculation.
-    //
-    // r3 = (7 * d1 - d2) * d3
-    //
-    // The last-loaded library implements the calculation
-    //
-    // r3 = (10 + d1) * d2 - d3
-    //
-    // Putting the processing for each library together in the appropriate
-    // order, we get:
-    //
-    // r3 = ((10 * d1 + d1) - d2) * d2 * d3 - d3
-    {
-        SCOPED_TRACE("Doing calculation with libraries loaded");
-        executeCallCallouts(manager, 10, 3, 33, 2, 62, 3, 183);
-    }
-
-    // Try unloading the libraries.
-    EXPECT_NO_THROW(lm_collection.unloadLibraries());
 
-    // Re-execute the calculation - callouts can be called but as nothing
-    // happens, the result should always be -1.
-    {
-        SCOPED_TRACE("Doing calculation with libraries not loaded");
-        executeCallCallouts(manager, -1, 3, -1, 22, -1, 83, -1);
-    }
+    // Expect no libraries were loaded.
+    EXPECT_EQ(0, lm_collection.getLoadedLibraryCount());
 }
 
 // Check that everything works even with no libraries loaded.
@@ -170,15 +142,110 @@ TEST_F(LibraryManagerCollectionTest, NoLibrariesLoaded) {
     // be using.
     LibraryManagerCollection lm_collection(library_names);
     EXPECT_TRUE(lm_collection.loadLibraries());
+    EXPECT_EQ(0, lm_collection.getLoadedLibraryCount());
     boost::shared_ptr<CalloutManager> manager =
                                       lm_collection.getCalloutManager();
 
-    // Load the libraries.
-    EXPECT_TRUE(lm_collection.loadLibraries());
-
     // Eecute the calculation - callouts can be called but as nothing
     // happens, the result should always be -1.
     executeCallCallouts(manager, -1, 3, -1, 22, -1, 83, -1);
 }
 
+// Check that we can get the names of the libraries.
+
+TEST_F(LibraryManagerCollectionTest, LibraryNames) {
+
+    // Set up the list of libraries to be loaded.
+    std::vector<std::string> library_names;
+    library_names.push_back(std::string(FULL_CALLOUT_LIBRARY));
+    library_names.push_back(std::string(BASIC_CALLOUT_LIBRARY));
+
+    // Set up the library manager collection and get the callout manager we'll
+    // be using.
+    PublicLibraryManagerCollection lm_collection(library_names);
+
+    // Check the names before the libraries are loaded.
+    std::vector<std::string> collection_names = lm_collection.getLibraryNames();
+    EXPECT_TRUE(library_names == collection_names);
+
+    // Load the libraries and check the names again.
+    EXPECT_TRUE(lm_collection.loadLibraries());
+    EXPECT_EQ(2, lm_collection.getLoadedLibraryCount());
+    collection_names = lm_collection.getLibraryNames();
+    EXPECT_TRUE(library_names == collection_names);
+}
+
+// Test the library validation function.
+
+TEST_F(LibraryManagerCollectionTest, validateLibraries) {
+    // Vector of libraries that failed validation
+    std::vector<std::string> failed;
+
+    // Test different vectors of libraries.
+
+    // No libraries should return a success.
+    std::vector<std::string> libraries;
+
+    failed = LibraryManagerCollection::validateLibraries(libraries);
+    EXPECT_TRUE(failed.empty());
+
+    // Single valid library should validate.
+    libraries.clear();
+    libraries.push_back(BASIC_CALLOUT_LIBRARY);
+
+    failed = LibraryManagerCollection::validateLibraries(libraries);
+    EXPECT_TRUE(failed.empty());
+
+    // Multiple valid libraries should succeed.
+    libraries.clear();
+    libraries.push_back(BASIC_CALLOUT_LIBRARY);
+    libraries.push_back(FULL_CALLOUT_LIBRARY);
+    libraries.push_back(UNLOAD_CALLOUT_LIBRARY);
+
+    failed = LibraryManagerCollection::validateLibraries(libraries);
+    EXPECT_TRUE(failed.empty());
+
+    // Single invalid library should fail.
+    libraries.clear();
+    libraries.push_back(NOT_PRESENT_LIBRARY);
+
+    failed = LibraryManagerCollection::validateLibraries(libraries);
+    EXPECT_TRUE(failed == libraries);
+
+    // Multiple invalid libraries should fail.
+    libraries.clear();
+    libraries.push_back(INCORRECT_VERSION_LIBRARY);
+    libraries.push_back(NO_VERSION_LIBRARY);
+    libraries.push_back(FRAMEWORK_EXCEPTION_LIBRARY);
+
+    failed = LibraryManagerCollection::validateLibraries(libraries);
+    EXPECT_TRUE(failed == libraries);
+
+    // Combination of valid and invalid (first one valid) should fail.
+    libraries.clear();
+    libraries.push_back(FULL_CALLOUT_LIBRARY);
+    libraries.push_back(INCORRECT_VERSION_LIBRARY);
+    libraries.push_back(NO_VERSION_LIBRARY);
+
+    std::vector<std::string> expected_failures;
+    expected_failures.push_back(INCORRECT_VERSION_LIBRARY);
+    expected_failures.push_back(NO_VERSION_LIBRARY);
+
+    failed = LibraryManagerCollection::validateLibraries(libraries);
+    EXPECT_TRUE(failed == expected_failures);
+
+    // Combination of valid and invalid (first one invalid) should fail.
+    libraries.clear();
+    libraries.push_back(NO_VERSION_LIBRARY);
+    libraries.push_back(FULL_CALLOUT_LIBRARY);
+    libraries.push_back(INCORRECT_VERSION_LIBRARY);
+
+    expected_failures.clear();
+    expected_failures.push_back(NO_VERSION_LIBRARY);
+    expected_failures.push_back(INCORRECT_VERSION_LIBRARY);
+
+    failed = LibraryManagerCollection::validateLibraries(libraries);
+    EXPECT_TRUE(failed == expected_failures);
+}
+
 } // Anonymous namespace

+ 14 - 0
src/lib/hooks/tests/library_manager_unittest.cc

@@ -552,4 +552,18 @@ TEST_F(LibraryManagerTest, LoadMultipleLibraries) {
     EXPECT_TRUE(lib_manager_4.unloadLibrary());
 }
 
+// Check that libraries can be validated.
+
+TEST_F(LibraryManagerTest, validateLibraries) {
+    EXPECT_TRUE(LibraryManager::validateLibrary(BASIC_CALLOUT_LIBRARY));
+    EXPECT_TRUE(LibraryManager::validateLibrary(FULL_CALLOUT_LIBRARY));
+    EXPECT_FALSE(LibraryManager::validateLibrary(FRAMEWORK_EXCEPTION_LIBRARY));
+    EXPECT_FALSE(LibraryManager::validateLibrary(INCORRECT_VERSION_LIBRARY));
+    EXPECT_TRUE(LibraryManager::validateLibrary(LOAD_CALLOUT_LIBRARY));
+    EXPECT_TRUE(LibraryManager::validateLibrary(LOAD_ERROR_CALLOUT_LIBRARY));
+    EXPECT_FALSE(LibraryManager::validateLibrary(NOT_PRESENT_LIBRARY));
+    EXPECT_FALSE(LibraryManager::validateLibrary(NO_VERSION_LIBRARY));
+    EXPECT_TRUE(LibraryManager::validateLibrary(UNLOAD_CALLOUT_LIBRARY));
+}
+
 } // Anonymous namespace

+ 9 - 0
src/lib/util/tests/memory_segment_mapped_unittest.cc

@@ -237,6 +237,15 @@ TEST_F(MemorySegmentMappedTest, allocate) {
 }
 
 TEST_F(MemorySegmentMappedTest, badAllocate) {
+    // If the test is run as the root user, the following allocate()
+    // call will result in a successful MemorySegmentGrown exception,
+    // instead of an abort (due to insufficient permissions during
+    // reopen).
+    if (getuid() == 0) {
+        std::cerr << "Skipping test as it's run as the root user" << std::endl;
+        return;
+    }
+
     // Make the mapped file non-writable; managed_mapped_file::grow() will
     // fail, resulting in abort.
     const int ret = chmod(mapped_file, 0444);