Browse Source

[master] Merge branch 'master' into trac3336

Marcin Siodelski 11 years ago
parent
commit
7322535564
66 changed files with 2987 additions and 784 deletions
  1. 21 1
      ChangeLog
  2. 2 0
      doc/devel/mainpage.dox
  3. 38 0
      doc/examples/kea4/several-subnets.json
  4. 34 0
      doc/examples/kea4/single-subnet.json
  5. 75 34
      doc/guide/bind10-guide.xml
  6. 73 16
      src/bin/d2/d2_config.cc
  7. 103 24
      src/bin/d2/d2_config.h
  8. 19 5
      src/bin/d2/d2_update_message.cc
  9. 35 11
      src/bin/d2/d2_update_message.h
  10. 1 1
      src/bin/d2/dhcp-ddns.spec
  11. 37 34
      src/bin/d2/dns_client.cc
  12. 4 30
      src/bin/d2/dns_client.h
  13. 12 5
      src/bin/d2/nc_trans.cc
  14. 9 4
      src/bin/d2/nc_trans.h
  15. 122 71
      src/bin/d2/tests/d2_cfg_mgr_unittests.cc
  16. 3 3
      src/bin/d2/tests/d2_process_unittests.cc
  17. 125 11
      src/bin/d2/tests/d2_update_message_unittests.cc
  18. 2 2
      src/bin/d2/tests/d_test_stubs.cc
  19. 160 26
      src/bin/d2/tests/dns_client_unittests.cc
  20. 3 5
      src/bin/d2/tests/nc_add_unittests.cc
  21. 3 5
      src/bin/d2/tests/nc_remove_unittests.cc
  22. 95 14
      src/bin/d2/tests/nc_test_utils.cc
  23. 89 7
      src/bin/d2/tests/nc_test_utils.h
  24. 168 9
      src/bin/d2/tests/nc_trans_unittests.cc
  25. 9 1
      src/bin/dhcp4/Makefile.am
  26. 219 0
      src/bin/dhcp4/bundy_controller.cc
  27. 112 213
      src/bin/dhcp4/ctrl_dhcp4_srv.cc
  28. 94 65
      src/bin/dhcp4/ctrl_dhcp4_srv.h
  29. 40 0
      src/bin/dhcp4/dhcp4.dox
  30. 5 7
      src/bin/dhcp4/dhcp4_messages.mes
  31. 3 2
      src/bin/dhcp4/dhcp4_srv.h
  32. 1 1
      src/bin/dhcp4/config_parser.cc
  33. 0 0
      src/bin/dhcp4/json_config_parser.h
  34. 124 0
      src/bin/dhcp4/kea_controller.cc
  35. 40 33
      src/bin/dhcp4/main.cc
  36. 17 1
      src/bin/dhcp4/tests/Makefile.am
  37. 30 0
      src/bin/dhcp4/tests/bundy_controller_unittest.cc
  38. 1 1
      src/bin/dhcp4/tests/config_parser_unittest.cc
  39. 5 0
      src/bin/dhcp4/tests/configs-list.txt
  40. 12 4
      src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc
  41. 1 1
      src/bin/dhcp4/tests/d2_unittest.cc
  42. 1 1
      src/bin/dhcp4/tests/dhcp4_srv_unittest.cc
  43. 1 18
      src/bin/dhcp4/tests/dhcp4_test.py
  44. 1 1
      src/bin/dhcp4/tests/dhcp4_test_utils.cc
  45. 1 1
      src/bin/dhcp4/tests/direct_client_unittest.cc
  46. 296 0
      src/bin/dhcp4/tests/kea_controller_unittest.cc
  47. 6 3
      src/bin/dhcp6/bundy_controller.cc
  48. 6 3
      src/bin/dhcp6/ctrl_dhcp6_srv.cc
  49. 14 10
      src/bin/dhcp6/ctrl_dhcp6_srv.h
  50. 43 10
      src/bin/dhcp6/dhcp6.dox
  51. 13 1
      src/bin/dhcp6/dhcp6_messages.mes
  52. 75 32
      src/bin/dhcp6/kea_controller.cc
  53. 20 28
      src/bin/dhcp6/main.cc
  54. 16 1
      src/bin/dhcp6/tests/Makefile.am
  55. 4 0
      src/bin/dhcp6/tests/bundy_controller_unittest.cc
  56. 164 0
      src/bin/dhcp6/tests/dhcp6_reconfigure_test.sh
  57. 110 0
      src/bin/dhcp6/tests/dhcp6_shutdown_test.sh
  58. 16 0
      src/bin/dhcp6/tests/dhcp6_sigint_test.sh
  59. 16 0
      src/bin/dhcp6/tests/dhcp6_sigterm_test.sh
  60. 0 17
      src/bin/dhcp6/tests/dhcp6_test.py
  61. 2 2
      src/lib/cc/data.cc
  62. 5 1
      src/lib/dhcpsrv/daemon.cc
  63. 31 7
      src/lib/dhcpsrv/daemon.h
  64. 5 0
      src/lib/dns/tsig.h
  65. 4 1
      src/lib/testutils/Makefile.am
  66. 191 0
      src/lib/testutils/dhcp_test_lib.sh

+ 21 - 1
ChangeLog

@@ -1,3 +1,23 @@
+788.	[func]		tomek
+	DHCPv4 server: New parameter added to configure.ac: --with-kea-config.
+	It allows selecting configuration backend and accepts one of two
+	values: BUNDY, which uses Bundy (former BIND10) framework as Kea
+	0.8 did, or JSON, which reads configuration from a JSON file.
+	(Trac #3399, git 6e4dd3ae58c091ba0fd64c87fa8d7c268210f99b)
+
+787.	[func]		marcin
+	DHCPv6 server: Implemented dynamic reconfiguration of the server,
+	triggered when the SIGHUP signal is received by the server's
+	process. Also, server performs a graceful shut down when SIGINT
+	or SIGTERM signal is received.
+	(Trac #3406, git 3be60fa6ac521aecae6ae92d26dc03792bc76903)
+
+786.	[func]		tmark
+	DHCP-DDNS now supports DDNS updates with TSIG.  Please refer to the
+	Kea Guide for details. Prior to this TSIG keys could be defined but
+	were not used.
+	(Trac #3432, git 80fea12a53d1e832d4e7b710ca6ea613300f73ea)
+
 785.	[bug]		marcin
 	DHCPv6 server avoids collisions between prefixes that are allocated
 	as a result of receiving hints from the clients. Previously the
@@ -18,7 +38,7 @@
 	(Trac #3268,    git bd60252e679f19b062f61926647f661ab169f21c)
 
 783.	[func]*		tomek
-	b10-dhcp6: New parameter added to configure: --with-kea-config.
+	DHCPv6 server: New parameter added to configure: --with-kea-config.
 	It allows selecting configuration backend and accepts one of two
 	values: BUNDY, which uses Bundy (former BIND10 framework as Kea
 	0.8 did, or JSON, which reads configuration from a JSON file.

+ 2 - 0
doc/devel/mainpage.dox

@@ -55,6 +55,7 @@
  *   - @subpage dhcpv4OptionsParse
  *   - @subpage dhcpv4DDNSIntegration
  *   - @subpage dhcpv4Classifier
+ *   - @subpage dhcpv4ConfigBackend
  *   - @subpage dhcpv4Other
  * - @subpage dhcp6
  *   - @subpage dhcpv6Session
@@ -64,6 +65,7 @@
  *   - @subpage dhcpv6OptionsParse
  *   - @subpage dhcpv6Classifier
  *   - @subpage dhcpv6ConfigBackend
+ *   - @subpage dhcpv6SignalBasedReconfiguration
  *   - @subpage dhcpv6Other
  * - @subpage d2
  *   - @subpage d2CPL

+ 38 - 0
doc/examples/kea4/several-subnets.json

@@ -0,0 +1,38 @@
+# This is an example configuration file for DHCPv4 server in Kea.
+# It's a basic scenario with three IPv4 subnets configured. In each
+# subnet, there's a smaller pool of dynamic addresses.
+
+{ "Dhcp4":
+
+{ 
+# Kea is told to listen on eth0 interface only.
+  "interfaces": [ "eth0" ],
+
+# We need to specify lease type. As of May 2014, three backends are supported:
+# memfile, mysql and pgsql. We'll just use memfile, because it doesn't require
+# any prior set up.
+  "lease-database": {
+    "type": "memfile"
+  },
+
+# Addresses will be assigned with the valid lifetimes being 4000.
+# Client is told to start renewing after 1000 seconds. If the server
+# does not repond after 2000 seconds since the lease was granted, client
+# is supposed to start REBIND procedure (emergency renewal that allows
+# switching to a different server).
+  "valid-lifetime": 4000,
+  "renew-timer": 1000,
+  "rebind-timer": 2000,
+
+# The following list defines subnets. Each subnet consists of at
+# least subnet and pool entries.
+  "subnet4": [ 
+  {    "pool": [ "192.0.2.1 - 192.0.2.200" ],
+       "subnet": "192.0.2.0/24"  },
+  {    "pool": [ "192.0.3.100 - 192.0.3.200" ],
+       "subnet": "192.0.3.0/24"  },
+  {    "pool": [ "192.0.4.1 - 192.0.4.254" ],
+       "subnet": "192.0.4.0/24"  } ]
+}
+
+}

+ 34 - 0
doc/examples/kea4/single-subnet.json

@@ -0,0 +1,34 @@
+# This is an example configuration file for the DHCPv4 server in Kea.
+# It is a basic scenario with one IPv4 subnet configured. The subnet
+# contains a single pool of dynamically allocated addresses.
+
+{ "Dhcp4":
+
+{ 
+# Kea is told to listen on eth0 interface only.
+  "interfaces": [ "eth0" ],
+
+# We need to specify lease type. As of May 2014, three backends are supported:
+# memfile, mysql and pgsql. We'll just use memfile, because it doesn't require
+# any prior set up.
+  "lease-database": {
+    "type": "memfile"
+  },
+
+# Addresses will be assigned with valid lifetimes being 4000. Client
+# is told to start renewing after 1000 seconds. If the server does not respond
+# after 2000 seconds since the lease was granted, client is supposed
+# to start REBIND procedure (emergency renewal that allows switching
+# to a different server).
+  "valid-lifetime": 4000,
+  "renew-timer": 1000,
+  "rebind-timer": 2000,
+
+# The following list defines subnets. We have only one subnet
+# here.
+  "subnet4": [ 
+  {    "pool": [ "192.0.2.1 - 192.0.2.200" ],
+       "subnet": "192.0.2.0/24"  } ]
+}
+
+}

+ 75 - 34
doc/guide/bind10-guide.xml

@@ -5372,75 +5372,118 @@ corresponding values in the DHCP servers' "dhcp-ddns" configuration section.
 
       <section id="d2-tsig-key-list-config">
         <title>TSIG Key List</title>
-<note>
-<simpara>
-        While this section may be displayed and edited using bindctl, the use
-        of TSIG in actual communications between D2 and DNS servers is not yet
-        implemented.
-</simpara>
-</note>
         <para>
-        DDNS protocol can be conducted with or without TSIG as defined in
-        RFC 2845. This configuration section allows the administrator to
-        define the dictionary of TSIG keys to may be used.  To use TSIG
-        when working with a specific DDNS Domain that key must be defined in
-        the TSIG Key List and referenced by name in that domain's entry in
-        the DDNS catalog.
-        </para>
+        A DDNS protocol exchange can be conducted with or without TSIG
+        (defined in <ulink url="http://tools.ietf/org/html/rfc2845">RFC
+        2845</ulink>). This configuration section allows the administrator
+        to define the set of TSIG keys that may be used in such
+        exchanges.</para>
+
+        <para>To use TSIG when updating entries in a DNS Domain,
+        a key must be defined in the TSIG Key List and referenced by
+        name in that domain's configuration entry.  When D2 matches a
+        change request to a domain, it checks whether the domain has
+        a TSIG key associated with it.  If so, D2 will use that key to
+        sign DNS update messages sent to and verify repsonses received
+        from the domain's DNS server(s). For each TSIG key required by
+        the DNS servers that D2 will be working with there must be a
+        corresponding TSIG key in the TSIG Key list.</para>
+
         <para>
-        As one might gather from its name, this section is a list of
-        TSIG keys. Each key has three parameters:
+        As one might gather from the name, the tsig_key section of the
+        D2 configuration lists the TSIG keys.  Each entry describes a
+        TSIG key used by one or more DNS servers to authenticate requests
+        and sign responses.  Every entry in the list has three parameters:
         <itemizedlist>
           <listitem>
             <simpara>
               <command>name</command> &mdash;
-              is a unique text label used to identify the this key within the
-              list.  It is this value that is used to specify which key (if any)
-              should be used with a specific DNS server. So long as it is
-              unique, its content is arbitrary.  It cannot be blank.
+              a unique text label used to identify this key within the
+              list.  This value is used to specify which key (if any) should be
+              used when updating a specific domain. So long as it is unique its
+              content is arbitrary, although for clarity and ease of maintenance
+              it is recommended that it match the name used on the DNS server(s).
+              It cannot be blank.
             </simpara>
           </listitem>
           <listitem>
             <simpara>
               <command>algorithm</command> &mdash;
               specifies which hashing algorithm should be used with this
-              key.  This value is not currently used.
+              key.  This value must specify the same algorithm used for the
+              key on the DNS server(s). The supported algorithms are listed below:
+              <itemizedlist>
+                <listitem>
+                   <command>HMAC-MD5</command>
+                </listitem>
+                <listitem>
+                    <command>HMAC-SHA1</command>
+                </listitem>
+                <listitem>
+                  <command>HMAC-SHA224</command>
+              </listitem>
+              <listitem>
+                  <command>HMAC-SHA256</command>
+              </listitem>
+              <listitem>
+                  <command>HMAC-SHA384</command>
+                  </listitem>
+              <listitem>
+                  <command>HMAC-SHA512</command>
+              </listitem>
+              </itemizedlist>
+              This value is not case sensitive.
             </simpara>
           </listitem>
           <listitem>
             <simpara>
               <command>secret</command> &mdash;
-              is used to specify the shared secret key code for this key. This
-              value is not currently used.
+              is used to specify the shared secret key code for this key.  This value is
+              case sensitive and must exactly match the value specified on the DNS server(s).
+              It is a base64-encoded text value.
             </simpara>
           </listitem>
         </itemizedlist>
         </para>
         <para>
+        As an example, suppose that a domain D2 will be updating is
+        maintained by a BIND9 DNS server which requires dynamic updates
+        to be secured with TSIG.  Suppose further that the entry for
+        the TSIG key in BIND9's named.conf file looks like this:
+<screen>
+   :
+   key "key.four.example.com." {
+       algorithm hmac-sha224;
+       secret "bZEG7Ow8OgAUPfLWV3aAUQ==";
+   };
+   :
+</screen>
         By default, the TSIG Key list is empty:
 <screen>
 <userinput>> config show DhcpDdns/tsig_keys</userinput>
 DhcpDdns/tsig_keys  []  list  (default)
 </screen>
-        To create a new key in the list, one must first add a new key element:
+        We must first create a new key in the list:
 <screen>
 <userinput>> config add DhcpDdns/tsig_keys</userinput>
 </screen>
-        Displaying the new element, reveals this:
+        Displaying the new element, reveals:
 <screen>
 <userinput>> config show DhcpDdns/tsig_keys[0]</userinput>
 DhcpDdns/tsig_keys[0]/name  ""  string  (default)
-DhcpDdns/tsig_keys[0]/algorithm "hmac_md5"  string  (modified)
+DhcpDdns/tsig_keys[0]/algorithm "HMAC-MD5"  string  (modified)
 DhcpDdns/tsig_keys[0]/secret  ""  string  (default)
 </screen>
-        Populating the key name and secret, while accepting the default value
-        for alogorithm:
+        Now set all three values to match BIND9's key:
 <screen>
-<userinput>> config set DhcpDdns/tsig_keys[0]/name "key1.example.com"</userinput>
-<userinput>> config set DhcpDdns/tsig_keys[0]/secret "123456789"</userinput>
+<userinput>> config set DhcpDdns/tsig_keys[0]/name "key.four.example.com"</userinput>
+<userinput>> config set DhcpDdns/tsig_keys[0]/algorithm "HMAC-SHA224"</userinput>
+<userinput>> config set DhcpDdns/tsig_keys[0]/secret "bZEG7Ow8OgAUPfLWV3aAUQ=="</userinput>
 <userinput>> config commit</userinput>
 </screen>
         </para>
+        These steps would be repeated for each TSIG key needed.  Note that the same TSIG key
+        can be used with more than one domain.
       </section> <!-- "d2-tsig-key-list-config" -->
 
       <section id="d2-forward-ddns-config">
@@ -5458,8 +5501,6 @@ DhcpDdns/forward_ddns/ddns_domains  [] list  (default)
         </para>
         <section id="add-forward-ddns-domain">
           <title>Adding Forward DDNS Domains</title>
-
-
           <para>
           A forward DDNS Domain maps a forward DNS zone to a set of DNS servers
           which maintain the forward DNS data for that zone.  You will need one
@@ -5638,8 +5679,8 @@ DhcpDdns/reverse_ddns/ddns_domains  [] list  (default)
               <simpara>
               <command>key_name</command> &mdash;
               If TSIG should be used with this domain's servers, then this
-              value should be the name of the key from within the TSIG Key List
-              to use.  If the value is blank (the default), TSIG will not be
+              value should be the name of that key from the TSIG Key List.
+              If the value is blank (the default), TSIG will not be
               used in DDNS conversations with this domain's servers.  Currently
               this value is not used as TSIG has not been implemented.
               </simpara>

+ 73 - 16
src/bin/d2/d2_config.cc

@@ -124,15 +124,59 @@ operator<<(std::ostream& os, const D2Params& config) {
 }
 
 // *********************** TSIGKeyInfo  *************************
+// Note these values match correpsonding values for Bind9's
+// dnssec-keygen
+const char* TSIGKeyInfo::HMAC_MD5_STR = "HMAC-MD5";
+const char* TSIGKeyInfo::HMAC_SHA1_STR = "HMAC-SHA1";
+const char* TSIGKeyInfo::HMAC_SHA224_STR = "HMAC-SHA224";
+const char* TSIGKeyInfo::HMAC_SHA256_STR = "HMAC-SHA256";
+const char* TSIGKeyInfo::HMAC_SHA384_STR = "HMAC-SHA384";
+const char* TSIGKeyInfo::HMAC_SHA512_STR = "HMAC-SHA512";
 
 TSIGKeyInfo::TSIGKeyInfo(const std::string& name, const std::string& algorithm,
                          const std::string& secret)
-    :name_(name), algorithm_(algorithm), secret_(secret) {
+    :name_(name), algorithm_(algorithm), secret_(secret), tsig_key_() {
+    remakeKey();
 }
 
 TSIGKeyInfo::~TSIGKeyInfo() {
 }
 
+const dns::Name&
+TSIGKeyInfo::stringToAlgorithmName(const std::string& algorithm_id) {
+    if (boost::iequals(algorithm_id, HMAC_MD5_STR)) {
+        return (dns::TSIGKey::HMACMD5_NAME());
+    } else if (boost::iequals(algorithm_id, HMAC_SHA1_STR)) {
+        return (dns::TSIGKey::HMACSHA1_NAME());
+    } else if (boost::iequals(algorithm_id, HMAC_SHA224_STR)) {
+        return (dns::TSIGKey::HMACSHA224_NAME());
+    } else if (boost::iequals(algorithm_id, HMAC_SHA256_STR)) {
+        return (dns::TSIGKey::HMACSHA256_NAME());
+    } else if (boost::iequals(algorithm_id, HMAC_SHA384_STR)) {
+        return (dns::TSIGKey::HMACSHA384_NAME());
+    } else if (boost::iequals(algorithm_id, HMAC_SHA512_STR)) {
+        return (dns::TSIGKey::HMACSHA512_NAME());
+    }
+
+    isc_throw(BadValue, "Unknown TSIG Key algorithm: " << algorithm_id);
+}
+
+void
+TSIGKeyInfo::remakeKey() {
+    try {
+        // Since our secret value is base64 encoded already, we need to
+        // build the input string for the appropriate TSIGKey constructor.
+        // If secret isn't a valid base64 value, the constructor will throw.
+        std::ostringstream stream;
+        stream << dns::Name(name_).toText() << ":"
+               << secret_ << ":"
+               << stringToAlgorithmName(algorithm_);
+
+        tsig_key_.reset(new dns::TSIGKey(stream.str()));
+    } catch (const std::exception& ex) {
+        isc_throw(D2CfgError, "Cannot make TSIGKey: " << ex.what());
+    }
+}
 
 // *********************** DnsServerInfo  *************************
 
@@ -164,14 +208,25 @@ operator<<(std::ostream& os, const DnsServerInfo& server) {
 
 // *********************** DdnsDomain  *************************
 
-DdnsDomain::DdnsDomain(const std::string& name, const std::string& key_name,
-                       DnsServerInfoStoragePtr servers)
-    : name_(name), key_name_(key_name), servers_(servers) {
+DdnsDomain::DdnsDomain(const std::string& name,
+                       DnsServerInfoStoragePtr servers,
+                       const TSIGKeyInfoPtr& tsig_key_info)
+    : name_(name), servers_(servers),
+      tsig_key_info_(tsig_key_info) {
 }
 
 DdnsDomain::~DdnsDomain() {
 }
 
+const std::string
+DdnsDomain::getKeyName() const {
+    if (tsig_key_info_) {
+        return (tsig_key_info_->getName());
+    }
+
+    return ("");
+}
+
 // *********************** DdnsDomainLstMgr  *************************
 
 const char* DdnsDomainListMgr::wildcard_domain_name_ = "*";
@@ -308,9 +363,6 @@ TSIGKeyInfoParser::build(isc::data::ConstElementPtr key_config) {
     local_scalars_.getParam("algorithm", algorithm);
     local_scalars_.getParam("secret", secret);
 
-    // @todo Validation here is very superficial. This will expand as TSIG
-    // Key use is more fully implemented.
-
     // Name cannot be blank.
     if (name.empty()) {
         isc_throw(D2CfgError, "TSIG Key Info must specify name");
@@ -361,9 +413,6 @@ TSIGKeyInfoParser::createConfigParser(const std::string& config_id) {
 
 void
 TSIGKeyInfoParser::commit() {
-    /// @todo if at some point  TSIG keys need some form of runtime resource
-    /// initialization, such as creating some sort of hash instance in
-    /// crytpolib.  Once TSIG is fully implemented under Trac #3432 we'll know.
 }
 
 // *********************** TSIGKeyInfoListParser  *************************
@@ -606,18 +655,26 @@ DdnsDomainParser::build(isc::data::ConstElementPtr domain_config) {
         isc_throw(D2CfgError, "Duplicate domain specified:" << name);
     }
 
-    // Key name is optional. If it is not blank, then validate it against
-    // the defined list of keys.
+    // Key name is optional. If it is not blank, then find the key in the
+    /// list of defined keys.
+    TSIGKeyInfoPtr tsig_key_info;
     local_scalars_.getParam("key_name", key_name, DCfgContextBase::OPTIONAL);
     if (!key_name.empty()) {
-        if ((!keys_) || (keys_->find(key_name) == keys_->end())) {
-            isc_throw(D2CfgError, "DdnsDomain :" << name <<
-                     " specifies and undefined key:" << key_name);
+        if (keys_) {
+            TSIGKeyInfoMap::iterator kit = keys_->find(key_name);
+            if (kit != keys_->end()) {
+                tsig_key_info = kit->second;
+            }
+        }
+
+        if (!tsig_key_info) {
+            isc_throw(D2CfgError, "DdnsDomain " << name <<
+                     " specifies an undefined key: " << key_name);
         }
     }
 
     // Instantiate the new domain and add it to domain storage.
-    DdnsDomainPtr domain(new DdnsDomain(name, key_name, local_servers_));
+    DdnsDomainPtr domain(new DdnsDomain(name, local_servers_, tsig_key_info));
 
     // Add the new domain to the domain storage.
     (*domains_)[name] = domain;

+ 103 - 24
src/bin/d2/d2_config.h

@@ -19,6 +19,7 @@
 #include <d2/d2_asio.h>
 #include <d2/d_cfg_mgr.h>
 #include <dhcpsrv/dhcp_parsers.h>
+#include <dns/tsig.h>
 #include <exceptions/exceptions.h>
 
 #include <boost/foreach.hpp>
@@ -264,22 +265,53 @@ typedef boost::shared_ptr<D2Params> D2ParamsPtr;
 
 /// @brief Represents a TSIG Key.
 ///
-/// Currently, this is simple storage class containing the basic attributes of
-/// a TSIG Key.  It is intended primarily as a reference for working with
-/// actual keys and may eventually be replaced by isc::dns::TSIGKey.  TSIG Key
-/// functionality at this stage is strictly limited to configuration parsing.
-/// @todo full functionality for using TSIG during DNS updates will be added
-/// in a future release.
+/// Acts as both a storage class containing the basic attributes which
+/// describe a TSIG Key, as well as owning and providing access to an
+/// instance of the actual key (@ref isc::dns::TSIGKey) that can be used
+/// by the IO layer for signing and verifying messages.
+///
 class TSIGKeyInfo {
 public:
+    /// @brief Defines string values for the supported TSIG algorithms
+    //@{
+    static const char* HMAC_MD5_STR;
+    static const char* HMAC_SHA1_STR;
+    static const char* HMAC_SHA256_STR;
+    static const char* HMAC_SHA224_STR;
+    static const char* HMAC_SHA384_STR;
+    static const char* HMAC_SHA512_STR;
+    //}@
 
     /// @brief Constructor
     ///
     /// @param name the unique label used to identify this key
-    /// @param algorithm the name of the encryption alogirthm this key uses.
-    /// (@todo This will be a fixed list of choices)
-    ///
-    /// @param secret the secret component of this key
+    /// @param algorithm the id of the encryption alogirthm this key uses.
+    /// Currently supported values are (case insensitive):
+    /// -# "HMAC-MD5"
+    /// -# "HMAC-SHA1"
+    /// -# "HMAC-SHA224"
+    /// -# "HMAC-SHA256"
+    /// -# "HMAC-SHA384"
+    /// -# "HMAC-SHA512"
+    ///
+    /// @param secret  The base-64 encoded secret component for this key.
+    /// (A suitable string for use here could be obtained by running the
+    /// BIND 9 dnssec-keygen program; the contents of resulting key file
+    /// will look similar to:
+    /// @code
+    ///   Private-key-format: v1.3
+    ///   Algorithm: 157 (HMAC_MD5)
+    ///   Key: LSWXnfkKZjdPJI5QxlpnfQ==
+    ///   Bits: AAA=
+    ///   Created: 20140515143700
+    ///   Publish: 20140515143700
+    ///   Activate: 20140515143700
+    /// @endcode
+    /// where the value the "Key:" entry is the secret component of the key.)
+    ///
+    /// @throw D2CfgError if values supplied are invalid:
+    /// name cannot be blank, algorithm must be a supported value,
+    /// secret must be a non-blank, base64 encoded string.
     TSIGKeyInfo(const std::string& name, const std::string& algorithm,
                 const std::string& secret);
 
@@ -293,7 +325,7 @@ public:
         return (name_);
     }
 
-    /// @brief Getter which returns the key's algorithm.
+    /// @brief Getter which returns the key's algorithm string ID
     ///
     /// @return returns the algorithm as as std::string.
     const std::string getAlgorithm() const {
@@ -307,18 +339,55 @@ public:
         return (secret_);
     }
 
+    /// @brief Getter which returns the TSIG key used to sign and verify
+    /// messages
+    ///
+    /// @return const pointer reference to dns::TSIGKey.
+    const dns::TSIGKeyPtr& getTSIGKey() const {
+        return (tsig_key_);
+    }
+
+    /// @brief Converts algorithm id to dns::TSIGKey algorithm dns::Name
+    ///
+    /// @param algorithm_id string value to translate into an algorithm name.
+    /// Currently supported values are (case insensitive):
+    /// -# "HMAC-MD5"
+    /// -# "HMAC-SHA1"
+    /// -# "HMAC-SHA224"
+    /// -# "HMAC-SHA256"
+    /// -# "HMAC-SHA384"
+    /// -# "HMAC-SHA512"
+    ///
+    /// @return const reference to a dns::Name containing the algorithm name
+    /// @throw BadValue if ID isn't recognized.
+    static const dns::Name& stringToAlgorithmName(const std::string&
+                                                  algorithm_id);
+
 private:
+    /// @brief Creates the actual TSIG key instance member
+    ///
+    /// Replaces this tsig_key member with a key newly created using the key
+    /// name, algorithm id, and secret.
+    /// This method is currently only called by the constructor, however it
+    /// could be called post-construction should keys ever support expiration.
+    ///
+    /// @throw D2CfgError with an explanation if the key could not be created.
+    void remakeKey();
+
     /// @brief The name of the key.
     ///
     /// This value is the unique identifier that domains use to
     /// to specify which TSIG key they need.
     std::string name_;
 
-    /// @brief The algorithm that should be used for this key.
+    /// @brief The string ID of the algorithm that should be used for this key.
     std::string algorithm_;
 
-    /// @brief The secret value component of this key.
+    /// @brief The base64 encoded string secret value component of this key.
     std::string secret_;
+
+    /// @brief The actual TSIG key.
+    dns::TSIGKeyPtr tsig_key_;
 };
 
 /// @brief Defines a pointer for TSIGKeyInfo instances.
@@ -454,10 +523,12 @@ public:
     /// @brief Constructor
     ///
     /// @param name is the domain name of the domain.
-    /// @param key_name is the TSIG key name for use with this domain.
     /// @param servers is the list of server(s) supporting this domain.
-    DdnsDomain(const std::string& name, const std::string& key_name,
-               DnsServerInfoStoragePtr servers);
+    /// @param tsig_key_info pointer to the TSIGKeyInfo for the dommain's key
+    /// It defaults to an empty pointer, signifying the domain has no key.
+    DdnsDomain(const std::string& name,
+               DnsServerInfoStoragePtr servers,
+               const TSIGKeyInfoPtr& tsig_key_info = TSIGKeyInfoPtr());
 
     /// @brief Destructor
     virtual ~DdnsDomain();
@@ -469,12 +540,11 @@ public:
         return (name_);
     }
 
-    /// @brief Getter which returns the domain's TSIG key name.
+    /// @brief Convenience method which returns the domain's TSIG key name.
     ///
-    /// @return returns the key name in an std::string.
-    const std::string getKeyName() const {
-        return (key_name_);
-    }
+    /// @return returns the key name in an std::string. If domain has no
+    /// TSIG key, the string will empty.
+    const std::string getKeyName() const;
 
     /// @brief Getter which returns the domain's list of servers.
     ///
@@ -483,15 +553,24 @@ public:
         return (servers_);
     }
 
+    /// @brief Getter which returns the domain's TSIGKey info
+    ///
+    /// @return returns the pointer to the server storage.  If the domain
+    /// is not configured to use TSIG the pointer will be empty.
+    const TSIGKeyInfoPtr& getTSIGKeyInfo() {
+        return (tsig_key_info_);
+    }
+
 private:
     /// @brief The domain name of the domain.
     std::string name_;
 
-    /// @brief The name of the TSIG key for use with this domain.
-    std::string key_name_;
-
     /// @brief The list of server(s) supporting this domain.
     DnsServerInfoStoragePtr servers_;
+
+    /// @brief Pointer to domain's the TSIGKeyInfo.
+    /// Value is empty if the domain is not configured for TSIG.
+    TSIGKeyInfoPtr tsig_key_info_;
 };
 
 /// @brief Defines a pointer for DdnsDomain instances.

+ 19 - 5
src/bin/d2/d2_update_message.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 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
@@ -108,7 +108,8 @@ D2UpdateMessage::addRRset(const UpdateMsgSection section,
 }
 
 void
-D2UpdateMessage::toWire(AbstractMessageRenderer& renderer) {
+D2UpdateMessage::toWire(AbstractMessageRenderer& renderer,
+                        TSIGContext* const tsig_context) {
     // We are preparing the wire format of the message, meaning
     // that this message will be sent as a request to the DNS.
     // Therefore, we expect that this message is a REQUEST.
@@ -122,16 +123,29 @@ D2UpdateMessage::toWire(AbstractMessageRenderer& renderer) {
         isc_throw(InvalidZoneSection, "Zone section of the DNS Update message"
                   " must comprise exactly one record (RFC2136, section 2.3)");
     }
-    message_.toWire(renderer);
+    message_.toWire(renderer, tsig_context);
 }
 
 void
-D2UpdateMessage::fromWire(isc::util::InputBuffer& buffer) {
+D2UpdateMessage::fromWire(const void* received_data, size_t bytes_received,
+                          dns::TSIGContext* const tsig_context) {
     // First, use the underlying dns::Message implementation to get the
     // contents of the DNS response message. Note that it may or may
     // not be the message that we are interested in, but needs to be
     // parsed so as we can check its ID, Opcode etc.
-    message_.fromWire(buffer);
+    isc::util::InputBuffer received_data_buffer(received_data, bytes_received);
+    message_.fromWire(received_data_buffer);
+
+    // If tsig_contex is not NULL, then we need to verify the message.
+    if (tsig_context) {
+        TSIGError error = tsig_context->verify(message_.getTSIGRecord(),
+                                               received_data, bytes_received);
+        if (error != TSIGError::NOERROR()) {
+            isc_throw(TSIGVerifyError, "TSIG verification failed: "
+                      << error.toText());
+        }
+    }
+
     // This class exposes the getZone() function. This function will return
     // pointer to the D2Zone object if non-empty Zone section exists in the
     // received message. It will return NULL pointer if it doesn't exist.

+ 35 - 11
src/bin/d2/d2_update_message.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 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
@@ -21,6 +21,7 @@
 #include <dns/rcode.h>
 #include <dns/rrclass.h>
 #include <dns/rrset.h>
+#include <dns/tsig.h>
 
 #include <map>
 
@@ -63,6 +64,17 @@ public:
         isc::Exception(file, line, what) {}
 };
 
+/// @brief Exception indicating that a signed, inbound message failed to verfiy
+///
+/// This exception is thrown when TSIG verification of a DNS server's response
+/// fails.
+class TSIGVerifyError : public Exception {
+public:
+    TSIGVerifyError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) {}
+};
+
+
 class D2UpdateMessage;
 
 /// @brief Pointer to the DNS Update Message.
@@ -250,6 +262,9 @@ public:
     /// RRs respectively. The ZOCOUNT must be equal to 1 because RFC2136
     /// requires that the message comprises exactly one Zone record.
     ///
+    /// If given a TSIG context, this method will pass the context down into
+    /// dns::Message.toWire() method which signs the message using the context.
+    ///
     /// This function does not guarantee exception safety. However, exceptions
     /// should be rare because @c D2UpdateMessage class API prevents invalid
     /// use of the class. The typical case, when this function may throw an
@@ -260,18 +275,23 @@ public:
     ///
     /// @param renderer A renderer object used to generate the message wire
     /// format.
-    void toWire(dns::AbstractMessageRenderer& renderer);
+    /// @param tsig_ctx A TSIG context that is to be used for signing the
+    /// message. If NULL the message will not be signed.
+    void toWire(dns::AbstractMessageRenderer& renderer,
+                dns::TSIGContext* const tsig_ctx = NULL);
 
     /// @brief Decode incoming message from the wire format.
     ///
     /// This function decodes the DNS Update message stored in the buffer
-    /// specified by the function argument. In the first turn, this function
-    /// parses message header and extracts the section counters: ZOCOUNT,
-    /// PRCOUNT, UPCOUNT and ADCOUNT. Using these counters, function identifies
-    /// message sections, which follow message header. These sections can be
-    /// later accessed using: @c D2UpdateMessage::getZone,
-    /// @c D2UpdateMessage::beginSection and @c D2UpdateMessage::endSection
-    /// functions.
+    /// specified by the function argument.  If given a TSIG context, then
+    /// the function will first attempt to use that context to verify the
+    /// message signature.  If verification fails a TSIGVefiryError exception
+    /// will be thrown. The function then parses message header and extracts
+    /// the section counters: ZOCOUNT, PRCOUNT, UPCOUNT and ADCOUNT. Using
+    /// these counters, function identifies message sections, which follow
+    /// message header. These sections can be later accessed using:
+    /// @c D2UpdateMessage::getZone, @c D2UpdateMessage::beginSection and
+    /// @c D2UpdateMessage::endSection functions.
     ///
     /// This function is NOT exception safe. It signals message decoding errors
     /// through exceptions. Message decoding error may occur if the received
@@ -282,8 +302,12 @@ public:
     /// message is the server response.
     /// - The number of records in the Zone section is greater than 1.
     ///
-    /// @param buffer input buffer, holding DNS Update message to be parsed.
-    void fromWire(isc::util::InputBuffer& buffer);
+    /// @param received_data buffer holding DNS Update message to be parsed.
+    /// @param bytes_received the number of bytes in received_data
+    /// @param tsig_context A TSIG context that is to be used to verify the
+    /// message. If NULL TSIG verification will not be attempted.
+    void fromWire(const void* received_data, size_t bytes_received,
+                  dns::TSIGContext* const tsig_context = NULL);
     //@}
 
 private:

+ 1 - 1
src/bin/d2/dhcp-ddns.spec

@@ -44,7 +44,7 @@
             "item_name": "tsig_key",
             "item_type": "map",
             "item_optional": false,
-            "item_default": {"algorithm" : "hmac_md5"},
+            "item_default": {"algorithm" : "HMAC-MD5"},
             "map_item_spec": [ 
             {
                 "item_name": "name",

+ 37 - 34
src/bin/d2/dns_client.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 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
@@ -60,6 +60,8 @@ public:
     DNSClient::Callback* callback_;
     // A Transport Layer protocol used to communicate with a DNS.
     DNSClient::Protocol proto_;
+    // TSIG context used to sign outbound and verify inbound messages.
+    dns::TSIGContextPtr tsig_context_;
 
     // Constructor and Destructor
     DNSClientImpl(D2UpdateMessagePtr& response_placeholder,
@@ -73,12 +75,13 @@ public:
     // type, representing a response from the server is set.
     virtual void operator()(asiodns::IOFetch::Result result);
 
-    // Starts asynchronous DNS Update.
+    // Starts asynchronous DNS Update using TSIG.
     void doUpdate(asiolink::IOService& io_service,
                   const asiolink::IOAddress& ns_addr,
                   const uint16_t ns_port,
                   D2UpdateMessage& update,
-                  const unsigned int wait);
+                  const unsigned int wait,
+                  const dns::TSIGKeyPtr& tsig_key);
 
     // This function maps the IO error to the DNSClient error.
     DNSClient::Status getStatus(const asiodns::IOFetch::Result);
@@ -130,7 +133,6 @@ DNSClientImpl::operator()(asiodns::IOFetch::Result result) {
     // and pass the status code.
     DNSClient::Status status = getStatus(result);
     if (status == DNSClient::SUCCESS) {
-        InputBuffer response_buf(in_buf_->getData(), in_buf_->getLength());
         // Allocate a new response message. (Note that Message::fromWire
         // may only be run once per message, so we need to start fresh
         // each time.)
@@ -140,14 +142,19 @@ DNSClientImpl::operator()(asiodns::IOFetch::Result result) {
         // throw an exception. We want to catch this exception to return
         // appropriate status code to the caller and log this event.
         try {
-            response_->fromWire(response_buf);
-
+            response_->fromWire(in_buf_->getData(), in_buf_->getLength(),
+                                tsig_context_.get());
         } catch (const isc::Exception& ex) {
             status = DNSClient::INVALID_RESPONSE;
             LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL,
                       DHCP_DDNS_INVALID_RESPONSE).arg(ex.what());
 
         }
+
+        if (tsig_context_) {
+            // Context is a one-shot deal, get rid of it.
+            tsig_context_.reset();
+        }
     }
 
     // Once we are done with internal business, let's call a callback supplied
@@ -174,13 +181,31 @@ DNSClientImpl::getStatus(const asiodns::IOFetch::Result result) {
     }
     return (DNSClient::OTHER);
 }
-
 void
 DNSClientImpl::doUpdate(asiolink::IOService& io_service,
                         const IOAddress& ns_addr,
                         const uint16_t ns_port,
                         D2UpdateMessage& update,
-                        const unsigned int wait) {
+                        const unsigned int wait,
+                        const dns::TSIGKeyPtr& tsig_key) {
+    // The underlying implementation which we use to send DNS Updates uses
+    // signed integers for timeout. If we want to avoid overflows we need to
+    // respect this limitation here.
+    if (wait > DNSClient::getMaxTimeout()) {
+        isc_throw(isc::BadValue, "A timeout value for DNS Update request must"
+                  " not exceed " << DNSClient::getMaxTimeout()
+                  << ". Provided timeout value is '" << wait << "'");
+    }
+
+    // Create a TSIG context if we have a key, otherwise clear the context
+    // pointer.  Message marshalling uses non-null context is the indicator
+    // that TSIG should be used.
+    if (tsig_key) {
+        tsig_context_.reset(new TSIGContext(*tsig_key));
+    } else {
+        tsig_context_.reset();
+    }
+
     // A renderer is used by the toWire function which creates the on-wire data
     // from the DNS Update message. A renderer has its internal buffer where it
     // renders data by default. However, this buffer can't be directly accessed.
@@ -193,7 +218,7 @@ DNSClientImpl::doUpdate(asiolink::IOService& io_service,
 
     // Render DNS Update message. This may throw a bunch of exceptions if
     // invalid message object is given.
-    update.toWire(renderer);
+    update.toWire(renderer, tsig_context_.get());
 
     // IOFetch has all the mechanisms that we need to perform asynchronous
     // communication with the DNS server. The last but one argument points to
@@ -211,7 +236,6 @@ DNSClientImpl::doUpdate(asiolink::IOService& io_service,
     io_service.post(io_fetch);
 }
 
-
 DNSClient::DNSClient(D2UpdateMessagePtr& response_placeholder,
                      Callback* callback, const DNSClient::Protocol proto)
     : impl_(new DNSClientImpl(response_placeholder, callback, proto)) {
@@ -228,35 +252,14 @@ DNSClient::getMaxTimeout() {
 }
 
 void
-DNSClient::doUpdate(asiolink::IOService&,
-                    const IOAddress&,
-                    const uint16_t,
-                    D2UpdateMessage&,
-                    const unsigned int,
-                    const dns::TSIGKey&) {
-    isc_throw(isc::NotImplemented, "TSIG is currently not supported for"
-              "DNS Update message");
-}
-
-void
 DNSClient::doUpdate(asiolink::IOService& io_service,
                     const IOAddress& ns_addr,
                     const uint16_t ns_port,
                     D2UpdateMessage& update,
-                    const unsigned int wait) {
-    // The underlying implementation which we use to send DNS Updates uses
-    // signed integers for timeout. If we want to avoid overflows we need to
-    // respect this limitation here.
-    if (wait > getMaxTimeout()) {
-        isc_throw(isc::BadValue, "A timeout value for DNS Update request must"
-                  " not exceed " << getMaxTimeout()
-                  << ". Provided timeout value is '" << wait << "'");
-    }
-    impl_->doUpdate(io_service, ns_addr, ns_port, update, wait);
+                    const unsigned int wait,
+                    const dns::TSIGKeyPtr& tsig_key) {
+    impl_->doUpdate(io_service, ns_addr, ns_port, update, wait, tsig_key);
 }
 
-
-
 } // namespace d2
 } // namespace isc
-

+ 4 - 30
src/bin/d2/dns_client.h

@@ -146,41 +146,15 @@ public:
     /// @param wait A timeout (in milliseconds) for the response. If a response
     /// is not received within the timeout, exchange is interrupted. This value
     /// must not exceed maximal value for 'int' data type.
-    /// @param tsig_key An @c isc::dns::TSIGKey object representing TSIG
-    /// context which will be used to render the DNS Update message.
-    ///
-    /// @todo Implement TSIG Support. Currently any attempt to call this
-    /// function will result in exception.
+    /// @param tsig_key A pointer to an @c isc::dns::TSIGKey object that will
+    /// (if not null) be used to sign the DNS Update message and verify the
+    /// response.
     void doUpdate(asiolink::IOService& io_service,
                   const asiolink::IOAddress& ns_addr,
                   const uint16_t ns_port,
                   D2UpdateMessage& update,
                   const unsigned int wait,
-                  const dns::TSIGKey& tsig_key);
-
-    /// @brief Start asynchronous DNS Update without TSIG.
-    ///
-    /// This function starts asynchronous DNS Update and returns. The DNS Update
-    /// will be executed by the specified IO service. Once the message exchange
-    /// with a DNS server is complete, timeout occurs or IO operation is
-    /// interrupted, the caller-supplied callback function will be invoked.
-    ///
-    /// An address and port of the DNS server is specified through the function
-    /// arguments so as the same instance of the @c DNSClient can be used to
-    /// initiate multiple message exchanges.
-    ///
-    /// @param io_service IO service to be used to run the message exchange.
-    /// @param ns_addr DNS server address.
-    /// @param ns_port DNS server port.
-    /// @param update A DNS Update message to be sent to the server.
-    /// @param wait A timeout (in milliseconds) for the response. If a response
-    /// is not received within the timeout, exchange is interrupted. This value
-    /// must not exceed maximal value for 'int' data type.
-    void doUpdate(asiolink::IOService& io_service,
-                  const asiolink::IOAddress& ns_addr,
-                  const uint16_t ns_port,
-                  D2UpdateMessage& update,
-                  const unsigned int wait);
+                  const dns::TSIGKeyPtr& tsig_key = dns::TSIGKeyPtr());
 
 private:
     DNSClientImpl* impl_;  ///< Pointer to DNSClient implementation.

+ 12 - 5
src/bin/d2/nc_trans.cc

@@ -54,7 +54,7 @@ NameChangeTransaction(IOServicePtr& io_service,
      dns_update_status_(DNSClient::OTHER), dns_update_response_(),
      forward_change_completed_(false), reverse_change_completed_(false),
      current_server_list_(), current_server_(), next_server_pos_(0),
-     update_attempts_(0), cfg_mgr_(cfg_mgr) {
+     update_attempts_(0), cfg_mgr_(cfg_mgr), tsig_key_() {
     /// @todo if io_service is NULL we are multi-threading and should
     /// instantiate our own
     if (!io_service_) {
@@ -168,8 +168,7 @@ NameChangeTransaction::transactionOutcomeString() const {
 
 
 void
-NameChangeTransaction::sendUpdate(const std::string& comment,
-                                  bool /* use_tsig_ */) {
+NameChangeTransaction::sendUpdate(const std::string& comment) {
     try {
         ++update_attempts_;
         // @todo add logic to add/replace TSIG key info in request if
@@ -179,8 +178,7 @@ NameChangeTransaction::sendUpdate(const std::string& comment,
         D2ParamsPtr d2_params = cfg_mgr_->getD2Params();
         dns_client_->doUpdate(*io_service_, current_server_->getIpAddress(),
                               current_server_->getPort(), *dns_update_request_,
-                              d2_params->getDnsServerTimeout());
-
+                              d2_params->getDnsServerTimeout(), tsig_key_);
         // Message is on its way, so the next event should be NOP_EVT.
         postNextEvent(NOP_EVT);
         LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL,
@@ -424,6 +422,15 @@ NameChangeTransaction::initServerSelection(const DdnsDomainPtr& domain) {
         isc_throw(NameChangeTransactionError,
                   "initServerSelection called with an empty domain");
     }
+
+    // Set the tsig_key to that of the DdnsDomain.
+    TSIGKeyInfoPtr tsig_key_info = domain->getTSIGKeyInfo();
+    if (tsig_key_info) {
+        tsig_key_ = tsig_key_info->getTSIGKey();
+    } else {
+        tsig_key_.reset();
+    }
+
     current_server_list_ = domain->getServers();
     next_server_pos_ = 0;
     current_server_.reset();

+ 9 - 4
src/bin/d2/nc_trans.h

@@ -23,6 +23,7 @@
 #include <d2/dns_client.h>
 #include <d2/state_model.h>
 #include <dhcp_ddns/ncr_msg.h>
+#include <dns/tsig.h>
 
 #include <boost/shared_ptr.hpp>
 #include <map>
@@ -209,14 +210,15 @@ protected:
     /// currently selected server.  Since the send is asynchronous, the method
     /// posts NOP_EVT as the next event and then returns.
     ///
+    /// If tsig_key_ is not NULL, then the update will be conducted using
+    /// the key to sign the request and verify the response, otherwise it
+    /// will be conducted without TSIG.
+    ///
     /// @param comment text to include in log detail
-    /// @param use_tsig True if the update should be include a TSIG key. This
-    /// is not yet implemented.
     ///
     /// If an exception occurs it will be logged and and the transaction will
     /// be failed.
-    virtual void sendUpdate(const std::string& comment = "",
-                            bool use_tsig = false);
+    virtual void sendUpdate(const std::string& comment = "");
 
     /// @brief Adds events defined by NameChangeTransaction to the event set.
     ///
@@ -575,6 +577,9 @@ private:
 
     /// @brief Pointer to the configuration manager.
     D2CfgMgrPtr cfg_mgr_;
+
+    /// @brief Pointer to the TSIG key which should be used (if any).
+    dns::TSIGKeyPtr tsig_key_;
 };
 
 /// @brief Defines a pointer to a NameChangeTransaction.

+ 122 - 71
src/bin/d2/tests/d2_cfg_mgr_unittests.cc

@@ -17,6 +17,7 @@
 #include <d2/d2_cfg_mgr.h>
 #include <d_test_stubs.h>
 #include <test_data_files_config.h>
+#include <util/encode/base64.h>
 
 #include <boost/foreach.hpp>
 #include <gtest/gtest.h>
@@ -170,47 +171,24 @@ bool checkServer(DnsServerInfoPtr server, const char* hostname,
 }
 
 /// @brief Convenience function which compares the contents of the given
-/// TSIGKeyInfo against the given set of values.
+/// TSIGKeyInfo against the given set of values, and that the TSIGKey
+/// member points to a key.
 ///
-/// It is structured in such a way that each value is checked, and output
-/// is generate for all that do not match.
-///
-/// @param key is a pointer to the key to check against.
+/// @param key is a pointer to the TSIGKeyInfo instance to verify
 /// @param name is the value to compare against key's name_.
 /// @param algorithm is the string value to compare against key's algorithm.
 /// @param secret is the value to compare against key's secret.
 ///
 /// @return returns true if there is a match across the board, otherwise it
 /// returns false.
-bool checkKey(TSIGKeyInfoPtr key, const char* name,
-                 const char *algorithm, const char* secret)
-{
+bool checkKey(TSIGKeyInfoPtr key, const std::string& name,
+                 const std::string& algorithm, const std::string& secret) {
     // Return value, assume its a match.
-    bool result = true;
-    if (!key) {
-        EXPECT_TRUE(key);
-        return false;
-    }
-
-    // Check name.
-    if (key->getName() != name) {
-        EXPECT_EQ(name, key->getName());
-        result = false;
-    }
-
-    // Check algorithm.
-    if (key->getAlgorithm() != algorithm) {
-        EXPECT_EQ(algorithm, key->getAlgorithm());
-        result = false;
-    }
-
-    // Check secret.
-    if (key->getSecret() !=  secret) {
-        EXPECT_EQ (secret, key->getSecret());
-        result = false;
-    }
-
-    return (result);
+    return (((key) &&
+        (key->getName() == name) &&
+        (key->getAlgorithm() == algorithm)  &&
+        (key->getSecret() ==  secret)  &&
+        (key->getTSIGKey())));
 }
 
 /// @brief Test fixture class for testing DnsServerInfo parsing.
@@ -459,14 +437,12 @@ TEST_F(D2CfgMgrTest, invalidEntry) {
 /// 1. Name cannot be blank.
 /// 2. Algorithm cannot be blank.
 /// 3. Secret cannot be blank.
-/// @TODO TSIG keys are not fully functional. Only basic validation is
-/// currently supported. This test will need to expand as they evolve.
 TEST_F(TSIGKeyInfoTest, invalidEntry) {
     // Config with a blank name entry.
     std::string config = "{"
                          " \"name\": \"\" , "
-                         " \"algorithm\": \"md5\" , "
-                         " \"secret\": \"0123456789\" "
+                         " \"algorithm\": \"HMAC-MD5\" , "
+                         "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                          "}";
     ASSERT_TRUE(fromJSON(config));
 
@@ -477,7 +453,19 @@ TEST_F(TSIGKeyInfoTest, invalidEntry) {
     config = "{"
                          " \"name\": \"d2_key_one\" , "
                          " \"algorithm\": \"\" , "
-                         " \"secret\": \"0123456789\" "
+                         "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
+                         "}";
+
+    ASSERT_TRUE(fromJSON(config));
+
+    // Verify that build fails on blank algorithm.
+    EXPECT_THROW(parser_->build(config_set_), D2CfgError);
+
+    // Config with an invalid algorithm entry.
+    config = "{"
+                         " \"name\": \"d2_key_one\" , "
+                         " \"algorithm\": \"bogus\" , "
+                         "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                          "}";
 
     ASSERT_TRUE(fromJSON(config));
@@ -488,7 +476,7 @@ TEST_F(TSIGKeyInfoTest, invalidEntry) {
     // Config with a blank secret entry.
     config = "{"
                          " \"name\": \"d2_key_one\" , "
-                         " \"algorithm\": \"md5\" , "
+                         " \"algorithm\": \"HMAC-MD5\" , "
                          " \"secret\": \"\" "
                          "}";
 
@@ -496,6 +484,18 @@ TEST_F(TSIGKeyInfoTest, invalidEntry) {
 
     // Verify that build fails blank secret
     EXPECT_THROW(parser_->build(config_set_), D2CfgError);
+
+    // Config with an invalid secret entry.
+    config = "{"
+                         " \"name\": \"d2_key_one\" , "
+                         " \"algorithm\": \"HMAC-MD5\" , "
+                         " \"secret\": \"bogus\" "
+                         "}";
+
+    ASSERT_TRUE(fromJSON(config));
+
+    // Verify that build fails an invalid secret
+    EXPECT_THROW(parser_->build(config_set_), D2CfgError);
 }
 
 /// @brief Verifies that TSIGKeyInfo parsing creates a proper TSIGKeyInfo
@@ -504,13 +504,14 @@ TEST_F(TSIGKeyInfoTest, validEntry) {
     // Valid entries for TSIG key, all items are required.
     std::string config = "{"
                          " \"name\": \"d2_key_one\" , "
-                         " \"algorithm\": \"md5\" , "
-                         " \"secret\": \"0123456789\" "
+                         " \"algorithm\": \"HMAC-MD5\" , "
+                         " \"secret\": \"dGhpcyBrZXkgd2lsbCBtYXRjaA==\" "
                          "}";
     ASSERT_TRUE(fromJSON(config));
 
     // Verify that it builds and commits without throwing.
-    ASSERT_NO_THROW(parser_->build(config_set_));
+    //ASSERT_NO_THROW(parser_->build(config_set_));
+    (parser_->build(config_set_));
     ASSERT_NO_THROW(parser_->commit());
 
     // Verify the correct number of keys are present
@@ -523,7 +524,8 @@ TEST_F(TSIGKeyInfoTest, validEntry) {
     TSIGKeyInfoPtr& key = gotit->second;
 
     // Verify the key contents.
-    EXPECT_TRUE(checkKey(key, "d2_key_one", "md5", "0123456789"));
+    EXPECT_TRUE(checkKey(key, "d2_key_one", "HMAC-MD5",
+                         "dGhpcyBrZXkgd2lsbCBtYXRjaA=="));
 }
 
 /// @brief Verifies that attempting to parse an invalid list of TSIGKeyInfo
@@ -533,16 +535,17 @@ TEST_F(TSIGKeyInfoTest, invalidTSIGKeyList) {
     std::string config = "["
 
                          " { \"name\": \"key1\" , "
-                         "   \"algorithm\": \"algo1\" ,"
-                         "   \"secret\": \"secret11\" "
+                         "   \"algorithm\": \"HMAC-MD5\" ,"
+                         "   \"secret\": \"GWG/Xfbju4O2iXGqkSu4PQ==\" "
                          " },"
+                         // this entry has an invalid algorithm
                          " { \"name\": \"key2\" , "
                          "   \"algorithm\": \"\" ,"
-                         "   \"secret\": \"secret12\" "
+                         "   \"secret\": \"GWG/Xfbju4O2iXGqkSu4PQ==\" "
                          " },"
                          " { \"name\": \"key3\" , "
-                         "   \"algorithm\": \"algo3\" ,"
-                         "   \"secret\": \"secret13\" "
+                         "   \"algorithm\": \"HMAC-MD5\" ,"
+                         "   \"secret\": \"GWG/Xfbju4O2iXGqkSu4PQ==\" "
                          " }"
                          " ]";
 
@@ -563,16 +566,16 @@ TEST_F(TSIGKeyInfoTest, duplicateTSIGKey) {
     std::string config = "["
 
                          " { \"name\": \"key1\" , "
-                         "   \"algorithm\": \"algo1\" ,"
-                         "   \"secret\": \"secret11\" "
+                         "   \"algorithm\": \"HMAC-MD5\" ,"
+                         "   \"secret\": \"GWG/Xfbju4O2iXGqkSu4PQ==\" "
                          " },"
                          " { \"name\": \"key2\" , "
-                         "   \"algorithm\": \"algo2\" ,"
-                         "   \"secret\": \"secret12\" "
+                         "   \"algorithm\": \"HMAC-MD5\" ,"
+                         "   \"secret\": \"GWG/Xfbju4O2iXGqkSu4PQ==\" "
                          " },"
                          " { \"name\": \"key1\" , "
-                         "   \"algorithm\": \"algo3\" ,"
-                         "   \"secret\": \"secret13\" "
+                         "   \"algorithm\": \"HMAC-MD5\" ,"
+                         "   \"secret\": \"GWG/Xfbju4O2iXGqkSu4PQ==\" "
                          " }"
                          " ]";
 
@@ -587,21 +590,34 @@ TEST_F(TSIGKeyInfoTest, duplicateTSIGKey) {
 }
 
 /// @brief Verifies a valid list of TSIG Keys parses correctly.
+/// Also verifies that all of the supported algorithm names work.
 TEST_F(TSIGKeyInfoTest, validTSIGKeyList) {
     // Construct a valid list of keys.
     std::string config = "["
 
                          " { \"name\": \"key1\" , "
-                         "   \"algorithm\": \"algo1\" ,"
-                         "   \"secret\": \"secret1\" "
+                         "   \"algorithm\": \"HMAC-MD5\" ,"
+                         "  \"secret\": \"dGhpcyBrZXkgd2lsbCBtYXRjaA==\" "
                          " },"
                          " { \"name\": \"key2\" , "
-                         "   \"algorithm\": \"algo2\" ,"
-                         "   \"secret\": \"secret2\" "
+                         "   \"algorithm\": \"HMAC-SHA1\" ,"
+                         "  \"secret\": \"dGhpcyBrZXkgd2lsbCBtYXRjaA==\" "
                          " },"
                          " { \"name\": \"key3\" , "
-                         "   \"algorithm\": \"algo3\" ,"
-                         "   \"secret\": \"secret3\" "
+                         "   \"algorithm\": \"HMAC-SHA256\" ,"
+                         "  \"secret\": \"dGhpcyBrZXkgd2lsbCBtYXRjaA==\" "
+                         " },"
+                         " { \"name\": \"key4\" , "
+                         "   \"algorithm\": \"HMAC-SHA224\" ,"
+                         "  \"secret\": \"dGhpcyBrZXkgd2lsbCBtYXRjaA==\" "
+                         " },"
+                         " { \"name\": \"key5\" , "
+                         "   \"algorithm\": \"HMAC-SHA384\" ,"
+                         "  \"secret\": \"dGhpcyBrZXkgd2lsbCBtYXRjaA==\" "
+                         " },"
+                         " { \"name\": \"key6\" , "
+                         "   \"algorithm\": \"HMAC-SHA512\" ,"
+                         "   \"secret\": \"dGhpcyBrZXkgd2lsbCBtYXRjaA==\" "
                          " }"
                          " ]";
 
@@ -614,9 +630,10 @@ TEST_F(TSIGKeyInfoTest, validTSIGKeyList) {
     ASSERT_NO_THROW(parser->build(config_set_));
     ASSERT_NO_THROW(parser->commit());
 
+    std::string ref_secret = "dGhpcyBrZXkgd2lsbCBtYXRjaA==";
     // Verify the correct number of keys are present
     int count =  keys_->size();
-    ASSERT_EQ(3, count);
+    ASSERT_EQ(6, count);
 
     // Find the 1st key and retrieve it.
     TSIGKeyInfoMap::iterator gotit = keys_->find("key1");
@@ -624,7 +641,7 @@ TEST_F(TSIGKeyInfoTest, validTSIGKeyList) {
     TSIGKeyInfoPtr& key = gotit->second;
 
     // Verify the key contents.
-    EXPECT_TRUE(checkKey(key, "key1", "algo1", "secret1"));
+    EXPECT_TRUE(checkKey(key, "key1", TSIGKeyInfo::HMAC_MD5_STR, ref_secret));
 
     // Find the 2nd key and retrieve it.
     gotit = keys_->find("key2");
@@ -632,7 +649,7 @@ TEST_F(TSIGKeyInfoTest, validTSIGKeyList) {
     key = gotit->second;
 
     // Verify the key contents.
-    EXPECT_TRUE(checkKey(key, "key2", "algo2", "secret2"));
+    EXPECT_TRUE(checkKey(key, "key2", TSIGKeyInfo::HMAC_SHA1_STR, ref_secret));
 
     // Find the 3rd key and retrieve it.
     gotit = keys_->find("key3");
@@ -640,7 +657,35 @@ TEST_F(TSIGKeyInfoTest, validTSIGKeyList) {
     key = gotit->second;
 
     // Verify the key contents.
-    EXPECT_TRUE(checkKey(key, "key3", "algo3", "secret3"));
+    EXPECT_TRUE(checkKey(key, "key3", TSIGKeyInfo::HMAC_SHA256_STR,
+                         ref_secret));
+
+    // Find the 4th key and retrieve it.
+    gotit = keys_->find("key4");
+    ASSERT_TRUE(gotit != keys_->end());
+    key = gotit->second;
+
+    // Verify the key contents.
+    EXPECT_TRUE(checkKey(key, "key4", TSIGKeyInfo::HMAC_SHA224_STR,
+                         ref_secret));
+
+    // Find the 5th key and retrieve it.
+    gotit = keys_->find("key5");
+    ASSERT_TRUE(gotit != keys_->end());
+    key = gotit->second;
+
+    // Verify the key contents.
+    EXPECT_TRUE(checkKey(key, "key5", TSIGKeyInfo::HMAC_SHA384_STR,
+                         ref_secret));
+
+    // Find the 6th key and retrieve it.
+    gotit = keys_->find("key6");
+    ASSERT_TRUE(gotit != keys_->end());
+    key = gotit->second;
+
+    // Verify the key contents.
+    EXPECT_TRUE(checkKey(key, "key6", TSIGKeyInfo::HMAC_SHA512_STR,
+                         ref_secret));
 }
 
 /// @brief Tests the enforcement of data validation when parsing DnsServerInfos.
@@ -876,7 +921,7 @@ TEST_F(DdnsDomainTest, ddnsDomainParsing) {
     ASSERT_TRUE(fromJSON(config));
 
     // Add a TSIG key to the test key map, so key validation will pass.
-    addKey("d2_key.tmark.org", "md5", "0123456789");
+    addKey("d2_key.tmark.org", "HMAC-MD5", "GWG/Xfbju4O2iXGqkSu4PQ==");
 
     // Verify that the domain configuration builds and commits without error.
     ASSERT_NO_THROW(parser_->build(config_set_));
@@ -895,6 +940,8 @@ TEST_F(DdnsDomainTest, ddnsDomainParsing) {
     // Verify the name and key_name values.
     EXPECT_EQ("tmark.org", domain->getName());
     EXPECT_EQ("d2_key.tmark.org", domain->getKeyName());
+    ASSERT_TRUE(domain->getTSIGKeyInfo());
+    ASSERT_TRUE(domain->getTSIGKeyInfo()->getTSIGKey());
 
     // Verify that the server list exists and contains the correct number of
     // servers.
@@ -952,8 +999,8 @@ TEST_F(DdnsDomainTest, DdnsDomainListParsing) {
     ASSERT_TRUE(fromJSON(config));
 
     // Add keys to key map so key validation passes.
-    addKey("d2_key.tmark.org", "algo1", "secret1");
-    addKey("d2_key.billcat.net", "algo2", "secret2");
+    addKey("d2_key.tmark.org", "HMAC-MD5", "GWG/Xfbju4O2iXGqkSu4PQ==");
+    addKey("d2_key.billcat.net", "HMAC-MD5", "GWG/Xfbju4O2iXGqkSu4PQ==");
 
     // Create the list parser
     isc::dhcp::ParserPtr list_parser;
@@ -976,6 +1023,8 @@ TEST_F(DdnsDomainTest, DdnsDomainListParsing) {
     // Verify the name and key_name values of the first domain.
     EXPECT_EQ("tmark.org", domain->getName());
     EXPECT_EQ("d2_key.tmark.org", domain->getKeyName());
+    ASSERT_TRUE(domain->getTSIGKeyInfo());
+    ASSERT_TRUE(domain->getTSIGKeyInfo()->getTSIGKey());
 
     // Verify the each of the first domain's servers
     DnsServerInfoStoragePtr servers = domain->getServers();
@@ -1003,6 +1052,8 @@ TEST_F(DdnsDomainTest, DdnsDomainListParsing) {
     // Verify the name and key_name values of the second domain.
     EXPECT_EQ("billcat.net", domain->getName());
     EXPECT_EQ("d2_key.billcat.net", domain->getKeyName());
+    ASSERT_TRUE(domain->getTSIGKeyInfo());
+    ASSERT_TRUE(domain->getTSIGKeyInfo()->getTSIGKey());
 
     // Verify the each of second domain's servers
     servers = domain->getServers();
@@ -1090,13 +1141,13 @@ TEST_F(D2CfgMgrTest, fullConfig) {
                         "\"tsig_keys\": ["
                         "{"
                         "  \"name\": \"d2_key.tmark.org\" , "
-                        "  \"algorithm\": \"md5\" , "
-                        "  \"secret\": \"ssh-dont-tell\"  "
+                        "  \"algorithm\": \"hmac-md5\" , "
+                        "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                         "},"
                         "{"
                         "  \"name\": \"d2_key.billcat.net\" , "
-                        "  \"algorithm\": \"md5\" , "
-                        "  \"secret\": \"ollie-ollie-in-free\"  "
+                        "  \"algorithm\": \"hmac-md5\" , "
+                        "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                         "}"
                         "],"
                         "\"forward_ddns\" : {"

+ 3 - 3
src/bin/d2/tests/d2_process_unittests.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 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
@@ -39,8 +39,8 @@ const char* bad_ip_d2_config = "{ "
                         "\"port\" : 5031, "
                         "\"tsig_keys\": ["
                         "{ \"name\": \"d2_key.tmark.org\" , "
-                        "   \"algorithm\": \"md5\" ,"
-                        "   \"secret\": \"0123456989\" "
+                        "   \"algorithm\": \"HMAC-MD5\" ,"
+                        "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                         "} ],"
                         "\"forward_ddns\" : {"
                         "\"ddns_domains\": [ "

+ 125 - 11
src/bin/d2/tests/d2_update_message_unittests.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 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
@@ -14,6 +14,7 @@
 
 #include <config.h>
 
+#include <d2/d2_config.h>
 #include <d2/d2_update_message.h>
 #include <d2/d2_zone.h>
 #include <dns/messagerenderer.h>
@@ -201,12 +202,12 @@ TEST_F(D2UpdateMessageTest, fromWire) {
         0x20, 0x01, 0x0D, 0xB8, 0x00, 0x01, 0x00, 0x00,
         0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01
     };
-    InputBuffer buf(bin_msg, sizeof(bin_msg));
 
     // Create an object to be used to decode the message from the wire format.
     D2UpdateMessage msg(D2UpdateMessage::INBOUND);
+
     // Decode the message.
-    ASSERT_NO_THROW(msg.fromWire(buf));
+    ASSERT_NO_THROW(msg.fromWire(bin_msg, sizeof(bin_msg)));
 
     // Check that the message header is valid.
     EXPECT_EQ(0x05AF, msg.getId());
@@ -287,14 +288,14 @@ TEST_F(D2UpdateMessageTest, fromWireInvalidOpcode) {
         0x0, 0x0,   // UPCOUNT=0
         0x0, 0x0    // ADCOUNT=0
     };
-    InputBuffer buf(bin_msg, sizeof(bin_msg));
     // The 'true' argument passed to the constructor turns the
     // message into the parse mode in which the fromWire function
     // can be used to decode the binary mesasage data.
     D2UpdateMessage msg(D2UpdateMessage::INBOUND);
     // When using invalid Opcode, the fromWire function should
     // throw NotUpdateMessage exception.
-    EXPECT_THROW(msg.fromWire(buf), isc::d2::NotUpdateMessage);
+    EXPECT_THROW(msg.fromWire(bin_msg, sizeof(bin_msg)),
+                 isc::d2::NotUpdateMessage);
 }
 
 // This test verifies that the fromWire function throws appropriate exception
@@ -311,14 +312,14 @@ TEST_F(D2UpdateMessageTest, fromWireInvalidQRFlag) {
         0x0, 0x0,   // UPCOUNT=0
         0x0, 0x0    // ADCOUNT=0
     };
-    InputBuffer buf(bin_msg, sizeof(bin_msg));
     // The 'true' argument passed to the constructor turns the
     // message into the parse mode in which the fromWire function
     // can be used to decode the binary mesasage data.
     D2UpdateMessage msg(D2UpdateMessage::INBOUND);
     // When using invalid QR flag, the fromWire function should
     // throw InvalidQRFlag exception.
-    EXPECT_THROW(msg.fromWire(buf), isc::d2::InvalidQRFlag);
+    EXPECT_THROW(msg.fromWire(bin_msg, sizeof(bin_msg)),
+                              isc::d2::InvalidQRFlag);
 }
 
 // This test verifies that the fromWire function throws appropriate exception
@@ -349,7 +350,6 @@ TEST_F(D2UpdateMessageTest, fromWireTooManyZones) {
         0x0, 0x6, // ZTYPE='SOA'
         0x0, 0x1  // ZCLASS='IN'
     };
-    InputBuffer buf(bin_msg, sizeof(bin_msg));
 
     // The 'true' argument passed to the constructor turns the
     // message into the parse mode in which the fromWire function
@@ -357,7 +357,8 @@ TEST_F(D2UpdateMessageTest, fromWireTooManyZones) {
     D2UpdateMessage msg(D2UpdateMessage::INBOUND);
     // When parsing a message with more than one Zone record,
     // exception should be thrown.
-    EXPECT_THROW(msg.fromWire(buf), isc::d2::InvalidZoneSection);
+    EXPECT_THROW(msg.fromWire(bin_msg, sizeof(bin_msg)),
+                 isc::d2::InvalidZoneSection);
 }
 
 // This test verifies that the wire format of the message is produced
@@ -571,12 +572,11 @@ TEST_F(D2UpdateMessageTest, toWireInvalidQRFlag) {
         0x0, 0x0    // ADCOUNT=0
     };
 
-    InputBuffer buf(bin_msg, sizeof(bin_msg));
     // The 'true' argument passed to the constructor turns the
     // message into the parse mode in which the fromWire function
     // can be used to decode the binary mesasage data.
     D2UpdateMessage msg(D2UpdateMessage::INBOUND);
-    ASSERT_NO_THROW(msg.fromWire(buf));
+    ASSERT_NO_THROW(msg.fromWire(bin_msg, sizeof(bin_msg)));
 
     // The message is parsed. The QR Flag should now indicate that
     // it is a Response message.
@@ -588,4 +588,118 @@ TEST_F(D2UpdateMessageTest, toWireInvalidQRFlag) {
     EXPECT_THROW(msg.toWire(renderer), isc::d2::InvalidQRFlag);
 }
 
+// TSIG test
+TEST_F(D2UpdateMessageTest, validTSIG) {
+    // Create a TSIG Key and context
+    std::string secret("this key will match");
+    TSIGKeyPtr right_key;
+    ASSERT_NO_THROW(right_key.reset(new
+                                    TSIGKey(Name("right.com"),
+                                            TSIGKey::HMACMD5_NAME(),
+                                            secret.c_str(), secret.size())));
+
+    TSIGKeyPtr wrong_key;
+    secret = "this key will not match";
+    ASSERT_NO_THROW(wrong_key.reset(new
+                                    TSIGKey(Name("wrong.com"),
+                                            TSIGKey::HMACMD5_NAME(),
+                                            secret.c_str(), secret.size())));
+
+
+    // Build a request message
+    D2UpdateMessage msg;
+    msg.setId(0x1234);
+    msg.setRcode(Rcode(Rcode::NOERROR_CODE));
+    msg.setZone(Name("example.com"), RRClass::IN());
+    RRsetPtr prereq1(new RRset(Name("foo.example.com"), RRClass::NONE(),
+                               RRType::ANY(), RRTTL(0)));
+    msg.addRRset(D2UpdateMessage::SECTION_PREREQUISITE, prereq1);
+    RRsetPtr prereq2(new RRset(Name("bar.example.com"), RRClass::ANY(),
+                               RRType::ANY(), RRTTL(0)));
+    msg.addRRset(D2UpdateMessage::SECTION_PREREQUISITE, prereq2);
+    RRsetPtr updaterr1(new RRset(Name("foo.example.com"), RRClass::IN(),
+                                 RRType::A(), RRTTL(10)));
+    char rdata1[] = {
+        0xA, 0xA , 0x1, 0x1
+    };
+    InputBuffer buf_rdata1(rdata1, 4);
+    updaterr1->addRdata(createRdata(RRType::A(), RRClass::IN(), buf_rdata1,
+                                    buf_rdata1.getLength()));
+    msg.addRRset(D2UpdateMessage::SECTION_UPDATE, updaterr1);
+
+    // Make a context to send the message with and use it to render
+    // the message into the wire format.
+    TSIGContextPtr context;
+    ASSERT_NO_THROW(context.reset(new TSIGContext(*right_key)));
+    MessageRenderer renderer;
+    ASSERT_NO_THROW(msg.toWire(renderer, context.get()));
+
+    // Grab the wire data from the signed message.
+    const void* wire_data = renderer.getData();
+    const size_t wire_size = renderer.getLength();
+
+    // Make a context with the wrong key and use it to convert the wired data.
+    // Verification should fail.
+    D2UpdateMessage msg2(D2UpdateMessage::INBOUND);
+    ASSERT_NO_THROW(context.reset(new TSIGContext(*wrong_key)));
+    ASSERT_THROW(msg2.fromWire(wire_data, wire_size, context.get()),
+                 TSIGVerifyError);
+
+    // Now make a context with the correct key and try again.
+    // If the message passes TSIG verification, then the QR Flag test in
+    // the subsequent call to D2UpdateMessage::validateResponse should
+    // fail because this isn't really received message.
+    ASSERT_NO_THROW(context.reset(new TSIGContext(*right_key)));
+    ASSERT_THROW(msg2.fromWire(wire_data, wire_size, context.get()),
+                 InvalidQRFlag);
+}
+
+// Tests message signing and verification for all supported algorithms.
+TEST_F(D2UpdateMessageTest, allValidTSIG) {
+    std::vector<std::string>algorithms;
+    algorithms.push_back(TSIGKeyInfo::HMAC_MD5_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA1_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA224_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA256_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA384_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA512_STR);
+
+    dns::Name key_name("test_key");
+    std::string secret("random text for secret");
+    for (int i = 0; i < algorithms.size(); ++i) {
+        dns::TSIGKey key(key_name,
+                         TSIGKeyInfo::stringToAlgorithmName(algorithms[i]),
+                         secret.c_str(), secret.size());
+
+        // Build a request message
+        D2UpdateMessage msg;
+        msg.setId(0x1234);
+        msg.setRcode(Rcode(Rcode::NOERROR_CODE));
+        msg.setZone(Name("example.com"), RRClass::IN());
+
+        // Make a context to send the message with and use it to render
+        // the message into the wire format.
+        TSIGContextPtr context;
+        ASSERT_NO_THROW(context.reset(new TSIGContext(key)));
+        MessageRenderer renderer;
+        ASSERT_NO_THROW(msg.toWire(renderer, context.get()));
+
+        // Grab the wire data from the signed message.
+        const void* wire_data = renderer.getData();
+        const size_t wire_size = renderer.getLength();
+
+        // Create a fresh context to "receive" the message. (We can't use the
+        // one we signed it with, as its expecting a signed response to its
+        // request. Here we are acting like the server).
+        // If the message passes TSIG verification, then the QR Flag test in
+        // the subsequent call to D2UpdateMessage::validateResponse should
+        // fail because this isn't really received message.
+        ASSERT_NO_THROW(context.reset(new TSIGContext(key)));
+        D2UpdateMessage msg2(D2UpdateMessage::INBOUND);
+        ASSERT_THROW(msg2.fromWire(wire_data, wire_size, context.get()),
+                                   InvalidQRFlag);
+    }
+}
+
+
 } // End of anonymous namespace

+ 2 - 2
src/bin/d2/tests/d_test_stubs.cc

@@ -26,8 +26,8 @@ const char* valid_d2_config = "{ "
                         "\"port\" : 5031, "
                         "\"tsig_keys\": ["
                         "{ \"name\": \"d2_key.tmark.org\" , "
-                        "   \"algorithm\": \"md5\" ,"
-                        "   \"secret\": \"0123456989\" "
+                        "   \"algorithm\": \"HMAC-MD5\" ,"
+                        "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                         "} ],"
                         "\"forward_ddns\" : {"
                         "\"ddns_domains\": [ "

+ 160 - 26
src/bin/d2/tests/dns_client_unittests.cc

@@ -14,12 +14,11 @@
 
 #include <config.h>
 #include <d2/dns_client.h>
+#include <dns/opcode.h>
 #include <asiodns/io_fetch.h>
 #include <asiodns/logger.h>
 #include <asiolink/interval_timer.h>
-#include <dns/rcode.h>
-#include <dns/rrclass.h>
-#include <dns/tsig.h>
+#include <dns/messagerenderer.h>
 #include <asio/ip/udp.hpp>
 #include <asio/socket_base.hpp>
 #include <boost/bind.hpp>
@@ -189,6 +188,80 @@ public:
                         *remote);
     }
 
+    // @brief Request handler for testing clients using TSIG
+    //
+    // This callback handler is installed when performing async read on a
+    // socket to emulate reception of the DNS Update request with TSIG by a
+    // server.  As a result, this handler will send an appropriate DNS Update
+    // response message back to the address from which the request has come.
+    //
+    // @param socket A pointer to a socket used to receive a query and send a
+    // response.
+    // @param remote A pointer to an object which specifies the host (address
+    // and port) from which a request has come.
+    // @param receive_length A length (in bytes) of the received data.
+    // @param corrupt_response A bool value which indicates that the server's
+    // response should be invalid (true) or valid (false)
+    // @param client_key TSIG key the server should use to verify the inbound
+    // request.  If the pointer is NULL, the server will not attempt to
+    // verify the request.
+    // @param server_key TSIG key the server should use to sign the outbound
+    // request. If the pointer is NULL, the server will not sign the outbound
+    // response.  If the pointer is not NULL and not the same value as the
+    // client_key, the server will use a new context to sign the response then
+    // the one used to verify it.  This allows us to simulate the server
+    // signing with the wrong key.
+    void TSIGReceiveHandler(udp::socket* socket, udp::endpoint* remote,
+                            size_t receive_length,
+                            TSIGKeyPtr client_key,
+                            TSIGKeyPtr server_key) {
+
+        TSIGContextPtr context;
+        if (client_key) {
+            context.reset(new TSIGContext(*client_key));
+        }
+
+        isc::util::InputBuffer received_data_buffer(receive_buffer_,
+                                                    receive_length);
+
+        dns::Message request(Message::PARSE);
+        request.fromWire(received_data_buffer);
+
+        // If contex is not NULL, then we need to verify the message.
+        if (context) {
+            TSIGError error = context->verify(request.getTSIGRecord(),
+                                              receive_buffer_, receive_length);
+            if (error != TSIGError::NOERROR()) {
+                isc_throw(TSIGVerifyError, "TSIG verification failed: "
+                          << error.toText());
+            }
+        }
+
+        dns::Message response(Message::RENDER);
+        response.setOpcode(Opcode(Opcode::UPDATE_CODE));
+        response.setHeaderFlag(dns::Message::HEADERFLAG_QR, true);
+        response.setQid(request.getQid());
+        response.setRcode(Rcode::NOERROR());
+        dns::Question question(Name("example.com."),
+                                    RRClass::IN(), RRType::SOA());
+        response.addQuestion(question);
+
+        MessageRenderer renderer;
+
+        if (!server_key) {
+            // don't sign the response.
+            context.reset();
+        } else if (server_key != client_key) {
+            // use a different key to sign the response.
+            context.reset(new TSIGContext(*server_key));
+        }  // otherwise use the context based on client_key.
+
+        response.toWire(renderer, context.get());
+        // A response message is now ready to send. Send it!
+        socket->send_to(asio::buffer(renderer.getData(), renderer.getLength()),
+                        *remote);
+    }
+
     // This test verifies that when invalid response placeholder object is
     // passed to a constructor, constructor throws the appropriate exception.
     // It also verifies that the constructor will not throw if the supplied
@@ -229,26 +302,6 @@ public:
                      isc::BadValue);
     }
 
-    // This test verifies that isc::NotImplemented exception is thrown when
-    // attempt to send DNS Update message with TSIG is attempted.
-    void runTSIGTest() {
-        // Create outgoing message. Simply set the required message fields:
-        // error code and Zone section. This is enough to create on-wire format
-        // of this message and send it.
-        D2UpdateMessage message(D2UpdateMessage::OUTBOUND);
-        ASSERT_NO_THROW(message.setRcode(Rcode(Rcode::NOERROR_CODE)));
-        ASSERT_NO_THROW(message.setZone(Name("example.com"), RRClass::IN()));
-
-        const int timeout = 0;
-        // Try to send DNS Update with TSIG key. Currently TSIG is not supported
-        // and therefore we expect an exception.
-        TSIGKey tsig_key("key.example:MSG6Ng==");
-        EXPECT_THROW(dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS),
-                                           TEST_PORT, message, timeout,
-                                           tsig_key),
-                     isc::NotImplemented);
-    }
-
     // This test verifies the DNSClient behavior when a server does not respond
     // do the DNS Update message. In such case, the callback function is
     // expected to be called and the TIME_OUT error code should be returned.
@@ -359,6 +412,59 @@ public:
         // run_one() to work.
         service_.get_io_service().reset();
     }
+
+    // Performs a single request-response exchange with or without TSIG
+    //
+    // @param client_key TSIG passed to dns_client and also used by the
+    // ""server" to verify the request.
+    // request.
+    // @param server_key TSIG key the "server" should use to sign the response.
+    // If this is NULL, then client_key is used.
+    // @param should_pass indicates if the test should pass.
+    void runTSIGTest(TSIGKeyPtr client_key, TSIGKeyPtr server_key,
+                     bool should_pass = true) {
+        // Tell operator() method if we expect an invalid response.
+        corrupt_response_ = !should_pass;
+
+        // Create a request DNS Update message.
+        D2UpdateMessage message(D2UpdateMessage::OUTBOUND);
+        ASSERT_NO_THROW(message.setRcode(Rcode(Rcode::NOERROR_CODE)));
+        ASSERT_NO_THROW(message.setZone(Name("example.com"), RRClass::IN()));
+
+        // Setup our "loopback" server.
+        udp::socket udp_socket(service_.get_io_service(), asio::ip::udp::v4());
+        udp_socket.set_option(socket_base::reuse_address(true));
+        udp_socket.bind(udp::endpoint(address::from_string(TEST_ADDRESS),
+                                      TEST_PORT));
+        udp::endpoint remote;
+        udp_socket.async_receive_from(asio::buffer(receive_buffer_,
+                                                   sizeof(receive_buffer_)),
+                                      remote,
+                                      boost::bind(&DNSClientTest::
+                                                  TSIGReceiveHandler, this,
+                                                  &udp_socket, &remote, _2,
+                                                  client_key, server_key));
+
+        // The socket is now ready to receive the data. Let's post some request
+        // message then. Set timeout to some reasonable value to make sure that
+        // there is sufficient amount of time for the test to generate a
+        // response.
+        const int timeout = 500;
+        expected_++;
+        dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS), TEST_PORT,
+                              message, timeout, client_key);
+
+        // Kick of the message exchange by actually running the scheduled
+        // "send" and "receive" operations.
+        service_.run();
+
+        udp_socket.close();
+
+        // Since the callback, operator(), calls stop() on the io_service,
+        // we must reset it in order for subsequent calls to run() or
+        // run_one() to work.
+        service_.get_io_service().reset();
+    }
 };
 
 // Verify that the DNSClient object can be created if provided parameters are
@@ -384,10 +490,38 @@ TEST_F(DNSClientTest, invalidTimeout) {
     runInvalidTimeoutTest();
 }
 
-// Verify that exception is thrown when an attempt to send DNS Update with TSIG
-// is made. This test will be removed/changed once TSIG support is added.
+// Verifies that TSIG can be used to sign requests and verify responses.
 TEST_F(DNSClientTest, runTSIGTest) {
-    runTSIGTest();
+    std::string secret ("key number one");
+    TSIGKeyPtr key_one;
+    ASSERT_NO_THROW(key_one.reset(new
+                                    TSIGKey(Name("one.com"),
+                                            TSIGKey::HMACMD5_NAME(),
+                                            secret.c_str(), secret.size())));
+    secret = "key number two";
+    TSIGKeyPtr key_two;
+    ASSERT_NO_THROW(key_two.reset(new
+                                    TSIGKey(Name("two.com"),
+                                            TSIGKey::HMACMD5_NAME(),
+                                            secret.c_str(), secret.size())));
+    TSIGKeyPtr nokey;
+
+    // Should be able to send and receive with no keys.
+    // Neither client nor server will attempt to sign or verify.
+    runTSIGTest(nokey, nokey);
+
+    // Client signs the request, server verfies but doesn't sign.
+    runTSIGTest(key_one, nokey, false);
+
+    // Client and server use the same key to sign and verify.
+    runTSIGTest(key_one, key_one);
+
+    // Server uses different key to sign the response.
+    runTSIGTest(key_one, key_two, false);
+
+    // Client neither signs nor verifies, server responds with a signed answer
+    // Since we are "liberal" in what we accept this should be ok.
+    runTSIGTest(nokey, key_two);
 }
 
 // Verify that the DNSClient receives the response from DNS and the received

+ 3 - 5
src/bin/d2/tests/nc_add_unittests.cc

@@ -57,10 +57,8 @@ public:
     /// the simulate_send_exception_ flag is true.
     ///
     /// @param comment Parameter is unused, but present in base class method.
-    /// @param use_tsig_ Parameter is unused, but present in base class method.
     ///
-    virtual void sendUpdate(const std::string& /*comment*/,
-                            bool /* use_tsig_ = false */) {
+    virtual void sendUpdate(const std::string& /*comment*/) {
         if (simulate_send_exception_) {
             // Make the flag a one-shot by resetting it.
             simulate_send_exception_ = false;
@@ -290,8 +288,8 @@ TEST(NameAddTransaction, construction) {
     DdnsDomainPtr empty_domain;
 
     ASSERT_NO_THROW(ncr = dhcp_ddns::NameChangeRequest::fromJSON(msg_str));
-    ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", "", servers)));
-    ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", "", servers)));
+    ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", servers)));
+    ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", servers)));
 
     // Verify that construction with wrong change type fails.
     EXPECT_THROW(NameAddTransaction(io_service, ncr,

+ 3 - 5
src/bin/d2/tests/nc_remove_unittests.cc

@@ -57,10 +57,8 @@ public:
     /// the simulate_send_exception_ flag is true.
     ///
     /// @param comment Parameter is unused, but present in base class method
-    /// @param use_tsig Parameter is unused, but present in base class method.
     ///
-    virtual void sendUpdate(const std::string& /* comment */,
-                            bool /* use_tsig = false */) {
+    virtual void sendUpdate(const std::string& /* comment */) {
         if (simulate_send_exception_) {
             // Make the flag a one-shot by resetting it.
             simulate_send_exception_ = false;
@@ -292,8 +290,8 @@ TEST(NameRemoveTransaction, construction) {
     DdnsDomainPtr empty_domain;
 
     ASSERT_NO_THROW(ncr = dhcp_ddns::NameChangeRequest::fromJSON(msg_str));
-    ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", "", servers)));
-    ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", "", servers)));
+    ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", servers)));
+    ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", servers)));
 
     // Verify that construction with wrong change type fails.
     EXPECT_THROW(NameRemoveTransaction(io_service, ncr,

+ 95 - 14
src/bin/d2/tests/nc_test_utils.cc

@@ -18,6 +18,7 @@
 #include <nc_test_utils.h>
 #include <asio.hpp>
 #include <asiolink/udp_endpoint.h>
+#include <util/encode/base64.h>
 
 #include <gtest/gtest.h>
 
@@ -39,7 +40,8 @@ const bool NO_RDATA = false;
 FauxServer::FauxServer(asiolink::IOService& io_service,
                        asiolink::IOAddress& address, size_t port)
     :io_service_(io_service), address_(address), port_(port),
-     server_socket_(), receive_pending_(false), perpetual_receive_(true) {
+     server_socket_(), receive_pending_(false), perpetual_receive_(true),
+     tsig_key_() {
 
     server_socket_.reset(new asio::ip::udp::socket(io_service_.get_io_service(),
                                                    asio::ip::udp::v4()));
@@ -53,7 +55,7 @@ FauxServer::FauxServer(asiolink::IOService& io_service,
                        DnsServerInfo& server)
     :io_service_(io_service), address_(server.getIpAddress()),
      port_(server.getPort()), server_socket_(), receive_pending_(false),
-     perpetual_receive_(true) {
+     perpetual_receive_(true), tsig_key_() {
     server_socket_.reset(new asio::ip::udp::socket(io_service_.get_io_service(),
                                                    asio::ip::udp::v4()));
     server_socket_->set_option(asio::socket_base::reuse_address(true));
@@ -87,14 +89,21 @@ FauxServer::requestHandler(const asio::error_code& error,
                            std::size_t bytes_recvd,
                            const ResponseMode& response_mode,
                            const dns::Rcode& response_rcode) {
+    receive_pending_ = false;
     // If we encountered an error or received no data then fail.
     // We expect the client to send good requests.
     if (error.value() != 0 || bytes_recvd < 1) {
-        ADD_FAILURE() << "FauxServer receive failed" << error.message();
-        receive_pending_ = false;
+        ADD_FAILURE() << "FauxServer receive failed: " << error.message();
         return;
     }
 
+    // If TSIG key isn't NULL, create a context and use to verify the
+    // request and sign the response.
+    dns::TSIGContextPtr context;
+    if (tsig_key_) {
+        context.reset(new dns::TSIGContext(*tsig_key_));
+    }
+
     // We have a successfully received data. We need to turn it into
     // a request in order to build a proper response.
     // Note D2UpdateMessage is geared towards making requests and
@@ -104,11 +113,21 @@ FauxServer::requestHandler(const asio::error_code& error,
     util::InputBuffer request_buf(receive_buffer_, bytes_recvd);
     try {
         request.fromWire(request_buf);
+
+        // If contex is not NULL, then we need to verify the message.
+        if (context) {
+            dns::TSIGError error = context->verify(request.getTSIGRecord(),
+                                                   receive_buffer_,
+                                                   bytes_recvd);
+            if (error != dns::TSIGError::NOERROR()) {
+                isc_throw(TSIGVerifyError, "TSIG verification failed: "
+                          << error.toText());
+            }
+        }
     } catch (const std::exception& ex) {
         // If the request cannot be parsed, then fail the test.
         // We expect the client to send good requests.
         ADD_FAILURE() << "FauxServer request is corrupt:" << ex.what();
-        receive_pending_ = false;
         return;
     }
 
@@ -131,7 +150,19 @@ FauxServer::requestHandler(const asio::error_code& error,
     dns::MessageRenderer renderer;
     util::OutputBuffer response_buf(TEST_MSG_MAX);
     renderer.setBuffer(&response_buf);
-    response.toWire(renderer);
+
+    if (response_mode == INVALID_TSIG) {
+        // Create a different key to sign the response.
+        std::string secret ("key that doesn't match");
+        dns::TSIGKeyPtr key;
+        ASSERT_NO_THROW(key.reset(new
+                                  dns::TSIGKey(dns::Name("badkey"),
+                                               dns::TSIGKey::HMACMD5_NAME(),
+                                               secret.c_str(), secret.size())));
+        context.reset(new dns::TSIGContext(*key));
+    }
+
+    response.toWire(renderer, context.get());
 
     // If mode is to ship garbage, then stomp on part of the rendered
     // message.
@@ -154,7 +185,6 @@ FauxServer::requestHandler(const asio::error_code& error,
         ADD_FAILURE() << "FauxServer send failed: " << ex.what();
     }
 
-    receive_pending_ = false;
     if (perpetual_receive_) {
         // Schedule the next receive
         receive (response_mode, response_rcode);
@@ -162,6 +192,7 @@ FauxServer::requestHandler(const asio::error_code& error,
 }
 
 
+
 //********************** TimedIO class ***********************
 
 TimedIO::TimedIO()
@@ -205,7 +236,8 @@ TransactionTest::~TransactionTest() {
 
 void
 TransactionTest::setupForIPv4Transaction(dhcp_ddns::NameChangeType chg_type,
-                                         int change_mask) {
+                                         int change_mask,
+                                         const TSIGKeyInfoPtr& tsig_key_info) {
     const char* msg_str =
         "{"
         " \"change_type\" : 0 , "
@@ -231,7 +263,7 @@ TransactionTest::setupForIPv4Transaction(dhcp_ddns::NameChangeType chg_type,
         forward_domain_.reset();
     } else {
         // Create the forward domain and then its servers.
-        forward_domain_ = makeDomain("example.com.");
+        forward_domain_ = makeDomain("example.com.", tsig_key_info);
         addDomainServer(forward_domain_, "forward.example.com",
                         "127.0.0.1", 5301);
         addDomainServer(forward_domain_, "forward2.example.com",
@@ -245,7 +277,7 @@ TransactionTest::setupForIPv4Transaction(dhcp_ddns::NameChangeType chg_type,
         reverse_domain_.reset();
     } else {
         // Create the reverse domain and its server.
-        reverse_domain_ = makeDomain("2.168.192.in.addr.arpa.");
+        reverse_domain_ = makeDomain("2.168.192.in.addr.arpa.", tsig_key_info);
         addDomainServer(reverse_domain_, "reverse.example.com",
                         "127.0.0.1", 5301);
         addDomainServer(reverse_domain_, "reverse2.example.com",
@@ -254,8 +286,16 @@ TransactionTest::setupForIPv4Transaction(dhcp_ddns::NameChangeType chg_type,
 }
 
 void
+TransactionTest::setupForIPv4Transaction(dhcp_ddns::NameChangeType chg_type,
+                                         int change_mask,
+                                         const std::string& key_name) {
+    setupForIPv4Transaction(chg_type, change_mask, makeTSIGKeyInfo(key_name));
+}
+
+void
 TransactionTest::setupForIPv6Transaction(dhcp_ddns::NameChangeType chg_type,
-                                         int change_mask) {
+                                         int change_mask,
+                                         const TSIGKeyInfoPtr& tsig_key_info) {
     const char* msg_str =
         "{"
         " \"change_type\" : 0 , "
@@ -281,7 +321,7 @@ TransactionTest::setupForIPv6Transaction(dhcp_ddns::NameChangeType chg_type,
         forward_domain_.reset();
     } else {
         // Create the forward domain and then its servers.
-        forward_domain_ = makeDomain("example.com.");
+        forward_domain_ = makeDomain("example.com.", tsig_key_info);
         addDomainServer(forward_domain_, "fwd6-server.example.com",
                         "::1", 5301);
         addDomainServer(forward_domain_, "fwd6-server2.example.com",
@@ -295,7 +335,7 @@ TransactionTest::setupForIPv6Transaction(dhcp_ddns::NameChangeType chg_type,
         reverse_domain_.reset();
     } else {
         // Create the reverse domain and its server.
-        reverse_domain_ = makeDomain("1.2001.ip6.arpa.");
+        reverse_domain_ = makeDomain("1.2001.ip6.arpa.", tsig_key_info);
         addDomainServer(reverse_domain_, "rev6-server.example.com",
                         "::1", 5301);
         addDomainServer(reverse_domain_, "rev6-server2.example.com",
@@ -303,6 +343,13 @@ TransactionTest::setupForIPv6Transaction(dhcp_ddns::NameChangeType chg_type,
     }
 }
 
+void
+TransactionTest::setupForIPv6Transaction(dhcp_ddns::NameChangeType chg_type,
+                                         int change_mask,
+                                         const std::string& key_name) {
+    setupForIPv6Transaction(chg_type, change_mask, makeTSIGKeyInfo(key_name));
+}
+
 
 //********************** Functions ****************************
 
@@ -385,12 +432,46 @@ dhcp_ddns::NameChangeRequestPtr makeNcrFromString(const std::string& ncr_str) {
 
 DdnsDomainPtr makeDomain(const std::string& zone_name,
                          const std::string& key_name) {
+    DnsServerInfoStoragePtr servers(new DnsServerInfoStorage());
+    DdnsDomainPtr domain(new DdnsDomain(zone_name, servers,
+                         makeTSIGKeyInfo(key_name)));
+    return (domain);
+}
+
+DdnsDomainPtr makeDomain(const std::string& zone_name,
+                         const TSIGKeyInfoPtr &tsig_key_info) {
     DdnsDomainPtr domain;
     DnsServerInfoStoragePtr servers(new DnsServerInfoStorage());
-    domain.reset(new DdnsDomain(zone_name, key_name, servers));
+    domain.reset(new DdnsDomain(zone_name, servers, tsig_key_info));
     return (domain);
 }
 
+TSIGKeyInfoPtr makeTSIGKeyInfo(const std::string& key_name,
+                           const std::string& secret,
+                           const std::string& algorithm) {
+    TSIGKeyInfoPtr key_info;
+    if (!key_name.empty()) {
+        if (!secret.empty()) {
+            key_info.reset(new TSIGKeyInfo(key_name, algorithm, secret));
+        } else {
+            // Since secret was left blank, we'll convert key_name into a
+            // base64 encoded string and use that.
+            const uint8_t* bytes = reinterpret_cast<const uint8_t*>
+                                                   (key_name.c_str());
+            size_t len = key_name.size();
+            const vector<uint8_t> key_name_v(bytes, bytes + len);
+            std::string key_name64
+                = isc::util::encode::encodeBase64(key_name_v);
+
+            // Now, make the TSIGKeyInfo with a real base64 secret.
+            key_info.reset(new TSIGKeyInfo(key_name, algorithm, key_name64));
+        }
+    }
+
+    return (key_info);
+
+}
+
 void addDomainServer(DdnsDomainPtr& domain, const std::string& name,
                      const std::string& ip, const size_t port) {
     DnsServerInfoPtr server(new DnsServerInfo(name, asiolink::IOAddress(ip),

+ 89 - 7
src/bin/d2/tests/nc_test_utils.h

@@ -40,8 +40,9 @@ typedef boost::shared_ptr<asio::ip::udp::socket> SocketPtr;
 class FauxServer {
 public:
     enum  ResponseMode {
-        USE_RCODE,   // Generate a response with a given RCODE
-        CORRUPT_RESP  // Generate a corrupt response
+        USE_RCODE,    // Generate a response with a given RCODE
+        CORRUPT_RESP, // Generate a corrupt response
+        INVALID_TSIG  // Generate a repsonse with the wrong TSIG key
     };
 
     // Reference to IOService to use for IO processing.
@@ -63,6 +64,9 @@ public:
     // a receive has been completed, a new one will be automatically
     // initiated.
     bool perpetual_receive_;
+    // TSIG Key to use to verify requests and sign responses.  If its
+    // NULL TSIG is not used.
+    dns::TSIGKeyPtr tsig_key_;
 
     /// @brief Constructor
     ///
@@ -96,7 +100,9 @@ public:
     /// @brief Socket IO Completion callback
     ///
     /// This method servers as the Server's UDP socket receive callback handler.
-    /// When the receive completes the handler is invoked with the
+    /// When the receive completes the handler is invoked with the parameters
+    /// listed.
+    ///
     /// @param error result code of the receive (determined by asio layer)
     /// @param bytes_recvd number of bytes received, if any
     /// @param response_mode type of response the handler should produce
@@ -111,6 +117,14 @@ public:
     bool isReceivePending() {
         return receive_pending_;
     }
+
+    /// @brief Sets the TSIG key to the given value.
+    ///
+    /// @param tsig_key Pointer to the TSIG key to use.  If the pointer is
+    /// empty, TSIG will not be used.
+    void setTSIGKey (const dns::TSIGKeyPtr& tsig_key) {
+        tsig_key_ = tsig_key;
+    }
 };
 
 /// @brief Provides a means to process IOService IO for a finite amount of time.
@@ -187,8 +201,28 @@ public:
     /// CHG_REMOVE.
     /// @param change_mask determines which change directions are requested
     /// FORWARD_CHG, REVERSE_CHG, or FWD_AND_REV_CHG.
+    /// @param tsig_key_info pointer to the TSIGKeyInfo to assign to both
+    /// domains in the transaction.  This will cause the transaction to
+    /// use TSIG.  If the pointer is empty, TSIG will not be used.
     void setupForIPv4Transaction(dhcp_ddns::NameChangeType change_type,
-                                 int change_mask);
+                                 int change_mask,
+                                 const TSIGKeyInfoPtr& tsig_key_info =
+                                 TSIGKeyInfoPtr());
+
+    /// @brief Creates a transaction which requests an IPv4 DNS update.
+    ///
+    /// Convenience wrapper around the above method which accepts a string
+    /// key_name from which the TSIGKeyInfo is constructed.  Note the string
+    /// may not be blank.
+    ///
+    /// @param change_type selects the type of change requested, CHG_ADD or
+    /// CHG_REMOVE.
+    /// @param change_mask determines which change directions are requested
+    /// FORWARD_CHG, REVERSE_CHG, or FWD_AND_REV_CHG.
+    /// @param key_name value to use to create TSIG key. The value may not
+    /// be blank.
+    void setupForIPv4Transaction(dhcp_ddns::NameChangeType change_type,
+                                 int change_mask, const std::string& key_name);
 
     /// @brief Creates a transaction which requests an IPv6 DNS update.
     ///
@@ -201,8 +235,29 @@ public:
     /// CHG_REMOVE.
     /// @param change_mask determines which change directions are requested
     /// FORWARD_CHG, REVERSE_CHG, or FWD_AND_REV_CHG.
+    /// @param tsig_key_info pointer to the TSIGKeyInfo to assign to both
+    /// domains in the transaction.  This will cause the transaction to
+    /// use TSIG.  If the pointer is empty, TSIG will not be used.
+    void setupForIPv6Transaction(dhcp_ddns::NameChangeType change_type,
+                                 int change_mask,
+                                 const TSIGKeyInfoPtr& tsig_key_info =
+                                 TSIGKeyInfoPtr());
+
+    /// @brief Creates a transaction which requests an IPv6 DNS update.
+    ///
+    /// Convenience wrapper around the above method which accepts a string
+    /// key_name from which the TSIGKeyInfo is constructed.  Note the string
+    /// may not be blank.
+    ///
+    /// @param change_type selects the type of change requested, CHG_ADD or
+    /// CHG_REMOVE.
+    /// @param change_mask determines which change directions are requested
+    /// FORWARD_CHG, REVERSE_CHG, or FWD_AND_REV_CHG.
+    /// @param key_name value to use to create TSIG key, if blank TSIG will not
+    /// be used.
     void setupForIPv6Transaction(dhcp_ddns::NameChangeType change_type,
-                                 int change_mask);
+                                 int change_mask, const std::string& key_name);
+
 };
 
 
@@ -336,11 +391,38 @@ dhcp_ddns::NameChangeRequestPtr makeNcrFromString(const std::string& ncr_str);
 /// @brief Creates a DdnsDomain with the one server.
 ///
 /// @param zone_name zone name of the domain
-/// @param key_name TSIG key name of the TSIG key for this domain
+/// @param key_name TSIG key name of the TSIG key for this domain. It will
+/// create a TSIGKeyInfo based on the key_name and assign it to the domain.
 ///
 /// @throw Underlying methods may throw.
 extern DdnsDomainPtr makeDomain(const std::string& zone_name,
-                                const std::string& key_name = "");
+                                const std::string& key_name);
+
+/// @brief Creates a DdnsDomain with the one server.
+///
+/// @param zone_name zone name of the domain
+/// @param tsig_key_info pointer to the TSIGInfog key for this domain.
+/// Defaults to an empty pointer, meaning this domain has no key.
+///
+/// @throw Underlying methods may throw.
+extern DdnsDomainPtr makeDomain(const std::string& zone_name,
+                                const TSIGKeyInfoPtr&
+                                tsig_key_info = TSIGKeyInfoPtr());
+
+/// @brief Creates a TSIGKeyInfo
+///
+/// @param key_name name of the key
+/// @param secret key secret data as a base64 encoded string. If blank,
+/// then the secret value will be generated from key_name.
+/// @param algorithm algorithm to use. Defaults to MD5.
+/// @return a TSIGKeyInfoPtr for the newly created key.  If key_name is blank
+/// the pointer will be empty.
+/// @throw Underlying methods may throw.
+extern
+TSIGKeyInfoPtr makeTSIGKeyInfo(const std::string& key_name,
+                               const std::string& secret = "",
+                               const std::string& algorithm
+                               = TSIGKeyInfo::HMAC_MD5_STR);
 
 /// @brief Creates a DnsServerInfo and adds it to the given DdnsDomain.
 ///

+ 168 - 9
src/bin/d2/tests/nc_trans_unittests.cc

@@ -78,15 +78,11 @@ public:
     /// sendUpdate without incorporating exectution of the state model
     /// into the test.
     /// It sets the DNS status update and posts IO_COMPLETED_EVT as does
-    /// the normal callback, but rather than invoking runModel it stops
-    /// the IO service.  This allows tests to be constructed that consisted
-    /// of generating a DNS request and invoking sendUpdate to post it and
-    /// wait for response.
+    /// the normal callback.
     virtual void operator()(DNSClient::Status status) {
         if (use_stub_callback_) {
             setDnsUpdateStatus(status);
             postNextEvent(IO_COMPLETED_EVT);
-            getIOService()->stop();
         } else {
             // For tests which need to use the real callback.
             NameChangeTransaction::operator()(status);
@@ -288,13 +284,34 @@ public:
     virtual ~NameChangeTransactionTest() {
     }
 
+
     /// @brief  Instantiates a NameChangeStub test transaction
     /// The transaction is constructed around a predefined (i.e "canned")
     /// NameChangeRequest. The request has both forward and reverse DNS
     /// changes requested, and both forward and reverse domains are populated.
-    NameChangeStubPtr makeCannedTransaction() {
+    /// @param tsig_key_info pointer to the TSIGKeyInfo to use, defaults to
+    /// an empty pointer, in which case TSIG will not be used.
+    NameChangeStubPtr makeCannedTransaction(const TSIGKeyInfoPtr&
+                                            tsig_key_info = TSIGKeyInfoPtr()) {
         // Creates IPv4 remove request, forward, and reverse domains.
-        setupForIPv4Transaction(dhcp_ddns::CHG_ADD, FWD_AND_REV_CHG);
+        setupForIPv4Transaction(dhcp_ddns::CHG_ADD, FWD_AND_REV_CHG,
+                                tsig_key_info);
+
+        // Now create the test transaction as would occur in update manager.
+        // Instantiate the transaction as would be done by update manager.
+        return (NameChangeStubPtr(new NameChangeStub(io_service_, ncr_,
+                                  forward_domain_, reverse_domain_, cfg_mgr_)));
+    }
+
+    /// @brief  Instantiates a NameChangeStub test transaction
+    /// The transaction is constructed around a predefined (i.e "canned")
+    /// NameChangeRequest. The request has both forward and reverse DNS
+    /// changes requested, and both forward and reverse domains are populated.
+    /// @param key_name value to use to create TSIG key, if blank TSIG will not
+    /// be used.
+    NameChangeStubPtr makeCannedTransaction(const std::string& key_name) {
+        // Creates IPv4 remove request, forward, and reverse domains.
+        setupForIPv4Transaction(dhcp_ddns::CHG_ADD, FWD_AND_REV_CHG, key_name);
 
         // Now create the test transaction as would occur in update manager.
         // Instantiate the transaction as would be done by update manager.
@@ -374,8 +391,8 @@ TEST(NameChangeTransaction, construction) {
     DdnsDomainPtr empty_domain;
 
     ASSERT_NO_THROW(ncr = dhcp_ddns::NameChangeRequest::fromJSON(msg_str));
-    ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", "", servers)));
-    ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", "", servers)));
+    ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", servers)));
+    ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", servers)));
 
     // Verify that construction with a null IOServicePtr fails.
     // @todo Subject to change if multi-threading is implemented.
@@ -1021,6 +1038,148 @@ TEST_F(NameChangeTransactionTest, sendUpdate) {
     EXPECT_EQ("response.example.com.", zone->getName().toText());
 }
 
+/// @brief Tests that an unsigned response to a signed request is an error
+TEST_F(NameChangeTransactionTest, tsigUnsignedResponse) {
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction("key_one"));
+    ASSERT_NO_THROW(name_change->initDictionaries());
+    ASSERT_TRUE(name_change->selectFwdServer());
+
+    // Create a server and start it listening.
+    FauxServer server(*io_service_, *(name_change->getCurrentServer()));
+    server.receive (FauxServer::USE_RCODE, dns::Rcode::NOERROR());
+
+    // Do the udpate.
+    ASSERT_NO_FATAL_FAILURE(doOneExchange(name_change));
+
+    // Verify that next event is IO_COMPLETED_EVT and DNS status is
+    // INVALID_RESPONSE.
+    ASSERT_EQ(NameChangeTransaction::IO_COMPLETED_EVT,
+              name_change->getNextEvent());
+
+    ASSERT_EQ(DNSClient::INVALID_RESPONSE, name_change->getDnsUpdateStatus());
+
+    // When TSIG errors occur, only the message header (including Rcode) is
+    // unpacked.  In this case, it should be NOERROR but have no other
+    // information.
+    D2UpdateMessagePtr response = name_change->getDnsUpdateResponse();
+    ASSERT_TRUE(response);
+    ASSERT_EQ(dns::Rcode::NOERROR().getCode(), response->getRcode().getCode());
+    EXPECT_FALSE(response->getZone());
+}
+
+/// @brief Tests that a response signed with the wrong key is an error
+TEST_F(NameChangeTransactionTest, tsigInvalidResponse) {
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction("key_one"));
+    ASSERT_NO_THROW(name_change->initDictionaries());
+    ASSERT_TRUE(name_change->selectFwdServer());
+
+    // Create a server, tell it to sign responses with a "random" key,
+    // then start it listening.
+    FauxServer server(*io_service_, *(name_change->getCurrentServer()));
+    server.receive (FauxServer::INVALID_TSIG, dns::Rcode::NOERROR());
+
+    // Do the udpate.
+    ASSERT_NO_FATAL_FAILURE(doOneExchange(name_change));
+
+    // Verify that next event is IO_COMPLETED_EVT and DNS status is
+    // INVALID_RESPONSE.
+    ASSERT_EQ(NameChangeTransaction::IO_COMPLETED_EVT,
+              name_change->getNextEvent());
+
+    ASSERT_EQ(DNSClient::INVALID_RESPONSE, name_change->getDnsUpdateStatus());
+
+    // When TSIG errors occur, only the message header (including Rcode) is
+    // unpacked.  In this case, it should be NOERROR but have no other
+    // information.
+    D2UpdateMessagePtr response = name_change->getDnsUpdateResponse();
+    ASSERT_TRUE(response);
+    ASSERT_EQ(dns::Rcode::NOERROR().getCode(), response->getRcode().getCode());
+    EXPECT_FALSE(response->getZone());
+}
+
+/// @brief Tests that a signed response to an unsigned request is ok.
+/// Currently our policy is to accept a signed response to an unsigned request
+/// even though the spec says a server MUST not do that.
+TEST_F(NameChangeTransactionTest, tsigUnexpectedSignedResponse) {
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction());
+    ASSERT_NO_THROW(name_change->initDictionaries());
+    ASSERT_TRUE(name_change->selectFwdServer());
+
+    // Create a server, tell it to sign responses with a "random" key,
+    // then start it listening.
+    FauxServer server(*io_service_, *(name_change->getCurrentServer()));
+    server.receive (FauxServer::INVALID_TSIG, dns::Rcode::NOERROR());
+
+    // Perform an update without TSIG.
+    ASSERT_NO_FATAL_FAILURE(doOneExchange(name_change));
+
+    // Verify that next event is IO_COMPLETED_EVT and DNS status is SUCCESS.
+    ASSERT_EQ(NameChangeTransaction::IO_COMPLETED_EVT,
+              name_change->getNextEvent());
+
+    ASSERT_EQ(DNSClient::SUCCESS, name_change->getDnsUpdateStatus());
+
+    D2UpdateMessagePtr response = name_change->getDnsUpdateResponse();
+    ASSERT_TRUE(response);
+    ASSERT_EQ(dns::Rcode::NOERROR().getCode(), response->getRcode().getCode());
+    D2ZonePtr zone = response->getZone();
+    EXPECT_TRUE(zone);
+    EXPECT_EQ("response.example.com.", zone->getName().toText());
+}
+
+/// @brief Tests that a TSIG udpate succeeds when client and server both use
+/// the right key.  Runs the test for all supported algorithms.
+TEST_F(NameChangeTransactionTest, tsigAllValid) {
+    std::vector<std::string>algorithms;
+    algorithms.push_back(TSIGKeyInfo::HMAC_MD5_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA1_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA224_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA256_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA384_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA512_STR);
+
+    for (int i = 0; i < algorithms.size(); ++i) {
+        SCOPED_TRACE (algorithms[i]);
+        TSIGKeyInfoPtr key;
+        ASSERT_NO_THROW(key.reset(new TSIGKeyInfo("test_key",
+                                                  algorithms[i],
+                                                  "GWG/Xfbju4O2iXGqkSu4PQ==")));
+        NameChangeStubPtr name_change;
+        ASSERT_NO_THROW(name_change = makeCannedTransaction(key));
+        ASSERT_NO_THROW(name_change->initDictionaries());
+        ASSERT_TRUE(name_change->selectFwdServer());
+
+        // Create a server, set its TSIG key, and then start it listening.
+        FauxServer server(*io_service_, *(name_change->getCurrentServer()));
+        // Since we create a new server instance each time we need to tell
+        // it not reschedule receives automatically.
+        server.perpetual_receive_ = false;
+        server.setTSIGKey(key->getTSIGKey());
+        server.receive (FauxServer::USE_RCODE, dns::Rcode::NOERROR());
+
+        // Do the update.
+        ASSERT_NO_FATAL_FAILURE(doOneExchange(name_change));
+
+        // Verify that next event is IO_COMPLETED_EVT and DNS status is SUCCESS.
+        ASSERT_EQ(NameChangeTransaction::IO_COMPLETED_EVT,
+                  name_change->getNextEvent());
+
+        ASSERT_EQ(DNSClient::SUCCESS, name_change->getDnsUpdateStatus());
+
+        D2UpdateMessagePtr response = name_change->getDnsUpdateResponse();
+        ASSERT_TRUE(response);
+        ASSERT_EQ(dns::Rcode::NOERROR().getCode(),
+                  response->getRcode().getCode());
+        D2ZonePtr zone = response->getZone();
+        EXPECT_TRUE(zone);
+        EXPECT_EQ("response.example.com.", zone->getName().toText());
+    }
+}
+
+
 /// @brief Tests the prepNewRequest method
 TEST_F(NameChangeTransactionTest, prepNewRequest) {
     NameChangeStubPtr name_change;

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

@@ -51,10 +51,18 @@ pkglibexec_PROGRAMS = b10-dhcp4
 
 b10_dhcp4_SOURCES  = main.cc
 b10_dhcp4_SOURCES += ctrl_dhcp4_srv.cc ctrl_dhcp4_srv.h
-b10_dhcp4_SOURCES += config_parser.cc config_parser.h
+b10_dhcp4_SOURCES += json_config_parser.cc json_config_parser.h
 b10_dhcp4_SOURCES += dhcp4_log.cc dhcp4_log.h
 b10_dhcp4_SOURCES += dhcp4_srv.cc dhcp4_srv.h
 
+if CONFIG_BACKEND_BUNDY
+b10_dhcp4_SOURCES += bundy_controller.cc
+endif
+
+if CONFIG_BACKEND_JSON
+b10_dhcp4_SOURCES += kea_controller.cc
+endif
+
 nodist_b10_dhcp4_SOURCES = dhcp4_messages.h dhcp4_messages.cc
 EXTRA_DIST += dhcp4_messages.mes
 

+ 219 - 0
src/bin/dhcp4/bundy_controller.cc

@@ -0,0 +1,219 @@
+// Copyright (C) 2012-2014 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.
+
+#include <config.h>
+
+#include <asiolink/asiolink.h>
+#include <cc/data.h>
+#include <cc/session.h>
+#include <config/ccsession.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp4/json_config_parser.h>
+#include <dhcp4/ctrl_dhcp4_srv.h>
+#include <dhcp4/dhcp4_log.h>
+#include <dhcp4/spec_config.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/dhcp_config_parser.h>
+#include <exceptions/exceptions.h>
+#include <hooks/hooks_manager.h>
+#include <util/buffer.h>
+
+#include <cassert>
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <vector>
+
+using namespace isc::asiolink;
+using namespace isc::cc;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::hooks;
+using namespace isc::log;
+using namespace isc::util;
+using namespace std;
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Helper session object that represents raw connection to msgq.
+isc::cc::Session* cc_session_ = NULL;
+
+/// @brief Session that receives configuration and commands
+isc::config::ModuleCCSession* config_session_ = NULL;
+
+/// @brief A dummy configuration handler that always returns success.
+///
+/// This configuration handler does not perform configuration
+/// parsing and always returns success. A dummy handler should
+/// be installed using \ref isc::config::ModuleCCSession ctor
+/// to get the initial configuration. This initial configuration
+/// comprises values for only those elements that were modified
+/// the previous session. The \ref dhcp4ConfigHandler can't be
+/// used to parse the initial configuration because it needs the
+/// full configuration to satisfy dependencies between the
+/// various configuration values. Installing the dummy handler
+/// that guarantees to return success causes initial configuration
+/// to be stored for the session being created and that it can
+/// be later accessed with
+/// \ref isc::config::ConfigData::getFullConfig().
+///
+/// @param new_config new configuration.
+///
+/// @return success configuration status.
+ConstElementPtr
+dhcp4StubConfigHandler(ConstElementPtr) {
+    // This configuration handler is intended to be used only
+    // when the initial configuration comes in. To receive this
+    // configuration a pointer to this handler must be passed
+    // using ModuleCCSession constructor. This constructor will
+    // invoke the handler and will store the configuration for
+    // the configuration session when the handler returns success.
+    // Since this configuration is partial we just pretend to
+    // parse it and always return success. The function that
+    // initiates the session must get the configuration on its
+    // own using getFullConfig.
+    return (isc::config::createAnswer(0, "Configuration accepted."));
+}
+
+ConstElementPtr
+bundyConfigHandler(ConstElementPtr new_config) {
+    if (!ControlledDhcpv4Srv::getInstance() || !config_session_) {
+        // That should never happen as we install config_handler
+        // after we instantiate the server.
+        ConstElementPtr answer =
+            isc::config::createAnswer(1, "Configuration rejected,"
+                                      " server is during startup/shutdown phase.");
+        return (answer);
+    }
+
+    // The configuration passed to this handler function is partial.
+    // In other words, it just includes the values being modified.
+    // In the same time, there are dependencies between various
+    // DHCP configuration parsers. For example: the option value can
+    // be set if the definition of this option is set. If someone removes
+    // an existing option definition then the partial configuration that
+    // removes that definition is triggered while a relevant option value
+    // may remain configured. This eventually results in the DHCP server
+    // configuration being in the inconsistent state.
+    // In order to work around this problem we need to merge the new
+    // configuration with the existing (full) configuration.
+
+    // Let's create a new object that will hold the merged configuration.
+    boost::shared_ptr<MapElement> merged_config(new MapElement());
+    // Let's get the existing configuration.
+    ConstElementPtr full_config = config_session_->getFullConfig();
+    // The full_config and merged_config should be always non-NULL
+    // but to provide some level of exception safety we check that they
+    // really are (in case we go out of memory).
+    if (full_config && merged_config) {
+        merged_config->setValue(full_config->mapValue());
+
+        // Merge an existing and new configuration.
+        isc::data::merge(merged_config, new_config);
+        LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND, DHCP4_CONFIG_UPDATE)
+            .arg(full_config->str());
+    }
+
+    // Configure the server.
+    return (ControlledDhcpv4Srv::processConfig(merged_config));
+}
+
+
+
+
+void ControlledDhcpv4Srv::init(const std::string& /*config_file*/) {
+
+    string specfile;
+    if (getenv("B10_FROM_BUILD")) {
+        specfile = string(getenv("B10_FROM_BUILD")) +
+            "/src/bin/dhcp4/dhcp4.spec";
+    } else {
+        specfile = string(DHCP4_SPECFILE_LOCATION);
+    }
+
+    /// @todo: Check if session is not established already. Throw, if it is.
+
+    LOG_DEBUG(dhcp4_logger, DBG_DHCP4_START, DHCP4_CCSESSION_STARTING)
+              .arg(specfile);
+    cc_session_ = new Session(io_service_.get_io_service());
+    // Create a session with the dummy configuration handler.
+    // Dumy configuration handler is internally invoked by the
+    // constructor and on success the constructor updates
+    // the current session with the configuration that had been
+    // committed in the previous session. If we did not install
+    // the dummy handler, the previous configuration would have
+    // been lost.
+    config_session_ = new ModuleCCSession(specfile, *cc_session_,
+                                          dhcp4StubConfigHandler,
+                                          processCommand, false);
+    config_session_->start();
+
+    // We initially create ModuleCCSession() without configHandler, as
+    // the session module is too eager to send partial configuration.
+    // We want to get the full configuration, so we explicitly call
+    // getFullConfig() and then pass it to our configHandler.
+    config_session_->setConfigHandler(bundyConfigHandler);
+
+    try {
+        configureDhcp4Server(*this, config_session_->getFullConfig());
+
+        // Server will start DDNS communications if its enabled.
+        server_->startD2();
+
+        // Configuration may disable or enable interfaces so we have to
+        // reopen sockets according to new configuration.
+        openActiveSockets(getPort(), useBroadcast());
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL).arg(ex.what());
+
+    }
+
+    /// Integrate the asynchronous I/O model of BIND 10 configuration
+    /// control with the "select" model of the DHCP server.  This is
+    /// fully explained in \ref dhcpv4Session.
+    int ctrl_socket = cc_session_->getSocketDesc();
+    LOG_DEBUG(dhcp4_logger, DBG_DHCP4_START, DHCP4_CCSESSION_STARTED)
+              .arg(ctrl_socket);
+    IfaceMgr::instance().addExternalSocket(ctrl_socket, sessionReader);
+}
+
+
+void ControlledDhcpv4Srv::cleanup() {
+    if (config_session_) {
+        delete config_session_;
+        config_session_ = NULL;
+    }
+    if (cc_session_) {
+
+        int ctrl_socket = cc_session_->getSocketDesc();
+        cc_session_->disconnect();
+
+        IfaceMgr::instance().deleteExternalSocket(ctrl_socket);
+        delete cc_session_;
+        cc_session_ = NULL;
+    }
+}
+
+void
+Daemon::loggerInit(const char* log_name, bool verbose) {
+    isc::log::initLogger(log_name,
+                         (verbose ? isc::log::DEBUG : isc::log::INFO),
+                         isc::log::MAX_DEBUG_LEVEL, NULL, true);
+}
+
+};
+};

+ 112 - 213
src/bin/dhcp4/ctrl_dhcp4_srv.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014 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
@@ -13,36 +13,14 @@
 // PERFORMANCE OF THIS SOFTWARE.
 
 #include <config.h>
-
-#include <asiolink/asiolink.h>
 #include <cc/data.h>
-#include <cc/session.h>
-#include <config/ccsession.h>
-#include <dhcp/iface_mgr.h>
-#include <dhcp4/config_parser.h>
 #include <dhcp4/ctrl_dhcp4_srv.h>
 #include <dhcp4/dhcp4_log.h>
-#include <dhcp4/spec_config.h>
-#include <dhcpsrv/cfgmgr.h>
-#include <dhcpsrv/dhcp_config_parser.h>
-#include <exceptions/exceptions.h>
 #include <hooks/hooks_manager.h>
-#include <util/buffer.h>
-
-#include <cassert>
-#include <iostream>
-#include <sstream>
-#include <string>
-#include <vector>
+#include <dhcp4/json_config_parser.h>
 
-using namespace isc::asiolink;
-using namespace isc::cc;
-using namespace isc::config;
 using namespace isc::data;
-using namespace isc::dhcp;
 using namespace isc::hooks;
-using namespace isc::log;
-using namespace isc::util;
 using namespace std;
 
 namespace isc {
@@ -51,217 +29,141 @@ namespace dhcp {
 ControlledDhcpv4Srv* ControlledDhcpv4Srv::server_ = NULL;
 
 ConstElementPtr
-ControlledDhcpv4Srv::dhcp4StubConfigHandler(ConstElementPtr) {
-    // This configuration handler is intended to be used only
-    // when the initial configuration comes in. To receive this
-    // configuration a pointer to this handler must be passed
-    // using ModuleCCSession constructor. This constructor will
-    // invoke the handler and will store the configuration for
-    // the configuration session when the handler returns success.
-    // Since this configuration is partial we just pretend to
-    // parse it and always return success. The function that
-    // initiates the session must get the configuration on its
-    // own using getFullConfig.
-    return (isc::config::createAnswer(0, "Configuration accepted."));
+ControlledDhcpv4Srv::commandShutdownHandler(const string&, ConstElementPtr) {
+    if (ControlledDhcpv4Srv::getInstance()) {
+        ControlledDhcpv4Srv::getInstance()->shutdown();
+    } else {
+        LOG_WARN(dhcp4_logger, DHCP4_NOT_RUNNING);
+        ConstElementPtr answer = isc::config::createAnswer(1,
+                                              "Shutdown failure.");
+        return (answer);
+    }
+    ConstElementPtr answer = isc::config::createAnswer(0, "Shutting down.");
+    return (answer);
 }
 
 ConstElementPtr
-ControlledDhcpv4Srv::dhcp4ConfigHandler(ConstElementPtr new_config) {
-    if (!server_ || !server_->config_session_) {
-        // That should never happen as we install config_handler
-        // after we instantiate the server.
-        ConstElementPtr answer =
-            isc::config::createAnswer(1, "Configuration rejected,"
-                                      " server is during startup/shutdown phase.");
+ControlledDhcpv4Srv::commandLibReloadHandler(const string&, ConstElementPtr) {
+
+    /// @todo delete any stored CalloutHandles referring to the old libraries
+    /// Get list of currently loaded libraries and reload them.
+    vector<string> loaded = HooksManager::getLibraryNames();
+    bool status = HooksManager::loadLibraries(loaded);
+    if (!status) {
+        LOG_ERROR(dhcp4_logger, DHCP4_HOOKS_LIBS_RELOAD_FAIL);
+        ConstElementPtr answer = isc::config::createAnswer(1,
+                                 "Failed to reload hooks libraries.");
         return (answer);
     }
+    ConstElementPtr answer = isc::config::createAnswer(0,
+                             "Hooks libraries successfully reloaded.");
+    return (answer);
+}
 
-    // The configuration passed to this handler function is partial.
-    // In other words, it just includes the values being modified.
-    // In the same time, there are dependencies between various
-    // DHCP configuration parsers. For example: the option value can
-    // be set if the definition of this option is set. If someone removes
-    // an existing option definition then the partial configuration that
-    // removes that definition is triggered while a relevant option value
-    // may remain configured. This eventually results in the DHCP server
-    // configuration being in the inconsistent state.
-    // In order to work around this problem we need to merge the new
-    // configuration with the existing (full) configuration.
-
-    // Let's create a new object that will hold the merged configuration.
-    boost::shared_ptr<MapElement> merged_config(new MapElement());
-    // Let's get the existing configuration.
-    ConstElementPtr full_config = server_->config_session_->getFullConfig();
-    // The full_config and merged_config should be always non-NULL
-    // but to provide some level of exception safety we check that they
-    // really are (in case we go out of memory).
-    if (full_config && merged_config) {
-        merged_config->setValue(full_config->mapValue());
-
-        // Merge an existing and new configuration.
-        isc::data::merge(merged_config, new_config);
-        LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND, DHCP4_CONFIG_UPDATE)
-            .arg(full_config->str());
-    }
+ConstElementPtr
+ControlledDhcpv4Srv::commandConfigReloadHandler(const string&,
+                                                ConstElementPtr args) {
+    return (processConfig(args));
+}
 
-    // Configure the server.
-    ConstElementPtr answer = configureDhcp4Server(*server_, merged_config);
+ConstElementPtr
+ControlledDhcpv4Srv::processCommand(const string& command,
+                                    ConstElementPtr args) {
+    LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND, DHCP4_COMMAND_RECEIVED)
+              .arg(command).arg(args->str());
 
-    // Check that configuration was successful. If not, do not reopen sockets.
-    int rcode = 0;
-    parseAnswer(rcode, answer);
-    if (rcode != 0) {
-        return (answer);
-    }
+    ControlledDhcpv4Srv* srv = ControlledDhcpv4Srv::getInstance();
 
-    // Server will start DDNS communications if its enabled.
-    try {
-        server_->startD2();
-    } catch (const std::exception& ex) {
-        std::ostringstream err;
-        err << "error starting DHCP_DDNS client "
-                " after server reconfiguration: " << ex.what();
-        return (isc::config::createAnswer(1, err.str()));
+    if (!srv) {
+        ConstElementPtr no_srv = isc::config::createAnswer(1,
+          "Server object not initialized, so can't process command '" +
+          command + "', arguments: '" + args->str() + "'.");
+        return (no_srv);
     }
 
-    // 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);
-}
+        if (command == "shutdown") {
+            return (srv->commandShutdownHandler(command, args));
 
-ConstElementPtr
-ControlledDhcpv4Srv::dhcp4CommandHandler(const string& command, ConstElementPtr args) {
-    LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND, DHCP4_COMMAND_RECEIVED)
-              .arg(command).arg(args->str());
+        } else if (command == "libreload") {
+            return (srv->commandLibReloadHandler(command, args));
 
-    if (command == "shutdown") {
-        if (ControlledDhcpv4Srv::server_) {
-            ControlledDhcpv4Srv::server_->shutdown();
-        } else {
-            LOG_WARN(dhcp4_logger, DHCP4_NOT_RUNNING);
-            ConstElementPtr answer = isc::config::createAnswer(1,
-                                     "Shutdown failure.");
-            return (answer);
-        }
-        ConstElementPtr answer = isc::config::createAnswer(0,
-                                 "Shutting down.");
-        return (answer);
+        } else if (command == "config-reload") {
+            return (srv->commandConfigReloadHandler(command, args));
 
-    } else if (command == "libreload") {
-        // TODO delete any stored CalloutHandles referring to the old libraries
-        // Get list of currently loaded libraries and reload them.
-        vector<string> loaded = HooksManager::getLibraryNames();
-        bool status = HooksManager::loadLibraries(loaded);
-        if (!status) {
-            LOG_ERROR(dhcp4_logger, DHCP4_HOOKS_LIBS_RELOAD_FAIL);
-            ConstElementPtr answer = isc::config::createAnswer(1,
-                                     "Failed to reload hooks libraries.");
-            return (answer);
         }
-        ConstElementPtr answer = isc::config::createAnswer(0,
-                                 "Hooks libraries successfully reloaded.");
+        ConstElementPtr answer = isc::config::createAnswer(1,
+                                 "Unrecognized command:" + command);
         return (answer);
+    } catch (const Exception& ex) {
+        return (isc::config::createAnswer(1, "Error while processing command '"
+                                          + command + "':" + ex.what() +
+                                          ", params: '" + args->str() + "'"));
     }
-
-    ConstElementPtr answer = isc::config::createAnswer(1,
-                             "Unrecognized command.");
-
-    return (answer);
 }
 
-void ControlledDhcpv4Srv::sessionReader(void) {
-    // Process one asio event. If there are more events, iface_mgr will call
-    // this callback more than once.
-    if (server_) {
-        server_->io_service_.run_one();
-    }
-}
+isc::data::ConstElementPtr
+ControlledDhcpv4Srv::processConfig(isc::data::ConstElementPtr config) {
 
-void ControlledDhcpv4Srv::establishSession() {
+    LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND, DHCP4_CONFIG_RECEIVED)
+              .arg(config->str());
 
-    string specfile;
-    if (getenv("B10_FROM_BUILD")) {
-        specfile = string(getenv("B10_FROM_BUILD")) +
-            "/src/bin/dhcp4/dhcp4.spec";
-    } else {
-        specfile = string(DHCP4_SPECFILE_LOCATION);
-    }
+    ControlledDhcpv4Srv* srv = ControlledDhcpv4Srv::getInstance();
 
-    /// @todo: Check if session is not established already. Throw, if it is.
+    // Single stream instance used in all error clauses
+    std::ostringstream err;
 
-    LOG_DEBUG(dhcp4_logger, DBG_DHCP4_START, DHCP4_CCSESSION_STARTING)
-              .arg(specfile);
-    cc_session_ = new Session(io_service_.get_io_service());
-    // Create a session with the dummy configuration handler.
-    // Dumy configuration handler is internally invoked by the
-    // constructor and on success the constructor updates
-    // the current session with the configuration that had been
-    // committed in the previous session. If we did not install
-    // the dummy handler, the previous configuration would have
-    // been lost.
-    config_session_ = new ModuleCCSession(specfile, *cc_session_,
-                                          dhcp4StubConfigHandler,
-                                          dhcp4CommandHandler, false);
-    config_session_->start();
+    if (!srv) {
+        err << "Server object not initialized, can't process config.";
+        return (isc::config::createAnswer(1, err.str()));
+    }
+    
+    ConstElementPtr answer = configureDhcp4Server(*srv, config);
 
-    // We initially create ModuleCCSession() without configHandler, as
-    // the session module is too eager to send partial configuration.
-    // We want to get the full configuration, so we explicitly call
-    // getFullConfig() and then pass it to our configHandler.
-    config_session_->setConfigHandler(dhcp4ConfigHandler);
 
+    // Check that configuration was successful. If not, do not reopen sockets
+    // and don't bother with DDNS stuff.
     try {
-        configureDhcp4Server(*this, config_session_->getFullConfig());
-
-        // Server will start DDNS communications if its enabled.
-        server_->startD2();
-
-        // Configuration may disable or enable interfaces so we have to
-        // reopen sockets according to new configuration.
-        openActiveSockets(getPort(), useBroadcast());
-
+        int rcode = 0;
+        isc::config::parseAnswer(rcode, answer);
+        if (rcode != 0) {
+            return (answer);
+        }
     } catch (const std::exception& ex) {
-        LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL).arg(ex.what());
-
+        err << "Failed to process configuration:" << ex.what();
+        return (isc::config::createAnswer(1, err.str()));
     }
 
-    /// Integrate the asynchronous I/O model of BIND 10 configuration
-    /// control with the "select" model of the DHCP server.  This is
-    /// fully explained in \ref dhcpv4Session.
-    int ctrl_socket = cc_session_->getSocketDesc();
-    LOG_DEBUG(dhcp4_logger, DBG_DHCP4_START, DHCP4_CCSESSION_STARTED)
-              .arg(ctrl_socket);
-    IfaceMgr::instance().addExternalSocket(ctrl_socket, sessionReader);
-}
-
-void ControlledDhcpv4Srv::disconnectSession() {
-    if (config_session_) {
-        delete config_session_;
-        config_session_ = NULL;
+    // Server will start DDNS communications if its enabled.
+    try {
+        srv->startD2();
+    } catch (const std::exception& ex) {
+        err << "Error starting DHCP_DDNS client after server reconfiguration: "
+            << ex.what();
+        return (isc::config::createAnswer(1, err.str()));
     }
-    if (cc_session_) {
 
-        int ctrl_socket = cc_session_->getSocketDesc();
-        cc_session_->disconnect();
-
-        IfaceMgr::instance().deleteExternalSocket(ctrl_socket);
-        delete cc_session_;
-        cc_session_ = NULL;
+    // 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 whoever called this
+    // method. Instead, catch an exception and create appropriate answer.
+    try {
+        srv->openActiveSockets(srv->getPort(), getInstance()->useBroadcast());
+    } catch (std::exception& ex) {
+        err << "failed to open sockets after server reconfiguration: "
+            << ex.what();
+        answer = isc::config::createAnswer(1, err.str());
     }
+    return (answer);
 }
 
 ControlledDhcpv4Srv::ControlledDhcpv4Srv(uint16_t port /*= DHCP4_SERVER_PORT*/)
-    :Dhcpv4Srv(port), cc_session_(NULL), config_session_(NULL) {
-    server_ = this; // remember this instance for use in callback
+    :Dhcpv4Srv(port) {
+    if (getInstance()) {
+        isc_throw(InvalidOperation,
+                  "There is another Dhcpv4Srv instance already.");
+    }
+    server_ = this; // remember this instance for later use in handlers
 }
 
 void ControlledDhcpv4Srv::shutdown() {
@@ -270,22 +172,19 @@ void ControlledDhcpv4Srv::shutdown() {
 }
 
 ControlledDhcpv4Srv::~ControlledDhcpv4Srv() {
-    disconnectSession();
-
-    server_ = NULL; // forget this instance. There should be no callback anymore
-                    // at this stage anyway.
+    cleanup();
+    
+    server_ = NULL; // forget this instance. Noone should call any handlers at
+                    // this stage.
 }
 
-isc::data::ConstElementPtr
-ControlledDhcpv4Srv::execDhcpv4ServerCommand(const std::string& command_id,
-                                             isc::data::ConstElementPtr args) {
-    try {
-        return (dhcp4CommandHandler(command_id, args));
-    } catch (const Exception& ex) {
-        ConstElementPtr answer = isc::config::createAnswer(1, ex.what());
-        return (answer);
+void ControlledDhcpv4Srv::sessionReader(void) {
+    // Process one asio event. If there are more events, iface_mgr will call
+    // this callback more than once.
+    if (getInstance()) {
+        getInstance()->io_service_.run_one();
     }
 }
 
-};
-};
+}; // end of isc::dhcp namespace
+}; // end of isc namespace

+ 94 - 65
src/bin/dhcp4/ctrl_dhcp4_srv.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2013  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014  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
@@ -26,12 +26,10 @@ namespace dhcp {
 
 /// @brief Controlled version of the DHCPv4 server
 ///
-/// This is a class that is responsible for establishing connection
-/// with msqg (receving commands and configuration). This is an extended
-/// version of Dhcpv4Srv class that is purely a DHCPv4 server, without
-/// external control. ControlledDhcpv4Srv should be used in typical BIND10
-/// (i.e. featuring msgq) environment, while Dhcpv4Srv should be used in
-/// embedded environments.
+/// This is a class that is responsible for DHCPv4 server being controllable.
+/// It does various things, depending on the configuration backend.
+/// For Bundy backend it establishes a connection with msqg and later receives
+/// commands over it. For Kea backend, it reads configuration file from disk.
 ///
 /// For detailed explanation or relations between main(), ControlledDhcpv4Srv,
 /// Dhcpv4Srv and other classes, see \ref dhcpv4Session.
@@ -46,44 +44,57 @@ public:
     /// @brief Destructor.
     ~ControlledDhcpv4Srv();
 
-    /// @brief Establishes msgq session.
+    /// @brief Initializes the server.
     ///
-    /// Creates session that will be used to receive commands and updated
-    /// configuration from cfgmgr (or indirectly from user via bindctl).
-    void establishSession();
+    /// Depending on the configuration backend, it establishes msgq session,
+    /// reads the JSON file from disk or may perform any other setup
+    /// operation. For specific details, see actual implementation in
+    /// *_backend.cc
+    ///
+    /// This method may throw if initialization fails. Exception types may be
+    /// specific to used configuration backend.
+    void init(const std::string& config_file);
 
-    /// @brief Terminates existing msgq session.
+    /// @brief Performs cleanup, immediately before termination
     ///
-    /// This method terminates existing session with msgq. After calling
+    /// This method performs final clean up, just before the Dhcpv4Srv object
+    /// is destroyed. The actual behavior is backend dependent. For Bundy
+    /// backend, it terminates existing session with msgq. After calling
     /// it, no further messages over msgq (commands or configuration updates)
-    /// may be received.
+    /// may be received. For JSON backend, it is no-op.
     ///
-    /// It is ok to call this method when session is disconnected already.
-    void disconnectSession();
+    /// For specific details, see actual implementation in *_backend.cc
+    void cleanup();
 
     /// @brief Initiates shutdown procedure for the whole DHCPv4 server.
     void shutdown();
 
-    /// @brief Session callback, processes received commands.
+    /// @brief Command processor
+    ///
+    /// This method is uniform for all config backends. It processes received
+    /// command (as a string + JSON arguments). Internally, it's just a
+    /// wrapper that calls process*Command() methods and catches exceptions
+    /// in them.
+    ///
+    /// Currently supported commands are:
+    /// - shutdown
+    /// - libreload
+    /// - config-reload
+    ///
+    /// @note It never throws.
     ///
     /// @param command Text represenation of the command (e.g. "shutdown")
     /// @param args Optional parameters
-    /// @param command text represenation of the command (e.g. "shutdown")
-    /// @param args optional parameters
     ///
     /// @return status of the command
     static isc::data::ConstElementPtr
-    execDhcpv4ServerCommand(const std::string& command,
-                            isc::data::ConstElementPtr args);
+    processCommand(const std::string& command, isc::data::ConstElementPtr args);
 
-protected:
-    /// @brief Static pointer to the sole instance of the DHCP server.
+    /// @brief Configuration processor
     ///
-    /// This is required for config and command handlers to gain access to
-    /// the server
-    static ControlledDhcpv4Srv* server_;
-
-    /// @brief A callback for handling incoming configuration updates.
+    /// This is a method for handling incoming configuration updates.
+    /// This method should be called by all configuration backends when the
+    /// server is starting up or when configuration has changed.
     ///
     /// As pointer to this method is used a callback in ASIO used in
     /// ModuleCCSession, it has to be static.
@@ -92,54 +103,72 @@ protected:
     ///
     /// @return status of the config update
     static isc::data::ConstElementPtr
-    dhcp4ConfigHandler(isc::data::ConstElementPtr new_config);
-
-    /// @brief A dummy configuration handler that always returns success.
-    ///
-    /// This configuration handler does not perform configuration
-    /// parsing and always returns success. A dummy handler should
-    /// be installed using \ref isc::config::ModuleCCSession ctor
-    /// to get the initial configuration. This initial configuration
-    /// comprises values for only those elements that were modified
-    /// the previous session. The \ref dhcp4ConfigHandler can't be
-    /// used to parse the initial configuration because it needs the
-    /// full configuration to satisfy dependencies between the
-    /// various configuration values. Installing the dummy handler
-    /// that guarantees to return success causes initial configuration
-    /// to be stored for the session being created and that it can
-    /// be later accessed with
-    /// \ref isc::config::ConfigData::getFullConfig().
-    ///
-    /// @param new_config new configuration.
-    ///
-    /// @return success configuration status.
-    static isc::data::ConstElementPtr
-    dhcp4StubConfigHandler(isc::data::ConstElementPtr new_config);
+    processConfig(isc::data::ConstElementPtr new_config);
 
-    /// @brief A callback for handling incoming commands.
+    /// @brief Returns pointer to the sole instance of Dhcpv4Srv
     ///
-    /// @param command textual representation of the command
-    /// @param args parameters of the command
+    /// @return server instance (may return NULL, if called before server is spawned)
+    static ControlledDhcpv4Srv* getInstance() {
+        return (server_);
+    }
+
+
+protected:
+    /// @brief Static pointer to the sole instance of the DHCP server.
     ///
-    /// @return status of the processed command
-    static isc::data::ConstElementPtr
-    dhcp4CommandHandler(const std::string& command, isc::data::ConstElementPtr args);
+    /// This is required for config and command handlers to gain access to
+    /// the server
+    static ControlledDhcpv4Srv* server_;
 
-    /// @brief Callback that will be called from iface_mgr when command/config arrives.
+    /// @brief Callback that will be called from iface_mgr when data
+    /// is received over control socket.
     ///
-    /// This static callback method is called from IfaceMgr::receive4() method,
-    /// when there is a new command or configuration sent over msgq.
+    /// This static callback method is called from IfaceMgr::receive6() method,
+    /// when there is a new command or configuration sent over control socket
+    /// (that was sent from msgq if backend is Bundy, or some yet unspecified
+    /// sender if the backend is JSON file).
     static void sessionReader(void);
 
-
     /// @brief IOService object, used for all ASIO operations.
     isc::asiolink::IOService io_service_;
 
-    /// @brief Helper session object that represents raw connection to msgq.
-    isc::cc::Session* cc_session_;
+    /// @brief Handler for processing 'shutdown' command
+    ///
+    /// This handler processes shutdown command, which initializes shutdown
+    /// procedure.
+    /// @param command (parameter ignored)
+    /// @param args (parameter ignored)
+    ///
+    /// @return status of the command
+    isc::data::ConstElementPtr
+    commandShutdownHandler(const std::string& command,
+                           isc::data::ConstElementPtr args);
 
-    /// @brief Session that receives configuration and commands
-    isc::config::ModuleCCSession* config_session_;
+    /// @brief Handler for processing 'libreload' command
+    ///
+    /// This handler processes libreload command, which unloads all hook
+    /// libraries and reloads them.
+    ///
+    /// @param command (parameter ignored)
+    /// @param args (parameter ignored)
+    ///
+    /// @return status of the command
+    isc::data::ConstElementPtr
+    commandLibReloadHandler(const std::string& command,
+                            isc::data::ConstElementPtr args);
+
+    /// @brief Handler for processing 'config-reload' command
+    ///
+    /// This handler processes config-reload command, which processes
+    /// configuration specified in args parameter.
+    ///
+    /// @param command (parameter ignored)
+    /// @param args configuration to be processed
+    ///
+    /// @return status of the command
+    isc::data::ConstElementPtr
+    commandConfigReloadHandler(const std::string& command,
+                               isc::data::ConstElementPtr args);
 };
 
 }; // namespace isc::dhcp

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

@@ -191,6 +191,46 @@ being passed in isc::dhcp::Dhcpv4Srv::selectSubnet() to isc::dhcp::CfgMgr::getSu
 Currently this capability is usable, but the number of scenarios it supports is
 limited.
 
+ @section dhcpv4ConfigBackend Configuration backend for DHCPv4
+
+There are many theoretical ways in which server configuration can be stored. Kea 0.8 and
+earlier versions used BIND10 framework and its internal storage for DHCPv6 server configuration.
+The legacy ISC-DHCP implementation uses flat files. Configuration stored in JSON files is
+becoming more and more popular among various projects. There are unofficial patches for
+ISC-DHCP that keep parts of the configuration in LDAP. It was also suggested that in some
+cases it would be convenient to keep configuration in XML files.
+
+Kea 0.9 introduces configuration backends that are switchable during compilation phase.
+There is a new parameter for configure script: --with-kea-config. It currently supports
+two values: BUNDY and JSON.
+
+BUNDY (which is the default value as of May 2014) means that Kea4 is linked with the
+Bundy (former BIND10) configuration backend that connects to the BIND10 framework and in general works
+exactly the same as Kea 0.8 and earlier versions. The benefits of that backend are uniform
+integration with Bundy/BIND10 framework, easy on-line reconfiguration using bindctl, available
+RESTful API. On the other hand, it requires the whole heavy Bundy framework that requires
+Python3 to be present. That framework is going away with the release of Kea 0.9.
+
+JSON is a new configuration backend that causes Kea to read JSON configuration file from
+disk. It does not require any framework and thus is considered more lightweight. It will
+allow dynamic on-line reconfiguration, but will lack remote capabilities (i.e. no RESTful
+API). This configuration backend is expected to be the default for upcoming Kea 0.9. It
+requires <tt> -c config-file </tt> command-line option.
+
+Internally, configuration backends are implemented as different implementations of the
+isc::dhcp::ControlledDhcpv4Srv class, stored in {kea,bundy}_controller.cc files. Depending on
+the choice made by ./configure script, only one of those files is compiled and linked.
+There are backend specific tests in src/bin/dhcp4/tests/{kea,bundy}_controller_unittest.cc.
+Only tests specific to selected backend are linked and executed during make distcheck.
+
+While it is unlikely that ISC will support more than one backend at any given time, there
+are several aspects that make that approach appealing in the long term. First, having
+two backends is essential during transition time, where both old and new backend is used.
+Second, there are external organizations that develop and presumably maintain LDAP backend
+for ISC-DHCP. Is at least possible that similar will happen for Kea. Finally, if we ever
+extend the isc::dhcp::CfgMgr with configuration export, this approach could be used as
+a migration tool.
+
 @section dhcpv4Other Other DHCPv4 topics
 
  For hooks API support in DHCPv4, see @ref dhcpv4Hooks.

+ 5 - 7
src/bin/dhcp4/dhcp4_messages.mes

@@ -44,6 +44,10 @@ name was malformed or due to internal server error.
 A debug message listing the command (and possible arguments) received
 from the BIND 10 control system by the DHCPv4 server.
 
+% DHCP4_CONFIG_RECEIVED received configuration %1
+A debug message listing the configuration received by the DHCPv4 server.
+The source of that configuration depends on used configuration backend.
+
 % DHCP4_CONFIG_COMPLETE DHCPv4 server has completed configuration: %1
 This is an informational message announcing the successful processing of a
 new configuration. It is output during server startup, and when an updated
@@ -339,17 +343,11 @@ core component within the DHCPv4 server (the Dhcpv4 server object)
 has failed.  As a result, the server will exit.  The reason for the
 failure is given within the message.
 
-% DHCP4_STANDALONE skipping message queue, running standalone
-This is a debug message indicating that the DHCPv4 server is running in
-standalone mode, not connected to the message queue.  Standalone mode
-is only useful during program development, and should not be used in a
-production environment.
-
 % DHCP4_STARTING server starting
 This informational message indicates that the DHCPv4 server has
 processed any command-line switches and is starting.
 
-% DHCP4_START_INFO pid: %1, port: %2, verbose: %3, standalone: %4
+% DHCP4_START_INFO pid: %1, port: %2, verbose: %3
 This is a debug message issued during the DHCPv4 server startup.
 It lists some information about the parameters with which the server
 is running.

+ 3 - 2
src/bin/dhcp4/dhcp4_srv.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2014 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
@@ -26,6 +26,7 @@
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/alloc_engine.h>
 #include <hooks/callout_handle.h>
+#include <dhcpsrv/daemon.h>
 
 #include <boost/noncopyable.hpp>
 
@@ -57,7 +58,7 @@ public:
 ///
 /// For detailed explanation or relations between main(), ControlledDhcpv4Srv,
 /// Dhcpv4Srv and other classes, see \ref dhcpv4Session.
-class Dhcpv4Srv : public boost::noncopyable {
+class Dhcpv4Srv : public Daemon {
 
 public:
 

+ 1 - 1
src/bin/dhcp4/config_parser.cc

@@ -17,7 +17,7 @@
 #include <dhcp/libdhcp++.h>
 #include <dhcp/option_definition.h>
 #include <dhcpsrv/cfgmgr.h>
-#include <dhcp4/config_parser.h>
+#include <dhcp4/json_config_parser.h>
 #include <dhcpsrv/dbaccess_parser.h>
 #include <dhcpsrv/dhcp_parsers.h>
 #include <dhcpsrv/option_space_container.h>

src/bin/dhcp4/config_parser.h → src/bin/dhcp4/json_config_parser.h


+ 124 - 0
src/bin/dhcp4/kea_controller.cc

@@ -0,0 +1,124 @@
+// Copyright (C) 2014 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.
+
+#include <config.h>
+
+#include <dhcp4/json_config_parser.h>
+#include <dhcp4/ctrl_dhcp4_srv.h>
+#include <dhcp4/dhcp4_log.h>
+#include <exceptions/exceptions.h>
+
+#include <string>
+
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace std;
+
+namespace isc {
+namespace dhcp {
+
+void
+ControlledDhcpv4Srv::init(const std::string& file_name) {
+    // This is a configuration backend implementation that reads the
+    // configuration from a JSON file.
+
+    isc::data::ConstElementPtr json;
+    isc::data::ConstElementPtr dhcp4;
+    isc::data::ConstElementPtr result;
+
+    // Basic sanity check: file name must not be empty.
+    try {
+        if (file_name.empty()) {
+            // Basic sanity check: file name must not be empty.
+            isc_throw(BadValue, "JSON configuration file not specified. Please "
+                      "use -c command line option.");
+        }
+
+        // Read contents of the file and parse it as JSON
+        json = isc::data::Element::fromJSONFile(file_name, true);
+
+        if (!json) {
+            LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL)
+                .arg("Config file " + file_name + " missing or empty.");
+            isc_throw(BadValue, "Unable to process JSON configuration file:"
+                      + file_name);
+        }
+
+        // Get Dhcp4 component from the config
+        dhcp4 = json->get("Dhcp4");
+
+        if (!dhcp4) {
+            LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL)
+                .arg("Config file " + file_name + " does not include 'Dhcp4' entry.");
+            isc_throw(BadValue, "Unable to process JSON configuration file:"
+                      + file_name);
+        }
+
+        // Use parsed JSON structures to configure the server
+        result = processCommand("config-reload", dhcp4);
+
+    }  catch (const std::exception& ex) {
+        LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL).arg(ex.what());
+        isc_throw(BadValue, "Unable to process JSON configuration file:"
+                  + file_name);
+    }
+
+    if (!result) {
+        // Undetermined status of the configuration. This should never happen,
+        // but as the configureDhcp4Server returns a pointer, it is theoretically
+        // possible that it will return NULL.
+        LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL)
+            .arg("Configuration failed: Undefined result of processCommand("
+                 "config-reload, " + file_name + ")");
+        isc_throw(BadValue, "Configuration failed: Undefined result of "
+                  "processCommand('config-reload', " + file_name + ")");
+    }
+
+    // Now check is the returned result is successful (rcode=0) or not
+    isc::data::ConstElementPtr comment; /// see @ref isc::config::parseAnswer
+    int rcode;
+    comment = isc::config::parseAnswer(rcode, result);
+    if (rcode != 0) {
+        string reason = "";
+        if (comment) {
+            reason = string(" (") + comment->stringValue() + string(")");
+        }
+        LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL).arg(reason);
+        isc_throw(BadValue, "Failed to apply configuration:" << reason);
+    }
+
+    // We don't need to call openActiveSockets() or startD2() as these
+    // methods are called in processConfig() which is called by
+    // processCommand("reload-config", ...)
+}
+
+void ControlledDhcpv4Srv::cleanup() {
+    // Nothing to do here. No need to disconnect from anything.
+}
+
+/// This is a logger initialization for JSON file backend.
+/// For now, it's just setting log messages to be printed on stdout.
+/// @todo: Implement this properly (see #3427)
+void Daemon::loggerInit(const char*, bool verbose) {
+
+    setenv("B10_LOCKFILE_DIR_FROM_BUILD", "/tmp", 1);
+    setenv("B10_LOGGER_ROOT", "kea", 0);
+    setenv("B10_LOGGER_SEVERITY", (verbose ? "DEBUG":"INFO"), 0);
+    setenv("B10_LOGGER_DBGLEVEL", "99", 0);
+    setenv("B10_LOGGER_DESTINATION",  "stdout", 0);
+    isc::log::initLogger();
+}
+
+};
+};

+ 40 - 33
src/bin/dhcp4/main.cc

@@ -39,13 +39,15 @@ namespace {
 
 const char* const DHCP4_NAME = "b10-dhcp4";
 
+const char* const DHCP4_LOGGER_NAME = "kea";
+
 void
 usage() {
-    cerr << "Usage: " << DHCP4_NAME << " [-v] [-s] [-p number]" << endl;
+    cerr << "Usage: " << DHCP4_NAME << " [-v] [-p number] [-c file]" << endl;
     cerr << "  -v: verbose output" << endl;
-    cerr << "  -s: stand-alone mode (don't connect to BIND10)" << endl;
     cerr << "  -p number: specify non-standard port number 1-65535 "
          << "(useful for testing only)" << endl;
+    cerr << "  -c file: specify configuration file" << endl;
     exit(EXIT_FAILURE);
 }
 } // end of anonymous namespace
@@ -55,19 +57,17 @@ main(int argc, char* argv[]) {
     int ch;
     int port_number = DHCP4_SERVER_PORT; // The default. any other values are
                                          // useful for testing only.
-    bool stand_alone = false;  // Should be connect to BIND10 msgq?
     bool verbose_mode = false; // Should server be verbose?
 
-    while ((ch = getopt(argc, argv, "vsp:")) != -1) {
+    // The standard config file
+    std::string config_file("");
+
+    while ((ch = getopt(argc, argv, "vp:c:")) != -1) {
         switch (ch) {
         case 'v':
             verbose_mode = true;
             break;
 
-        case 's':
-            stand_alone = true;
-            break;
-
         case 'p':
             try {
                 port_number = boost::lexical_cast<int>(optarg);
@@ -83,6 +83,10 @@ main(int argc, char* argv[]) {
             }
             break;
 
+        case 'c': // config file
+            config_file = optarg;
+            break;
+
         default:
             usage();
         }
@@ -93,36 +97,39 @@ main(int argc, char* argv[]) {
         usage();
     }
 
-    // Initialize logging.  If verbose, we'll use maximum verbosity.
-    // If standalone is enabled, do not buffer initial log messages
-    isc::log::initLogger(DHCP4_NAME,
-                         (verbose_mode ? isc::log::DEBUG : isc::log::INFO),
-                         isc::log::MAX_DEBUG_LEVEL, NULL, !stand_alone);
-    LOG_INFO(dhcp4_logger, DHCP4_STARTING);
-    LOG_DEBUG(dhcp4_logger, DBG_DHCP4_START, DHCP4_START_INFO)
-              .arg(getpid()).arg(port_number).arg(verbose_mode ? "yes" : "no")
-              .arg(stand_alone ? "yes" : "no" );
-
-
     int ret = EXIT_SUCCESS;
+
     try {
+        // Initialize logging.  If verbose, we'll use maximum verbosity.
+        // If standalone is enabled, do not buffer initial log messages
+        Daemon::loggerInit(DHCP4_LOGGER_NAME, verbose_mode);
+        LOG_DEBUG(dhcp4_logger, DBG_DHCP4_START, DHCP4_START_INFO)
+            .arg(getpid()).arg(port_number).arg(verbose_mode ? "yes" : "no");
+
+        LOG_INFO(dhcp4_logger, DHCP4_STARTING);
+
+        // Create the server instance.
         ControlledDhcpv4Srv server(port_number);
-        if (!stand_alone) {
-            try {
-                server.establishSession();
-            } catch (const std::exception& ex) {
-                LOG_ERROR(dhcp4_logger, DHCP4_SESSION_FAIL).arg(ex.what());
-                // Let's continue. It is useful to have the ability to run
-                // DHCP server in stand-alone mode, e.g. for testing
-                // We do need to make sure logging is no longer buffered
-                // since then it would not print until dhcp6 is stopped
-                isc::log::LoggerManager log_manager;
-                log_manager.process();
-            }
-        } else {
-            LOG_DEBUG(dhcp4_logger, DBG_DHCP4_START, DHCP4_STANDALONE);
+
+        try {
+            // Initialize the server.
+            server.init(config_file);
+        } catch (const std::exception& ex) {
+            LOG_ERROR(dhcp4_logger, DHCP4_SESSION_FAIL).arg(ex.what());
+
+            // We should not continue if were told to configure (either read
+            // config file or establish Bundy control session).
+
+            isc::log::LoggerManager log_manager;
+            log_manager.process();
+
+            cerr << "Failed to initialize server: " << ex.what() << endl;
+            return (EXIT_FAILURE);
         }
+
+        // And run the main loop of the server.
         server.run();
+
         LOG_INFO(dhcp4_logger, DHCP4_SHUTDOWN);
 
     } catch (const std::exception& ex) {

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

@@ -33,6 +33,7 @@ AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
 
 CLEANFILES = $(builddir)/interfaces.txt $(builddir)/logger_lockfile
 CLEANFILES += $(builddir)/load_marker.txt $(builddir)/unload_marker.txt
+CLEANFILES += *.json
 
 AM_CXXFLAGS = $(B10_CXXFLAGS)
 if USE_CLANGPP
@@ -77,7 +78,7 @@ TESTS += dhcp4_unittests
 
 dhcp4_unittests_SOURCES = ../dhcp4_srv.h ../dhcp4_srv.cc ../ctrl_dhcp4_srv.cc
 dhcp4_unittests_SOURCES += ../dhcp4_log.h ../dhcp4_log.cc
-dhcp4_unittests_SOURCES += ../config_parser.cc ../config_parser.h
+dhcp4_unittests_SOURCES += ../json_config_parser.cc ../json_config_parser.h
 dhcp4_unittests_SOURCES += d2_unittest.h d2_unittest.cc
 dhcp4_unittests_SOURCES += dhcp4_test_utils.h
 dhcp4_unittests_SOURCES += dhcp4_unittests.cc
@@ -89,6 +90,19 @@ dhcp4_unittests_SOURCES += ctrl_dhcp4_srv_unittest.cc
 dhcp4_unittests_SOURCES += config_parser_unittest.cc
 dhcp4_unittests_SOURCES += fqdn_unittest.cc
 dhcp4_unittests_SOURCES += marker_file.cc
+
+if CONFIG_BACKEND_BUNDY
+# For Bundy backend, we only need to run the usual tests. There are no
+# Bundy-specific tests yet.
+dhcp4_unittests_SOURCES += ../bundy_controller.cc
+dhcp4_unittests_SOURCES += bundy_controller_unittest.cc
+endif
+
+if CONFIG_BACKEND_JSON
+dhcp4_unittests_SOURCES += ../kea_controller.cc
+dhcp4_unittests_SOURCES += kea_controller_unittest.cc
+endif
+
 nodist_dhcp4_unittests_SOURCES = ../dhcp4_messages.h ../dhcp4_messages.cc
 nodist_dhcp4_unittests_SOURCES += marker_file.h test_libraries.h
 
@@ -109,4 +123,6 @@ dhcp4_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la
 endif
 
+noinst_EXTRA_DIST = configs-list.txt
+
 noinst_PROGRAMS = $(TESTS)

+ 30 - 0
src/bin/dhcp4/tests/bundy_controller_unittest.cc

@@ -0,0 +1,30 @@
+// Copyright (C) 2014 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.
+
+#include <config.h>
+
+#include <gtest/gtest.h>
+
+namespace {
+
+/// As of May 2014, maintaining or extending Bundy support is very low
+/// prority for Kea team. We are looking for contributors, who would
+/// like to maintain this backend.
+
+// Bundy framework specific tests should be added here.
+TEST(BundyBackendTest, dummy) {
+
+}
+
+} // End of anonymous namespace

+ 1 - 1
src/bin/dhcp4/tests/config_parser_unittest.cc

@@ -19,7 +19,7 @@
 
 #include <config/ccsession.h>
 #include <dhcp4/dhcp4_srv.h>
-#include <dhcp4/config_parser.h>
+#include <dhcp4/json_config_parser.h>
 #include <dhcp/option4_addrlst.h>
 #include <dhcp/option_custom.h>
 #include <dhcp/option_int.h>

+ 5 - 0
src/bin/dhcp4/tests/configs-list.txt

@@ -0,0 +1,5 @@
+# This is a list of config files that the unit-tests (specifically
+# JSONFileBackendTest.loadAllConfigs) is going to load.
+
+../../../../doc/examples/kea4/single-subnet.json
+../../../../doc/examples/kea4/several-subnets.json

+ 12 - 4
src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc

@@ -85,12 +85,12 @@ TEST_F(CtrlDhcpv4SrvTest, commands) {
     int rcode = -1;
 
     // Case 1: send bogus command
-    ConstElementPtr result = ControlledDhcpv4Srv::execDhcpv4ServerCommand("blah", params);
+    ConstElementPtr result = ControlledDhcpv4Srv::processCommand("blah", params);
     ConstElementPtr comment = parseAnswer(rcode, result);
     EXPECT_EQ(1, rcode); // expect failure (no such command as blah)
 
     // Case 2: send shutdown command without any parameters
-    result = ControlledDhcpv4Srv::execDhcpv4ServerCommand("shutdown", params);
+    result = ControlledDhcpv4Srv::processCommand("shutdown", params);
     comment = parseAnswer(rcode, result);
     EXPECT_EQ(0, rcode); // expect success
 
@@ -99,7 +99,7 @@ TEST_F(CtrlDhcpv4SrvTest, commands) {
     params->set("pid", x);
 
     // Case 3: send shutdown command with 1 parameter: pid
-    result = ControlledDhcpv4Srv::execDhcpv4ServerCommand("shutdown", params);
+    result = ControlledDhcpv4Srv::processCommand("shutdown", params);
     comment = parseAnswer(rcode, result);
     EXPECT_EQ(0, rcode); // expect success
 }
@@ -107,6 +107,14 @@ TEST_F(CtrlDhcpv4SrvTest, commands) {
 // Check that the "libreload" command will reload libraries
 
 TEST_F(CtrlDhcpv4SrvTest, libreload) {
+
+    // Sending commands for processing now requires a server that can process
+    // them.
+    boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+    ASSERT_NO_THROW(
+        srv.reset(new ControlledDhcpv4Srv(0))
+    );
+
     // Ensure no marker files to start with.
     ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
     ASSERT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
@@ -137,7 +145,7 @@ TEST_F(CtrlDhcpv4SrvTest, libreload) {
     int rcode = -1;
 
     ConstElementPtr result =
-        ControlledDhcpv4Srv::execDhcpv4ServerCommand("libreload", params);
+        ControlledDhcpv4Srv::processCommand("libreload", params);
     ConstElementPtr comment = parseAnswer(rcode, result);
     EXPECT_EQ(0, rcode); // Expect success
 

+ 1 - 1
src/bin/dhcp4/tests/d2_unittest.cc

@@ -13,7 +13,7 @@
 // PERFORMANCE OF THIS SOFTWARE.
 
 #include <dhcp/iface_mgr.h>
-#include <dhcp4/config_parser.h>
+#include <dhcp4/json_config_parser.h>
 #include <dhcp4/tests/d2_unittest.h>
 #include <dhcpsrv/cfgmgr.h>
 

+ 1 - 1
src/bin/dhcp4/tests/dhcp4_srv_unittest.cc

@@ -32,7 +32,7 @@
 #include <dhcp/tests/iface_mgr_test_config.h>
 #include <dhcp4/dhcp4_srv.h>
 #include <dhcp4/dhcp4_log.h>
-#include <dhcp4/config_parser.h>
+#include <dhcp4/json_config_parser.h>
 #include <hooks/server_hooks.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/lease_mgr.h>

+ 1 - 18
src/bin/dhcp4/tests/dhcp4_test.py

@@ -13,7 +13,7 @@
 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
-from init import ProcessInfo, parse_args, dump_pid, unlink_pid_file, _BASETIME
+from init import ProcessInfo
 
 import unittest
 import sys
@@ -207,22 +207,5 @@ class TestDhcpv4Daemon(unittest.TestCase):
         # Check that there is an error message about invalid port number printed on stderr
         self.assertEqual( str(error).count("Failed to parse port number"), 1)
 
-    def test_portnumber_nonroot(self):
-        print("Check that specifying unprivileged port number will work.")
-
-        # Check that there is a message about running with an unprivileged port
-        (returncode, output, error) = self.runCommand(['../b10-dhcp4', '-v', '-s', '-p', '10057'])
-        output_text = str(output) + str(error)
-        self.assertEqual(output_text.count("DHCP4_OPEN_SOCKET opening sockets on port 10057"), 1)
-
-    def test_skip_msgq(self):
-        print("Check that connection to BIND10 msgq can be disabled.")
-
-        # Check that the system outputs a message on one of its streams about running
-        # standalone.
-        (returncode, output, error) = self.runCommand(['../b10-dhcp4', '-v', '-s', '-p', '10057'])
-        output_text = str(output) + str(error)
-        self.assertEqual(output_text.count("DHCP4_STANDALONE"), 1)
-
 if __name__ == '__main__':
     unittest.main()

+ 1 - 1
src/bin/dhcp4/tests/dhcp4_test_utils.cc

@@ -17,7 +17,7 @@
 #include <asiolink/io_address.h>
 #include <cc/data.h>
 #include <config/ccsession.h>
-#include <dhcp4/config_parser.h>
+#include <dhcp4/json_config_parser.h>
 #include <dhcp4/tests/dhcp4_test_utils.h>
 #include <dhcp/option4_addrlst.h>
 #include <dhcp/option_int.h>

+ 1 - 1
src/bin/dhcp4/tests/direct_client_unittest.cc

@@ -19,7 +19,7 @@
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcpsrv/subnet.h>
-#include <dhcp4/config_parser.h>
+#include <dhcp4/json_config_parser.h>
 #include <dhcp4/tests/dhcp4_test_utils.h>
 #include <gtest/gtest.h>
 #include <string>

+ 296 - 0
src/bin/dhcp4/tests/kea_controller_unittest.cc

@@ -0,0 +1,296 @@
+// Copyright (C) 2014 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.
+
+#include <config.h>
+
+#include <config/ccsession.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp4/ctrl_dhcp4_srv.h>
+#include <dhcpsrv/cfgmgr.h>
+
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+#include <fstream>
+#include <iostream>
+#include <sstream>
+
+#include <arpa/inet.h>
+#include <unistd.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::hooks;
+
+namespace {
+
+class NakedControlledDhcpv4Srv: public ControlledDhcpv4Srv {
+    // "Naked" DHCPv4 server, exposes internal fields
+public:
+    NakedControlledDhcpv4Srv():ControlledDhcpv4Srv(0) { }
+};
+
+/// @brief test class for Kea configuration backend
+///
+/// This class is used for testing Kea configuration backend.
+/// It is very simple and currently focuses on reading
+/// config file from disk. It is expected to be expanded in the
+/// near future.
+class JSONFileBackendTest : public ::testing::Test {
+public:
+    JSONFileBackendTest() {
+    }
+
+    ~JSONFileBackendTest() {
+        static_cast<void>(unlink(TEST_FILE));
+    };
+
+    /// @brief writes specified content to a well known file
+    ///
+    /// Writes specified content to TEST_FILE. Tests will
+    /// attempt to read that file.
+    ///
+    /// @param content content to be written to file
+    void writeFile(const std::string& content) {
+        static_cast<void>(unlink(TEST_FILE));
+
+        ofstream out(TEST_FILE, ios::trunc);
+        EXPECT_TRUE(out.is_open());
+        out << content;
+        out.close();
+    }
+
+    /// Name of a config file used during tests
+    static const char* TEST_FILE;
+};
+
+const char* JSONFileBackendTest::TEST_FILE  = "test-config.json";
+
+// This test checks if configuration can be read from a JSON file.
+TEST_F(JSONFileBackendTest, jsonFile) {
+
+    // Prepare configuration file.
+    string config = "{ \"Dhcp4\": { \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet4\": [ { "
+        "    \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+        "    \"subnet\": \"192.0.2.0/24\" "
+        " },"
+        " {"
+        "    \"pool\": [ \"192.0.3.101 - 192.0.3.150\" ],"
+        "    \"subnet\": \"192.0.3.0/24\", "
+        "    \"id\": 0 "
+        " },"
+        " {"
+        "    \"pool\": [ \"192.0.4.101 - 192.0.4.150\" ],"
+        "    \"subnet\": \"192.0.4.0/24\" "
+        " } ],"
+        "\"valid-lifetime\": 4000 }"
+        "}";
+
+    writeFile(config);
+
+    // Now initialize the server
+    boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+    ASSERT_NO_THROW(
+        srv.reset(new ControlledDhcpv4Srv(0))
+    );
+
+    // And configure it using the config file.
+    EXPECT_NO_THROW(srv->init(TEST_FILE));
+
+    // Now check if the configuration has been applied correctly.
+    const Subnet4Collection* subnets = CfgMgr::instance().getSubnets4();
+    ASSERT_TRUE(subnets);
+    ASSERT_EQ(3, subnets->size()); // We expect 3 subnets.
+
+
+    // Check subnet 1.
+    EXPECT_EQ("192.0.2.0", subnets->at(0)->get().first.toText());
+    EXPECT_EQ(24, subnets->at(0)->get().second);
+
+    // Check pools in the first subnet.
+    const PoolCollection& pools1 = subnets->at(0)->getPools(Lease::TYPE_V4);
+    ASSERT_EQ(1, pools1.size());
+    EXPECT_EQ("192.0.2.1", pools1.at(0)->getFirstAddress().toText());
+    EXPECT_EQ("192.0.2.100", pools1.at(0)->getLastAddress().toText());
+    EXPECT_EQ(Lease::TYPE_V4, pools1.at(0)->getType());
+
+    // Check subnet 2.
+    EXPECT_EQ("192.0.3.0", subnets->at(1)->get().first.toText());
+    EXPECT_EQ(24, subnets->at(1)->get().second);
+
+    // Check pools in the second subnet.
+    const PoolCollection& pools2 = subnets->at(1)->getPools(Lease::TYPE_V4);
+    ASSERT_EQ(1, pools2.size());
+    EXPECT_EQ("192.0.3.101", pools2.at(0)->getFirstAddress().toText());
+    EXPECT_EQ("192.0.3.150", pools2.at(0)->getLastAddress().toText());
+    EXPECT_EQ(Lease::TYPE_V4, pools2.at(0)->getType());
+
+    // And finally check subnet 3.
+    EXPECT_EQ("192.0.4.0", subnets->at(2)->get().first.toText());
+    EXPECT_EQ(24, subnets->at(2)->get().second);
+
+    // ... and it's only pool.
+    const PoolCollection& pools3 = subnets->at(2)->getPools(Lease::TYPE_V4);
+    EXPECT_EQ("192.0.4.101", pools3.at(0)->getFirstAddress().toText());
+    EXPECT_EQ("192.0.4.150", pools3.at(0)->getLastAddress().toText());
+    EXPECT_EQ(Lease::TYPE_V4, pools3.at(0)->getType());
+}
+
+// This test checks if configuration can be read from a JSON file.
+TEST_F(JSONFileBackendTest, comments) {
+
+    string config_hash_comments = "# This is a comment. It should be \n"
+        "#ignored. Real config starts in line below\n"
+        "{ \"Dhcp4\": { \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, \n"
+        "# comments in the middle should be ignored, too\n"
+        "\"subnet4\": [ { "
+        "    \"pool\": [ \"192.0.2.0/24\" ],"
+        "    \"subnet\": \"192.0.2.0/22\" "
+        " } ],"
+        "\"valid-lifetime\": 4000 }"
+        "}";
+
+    /// @todo: Implement C++-style (// ...) comments
+    /// @todo: Implement C-style (/* ... */) comments
+
+    writeFile(config_hash_comments);
+
+    // Now initialize the server
+    boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+    ASSERT_NO_THROW(
+        srv.reset(new ControlledDhcpv4Srv(0))
+    );
+
+    // And configure it using config with comments.
+    EXPECT_NO_THROW(srv->init(TEST_FILE));
+
+    // Now check if the configuration has been applied correctly.
+    const Subnet4Collection* subnets = CfgMgr::instance().getSubnets4();
+    ASSERT_TRUE(subnets);
+    ASSERT_EQ(1, subnets->size());
+
+    // Check subnet 1.
+    EXPECT_EQ("192.0.2.0", subnets->at(0)->get().first.toText());
+    EXPECT_EQ(22, subnets->at(0)->get().second);
+
+    // Check pools in the first subnet.
+    const PoolCollection& pools1 = subnets->at(0)->getPools(Lease::TYPE_V4);
+    ASSERT_EQ(1, pools1.size());
+    EXPECT_EQ("192.0.2.0", pools1.at(0)->getFirstAddress().toText());
+    EXPECT_EQ("192.0.2.255", pools1.at(0)->getLastAddress().toText());
+    EXPECT_EQ(Lease::TYPE_V4, pools1.at(0)->getType());
+}
+
+// This test checks if configuration detects failure when trying:
+// - empty file
+// - empty filename
+// - no Dhcp4 element
+// - Config file that contains Dhcp4 but has a content error
+TEST_F(JSONFileBackendTest, configBroken) {
+
+    // Empty config is not allowed, because Dhcp4 element is missing
+    string config_empty = "";
+
+    // This config does not have mandatory Dhcp4 element
+    string config_v4 = "{ \"Dhcp6\": { \"interfaces\": [ \"*\" ],"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet4\": [ { "
+        "    \"pool\": [ \"2001:db8::/80\" ],"
+        "    \"subnet\": \"2001:db8::/64\" "
+        " } ]}";
+
+    // This has Dhcp4 element, but it's utter nonsense
+    string config_nonsense = "{ \"Dhcp4\": { \"reviews\": \"are so much fun\" } }";
+
+    // Now initialize the server
+    boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+    ASSERT_NO_THROW(
+        srv.reset(new ControlledDhcpv4Srv(0))
+    );
+
+    // Try to configure without filename. Should fail.
+    EXPECT_THROW(srv->init(""), BadValue);
+
+    // Try to configure it using empty file. Should fail.
+    writeFile(config_empty);
+    EXPECT_THROW(srv->init(TEST_FILE), BadValue);
+
+    // Now try to load a config that does not have Dhcp4 component.
+    writeFile(config_v4);
+    EXPECT_THROW(srv->init(TEST_FILE), BadValue);
+
+    // Now try to load a config with Dhcp4 full of nonsense.
+    writeFile(config_nonsense);
+    EXPECT_THROW(srv->init(TEST_FILE), BadValue);
+}
+
+/// This unit-test reads all files enumerated in configs-test.txt file, loads
+/// each of them and verify that they can be loaded.
+///
+/// @todo: Unfortunately, we have this test disabled, because all loaded
+/// configs use memfile, which attempts to create lease file in
+/// /usr/local/var/bind10/kea-leases4.csv. We have couple options here:
+/// a) disable persistence in example configs - a very bad thing to do
+///    as users will forget to reenable it and then will be surprised when their
+///    leases disappear
+/// b) change configs to store lease file in /tmp. It's almost as bad as the
+///    previous one. Users will then be displeased when all their leases are
+///    wiped. (most systems wipe /tmp during boot)
+/// c) read each config and rewrite it on the fly, so persistence is disabled.
+///    This is probably the way to go, but this is a work for a dedicated ticket.
+///
+/// Hence I'm leaving the test in, but it is disabled.
+TEST_F(JSONFileBackendTest, DISABLED_loadAllConfigs) {
+
+    // Create server first
+    boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+    ASSERT_NO_THROW(
+        srv.reset(new ControlledDhcpv4Srv(0))
+    );
+
+    const char* configs_list = "configs-list.txt";
+    fstream configs(configs_list, ios::in);
+    ASSERT_TRUE(configs.is_open());
+    std::string config_name;
+    while (std::getline(configs, config_name)) {
+
+        // Ignore empty and commented lines
+        if (config_name.empty() || config_name[0] == '#') {
+            continue;
+        }
+
+        // Unit-tests usually do not print out anything, but in this case I
+        // think printing out tests configs is warranted.
+        std::cout << "Loading config file " << config_name << std::endl;
+
+        try {
+            srv->init(config_name);
+        } catch (const std::exception& ex) {
+            ADD_FAILURE() << "Exception thrown" << ex.what() << endl;
+        }
+    }
+}
+
+} // End of anonymous namespace

+ 6 - 3
src/bin/dhcp6/bundy_controller.cc

@@ -133,7 +133,10 @@ bundyConfigHandler(ConstElementPtr new_config) {
 }
 
 void
-ControlledDhcpv6Srv::init(const std::string& /* config_file*/) {
+ControlledDhcpv6Srv::init(const std::string& config_file) {
+    // Call base class's init.
+    Daemon::init(config_file);
+
     // This is Bundy configuration backed. It established control session
     // that is used to connect to Bundy framework.
     //
@@ -217,10 +220,10 @@ void ControlledDhcpv6Srv::cleanup() {
 }
 
 void
-Daemon::loggerInit(const char* log_name, bool verbose, bool stand_alone) {
+Daemon::loggerInit(const char* log_name, bool verbose) {
     isc::log::initLogger(log_name,
                          (verbose ? isc::log::DEBUG : isc::log::INFO),
-                         isc::log::MAX_DEBUG_LEVEL, NULL, !stand_alone);
+                         isc::log::MAX_DEBUG_LEVEL, NULL, true);
 }
 
 }; // end of isc::dhcp namespace

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

@@ -43,8 +43,8 @@ ControlledDhcpv6Srv::commandShutdownHandler(const string&, ConstElementPtr) {
 
 ConstElementPtr
 ControlledDhcpv6Srv::commandLibReloadHandler(const string&, ConstElementPtr) {
-    // TODO delete any stored CalloutHandles referring to the old libraries
-    // Get list of currently loaded libraries and reload them.
+    /// @todo delete any stored CalloutHandles referring to the old libraries
+    /// Get list of currently loaded libraries and reload them.
     vector<string> loaded = HooksManager::getLibraryNames();
     bool status = HooksManager::loadLibraries(loaded);
     if (!status) {
@@ -99,9 +99,12 @@ ControlledDhcpv6Srv::processCommand(const std::string& command,
     }
 }
 
-
 isc::data::ConstElementPtr
 ControlledDhcpv6Srv::processConfig(isc::data::ConstElementPtr config) {
+
+    LOG_DEBUG(dhcp6_logger, DBG_DHCP6_COMMAND, DHCP6_CONFIG_RECEIVED)
+              .arg(config->str());
+
     ControlledDhcpv6Srv* srv = ControlledDhcpv6Srv::getInstance();
 
     if (!srv) {

+ 14 - 10
src/bin/dhcp6/ctrl_dhcp6_srv.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2013  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014  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
@@ -26,12 +26,10 @@ namespace dhcp {
 
 /// @brief Controlled version of the DHCPv6 server
 ///
-/// This is a class that is responsible for establishing connection
-/// with msqg (receving commands and configuration). This is an extended
-/// version of Dhcpv6Srv class that is purely a DHCPv6 server, without
-/// external control. ControlledDhcpv6Srv should be used in typical BIND10
-/// (i.e. featuring msgq) environment, while Dhcpv6Srv should be used in
-/// embedded environments.
+/// This is a class that is responsible for DHCPv6 server being controllable.
+/// It does various things, depending on the configuration backend.
+/// For Bundy backend it establishes a connection with msqg and later receives
+/// commands over it. For Kea backend, it reads configuration file from disk.
 ///
 /// For detailed explanation or relations between main(), ControlledDhcpv6Srv,
 /// Dhcpv6Srv and other classes, see \ref dhcpv6Session.
@@ -53,7 +51,8 @@ public:
     /// operation. For specific details, see actual implementation in
     /// *_backend.cc
     ///
-    /// @return true if initialization was successful, false if it failed
+    /// This method may throw if initialization fails. Exception types may be
+    /// specific to used configuration backend.
     void init(const std::string& config_file);
 
     /// @brief Performs cleanup, immediately before termination
@@ -77,6 +76,11 @@ public:
     /// wrapper that calls process*Command() methods and catches exceptions
     /// in them.
     ///
+    /// Currently supported commands are:
+    /// - shutdown
+    /// - libreload
+    /// - config-reload
+    ///
     /// @note It never throws.
     ///
     /// @param command Text represenation of the command (e.g. "shutdown")
@@ -88,7 +92,7 @@ public:
 
     /// @brief configuration processor
     ///
-    /// This is a callback for handling incoming configuration updates.
+    /// This is a method for handling incoming configuration updates.
     /// This method should be called by all configuration backends when the
     /// server is starting up or when configuration has changed.
     ///
@@ -103,7 +107,7 @@ public:
 
     /// @brief returns pointer to the sole instance of Dhcpv6Srv
     ///
-    /// @note may return NULL, if called before server is spawned
+    /// @return server instance (may return NULL, if called before server is spawned)
     static ControlledDhcpv6Srv* getInstance() {
         return (server_);
     }

+ 43 - 10
src/bin/dhcp6/dhcp6.dox

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014 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
@@ -234,24 +234,25 @@ cases it would be convenient to keep configuration in XML files.
 
 Kea 0.9 introduces configuration backends that are switchable during compilation phase.
 There is a new parameter for configure script: --with-kea-config. It currently supports
-two values: BIND10 and JSON.
+two values: BUNDY and JSON.
 
-BIND10 (which is the default value as of April 2014) means that Kea6 is linked with the
-BIND10 configuration backend that connects to the BIND10 framework and in general works
+BUNDY (which is the default value as of April 2014) means that Kea6 is linked with the
+Bundy (former BIND10) configuration backend that connects to the BIND10 framework and in general works
 exactly the same as Kea 0.8 and earlier versions. The benefits of that backend are uniform
-integration with BIND10 framework, easy on-line reconfiguration using bindctl, available
-RESTful API. On the other hand, it requires the whole heavy BIND10 framework that requires
-Python3 to be present. That backend is likely to go away with the release of Kea 0.9.
+integration with Bundy/BIND10 framework, easy on-line reconfiguration using bindctl, available
+RESTful API. On the other hand, it requires the whole heavy Bundy framework that requires
+Python3 to be present. That framework is going away with the release of Kea 0.9.
 
 JSON is a new configuration backend that causes Kea to read JSON configuration file from
 disk. It does not require any framework and thus is considered more lightweight. It will
 allow dynamic on-line reconfiguration, but will lack remote capabilities (i.e. no RESTful
-API). This configuration backend is expected to be the default for upcoming Kea 0.9.
+API). This configuration backend is expected to be the default for upcoming Kea 0.9. It
+requires <tt> -c config-file </tt> command-line option.
 
 Internally, configuration backends are implemented as different implementations of the
-isc::dhcp::ControlledDhcpv6Srv class, stored in ctrl_*_dhcpv6_srv.cc files. Depending on
+isc::dhcp::ControlledDhcpv6Srv class, stored in {kea,bundy}_controller.cc files. Depending on
 the choice made by ./configure script, only one of those files is compiled and linked.
-There are backend specific tests in src/bin/dhcp6/tests/ctrl_*_dhcpv6_srv_unittest.cc.
+There are backend specific tests in src/bin/dhcp6/tests/{kea,bundy}_controller_unittest.cc.
 Only tests specific to selected backend are linked and executed during make distcheck.
 
 While it is unlikely that ISC will support more than one backend at any given time, there
@@ -262,6 +263,38 @@ for ISC-DHCP. Is at least possible that similar will happen for Kea. Finally, if
 extend the isc::dhcp::CfgMgr with configuration export, this approach could be used as
 a migration tool.
 
+@section dhcpv6SignalBasedReconfiguration Reconfiguring DHCPv6 server with SIGHUP signal
+
+Online reconfiguration (reconfiguration without a need to restart the server) is an
+important feature which is supported by all modern DHCP servers. When using the JSON
+configuration backend, a configuration file name is specified with a command line
+option of the DHCP server binary. The configuration file is used to configure the
+server at startup. If the initial configuration fails, the server will fail to start.
+If the server starts and configures successfully it will use the initial configuration
+until it is reconfigured.
+
+The reconfiguration request can be triggered externally (from other process) by editing
+a configuration file and sending a SIGHUP signal to DHCP server process. After receiving
+the SIGHUP signal, the server will re-read the configuration file specified at startup.
+If the reconfiguration fails, the server will continue to run and use the last good
+configuration.
+
+The SIGHUP signal handler is defined in the kea_controller.cc. The handler calls the
+same function to reconfigure the server which is called to configure it at startup.
+The signal handler catches exceptions emitted during reconfiguration so as the
+uncaught exceptions don't cause the process to exit.
+
+Signal handlers are static and therefore they must call static functions. The
+@c ControlledDhcpv6Srv::processCommand which performs the actual server
+reconfiguration is static, so it can be called from the signal handler. In order
+for the signal handler to know the location of the configuration file (specified
+at process startup), the location of this file needs to be stored in a static
+variable so as it may be directly accessed by the signal handler. This static
+variable is stored in the @c dhcp::Daemon class and all Kea processes can use
+it (all processes derive from this class). The configuration file location is
+initialized when the @c Daemon::init method is called. Therefore, derived
+classes should call it in their implementations of the @c init method.
+
  @section dhcpv6Other Other DHCPv6 topics
 
  For hooks API support in DHCPv6, see @ref dhcpv6Hooks.

+ 13 - 1
src/bin/dhcp6/dhcp6_messages.mes

@@ -40,6 +40,10 @@ address assignment.  The most likely cause is a problem with the client.
 A debug message listing the command (and possible arguments) received
 from the BIND 10 control system by the IPv6 DHCP server.
 
+% DHCP6_CONFIG_RECEIVED received configuration: %1
+A debug message listing the configuration received by the DHCPv6 server.
+The source of that configuration depends on used configuration backend.
+
 % DHCP6_CONFIG_COMPLETE DHCPv6 server has completed configuration: %1
 This is an informational message announcing the successful processing of a
 new configuration. it is output during server startup, and when an updated
@@ -115,6 +119,14 @@ 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_DYNAMIC_RECONFIGURATION initate server reconfiguration using file: %1, after receiving SIGHUP signal
+This is the info message logged when the DHCPv6 server starts reconfiguration
+as a result of receiving SIGHUP signal.
+
+% DHCP6_DYNAMIC_RECONFIGURATION_FAIL dynamic server reconfiguration failed with file: %1
+This is an error message logged when the dynamic reconfiguration of the
+DHCP server failed.
+
 % DHCP6_EXTEND_LEASE_SUBNET_SELECTED the %1 subnet was selected for client extending its lease
 This is a debug message informing that a given subnet was selected. It will
 be used for extending lifetime of the lease. This is one of the early steps
@@ -532,7 +544,7 @@ production environment.
 This informational message indicates that the IPv6 DHCP server has
 processed any command-line switches and is starting.
 
-% DHCP6_START_INFO pid: %1, port: %2, verbose: %3, standalone: %4
+% DHCP6_START_INFO pid: %1, port: %2, verbose: %3
 This is a debug message issued during the IPv6 DHCP server startup.
 It lists some information about the parameters with which the server
 is running.

+ 75 - 32
src/bin/dhcp6/kea_controller.cc

@@ -15,41 +15,33 @@
 #include <config.h>
 
 #include <asiolink/asiolink.h>
-#include <dhcp/iface_mgr.h>
 #include <dhcpsrv/dhcp_config_parser.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcp6/json_config_parser.h>
 #include <dhcp6/ctrl_dhcp6_srv.h>
 #include <dhcp6/dhcp6_log.h>
-#include <dhcp6/spec_config.h>
-#include <log/logger_level.h>
-#include <log/logger_name.h>
-#include <log/logger_manager.h>
-#include <log/logger_specification.h>
-#include <log/logger_support.h>
-#include <log/output_option.h>
 #include <exceptions/exceptions.h>
-#include <util/buffer.h>
 
-#include <cassert>
-#include <iostream>
+#include <signal.h>
+
 #include <string>
-#include <vector>
 
 using namespace isc::asiolink;
-using namespace isc::cc;
-using namespace isc::config;
-using namespace isc::data;
 using namespace isc::dhcp;
-using namespace isc::log;
-using namespace isc::util;
 using namespace std;
 
-namespace isc {
-namespace dhcp {
-
-void
-ControlledDhcpv6Srv::init(const std::string& file_name) {
+namespace {
+
+/// @brief Configure DHCPv6 server using the configuration file specified.
+///
+/// This function is used to both configure the DHCP server on its startup
+/// and dynamically reconfigure the server when SIGHUP signal is received.
+///
+/// It fetches DHCPv6 server's configuration from the 'Dhcp6' section of
+/// the JSON configuration file.
+///
+/// @param file_name Configuration file location.
+void configure(const std::string& file_name) {
     // This is a configuration backend implementation that reads the
     // configuration from a JSON file.
 
@@ -61,17 +53,17 @@ ControlledDhcpv6Srv::init(const std::string& file_name) {
     try {
         if (file_name.empty()) {
             // Basic sanity check: file name must not be empty.
-            isc_throw(BadValue, "JSON configuration file not specified. Please "
+            isc_throw(isc::BadValue, "JSON configuration file not specified. Please "
                       "use -c command line option.");
         }
 
         // Read contents of the file and parse it as JSON
-        json = Element::fromJSONFile(file_name, true);
+        json = isc::data::Element::fromJSONFile(file_name, true);
 
         if (!json) {
             LOG_ERROR(dhcp6_logger, DHCP6_CONFIG_LOAD_FAIL)
                 .arg("Config file " + file_name + " missing or empty.");
-            isc_throw(BadValue, "Unable to process JSON configuration file:"
+            isc_throw(isc::BadValue, "Unable to process JSON configuration file:"
                       + file_name);
         }
 
@@ -81,16 +73,16 @@ ControlledDhcpv6Srv::init(const std::string& file_name) {
         if (!dhcp6) {
             LOG_ERROR(dhcp6_logger, DHCP6_CONFIG_LOAD_FAIL)
                 .arg("Config file " + file_name + " does not include 'Dhcp6' entry.");
-            isc_throw(BadValue, "Unable to process JSON configuration file:"
+            isc_throw(isc::BadValue, "Unable to process JSON configuration file:"
                       + file_name);
         }
 
         // Use parsed JSON structures to configure the server
-        result = processCommand("config-reload", dhcp6);
+        result = ControlledDhcpv6Srv::processCommand("config-reload", dhcp6);
 
     }  catch (const std::exception& ex) {
         LOG_ERROR(dhcp6_logger, DHCP6_CONFIG_LOAD_FAIL).arg(ex.what());
-        isc_throw(BadValue, "Unable to process JSON configuration file:"
+        isc_throw(isc::BadValue, "Unable to process JSON configuration file:"
                   + file_name);
     }
 
@@ -105,21 +97,72 @@ ControlledDhcpv6Srv::init(const std::string& file_name) {
     }
 
     // Now check is the returned result is successful (rcode=0) or not
-    ConstElementPtr comment; /// see @ref isc::config::parseAnswer
+    isc::data::ConstElementPtr comment; /// see @ref isc::config::parseAnswer
     int rcode;
-    comment = parseAnswer(rcode, result);
+    comment = isc::config::parseAnswer(rcode, result);
     if (rcode != 0) {
         string reason = "";
         if (comment) {
             reason = string(" (") + comment->stringValue() + string(")");
         }
         LOG_ERROR(dhcp6_logger, DHCP6_CONFIG_LOAD_FAIL).arg(reason);
-        isc_throw(BadValue, "Failed to apply configuration:" << reason);
+        isc_throw(isc::BadValue, "Failed to apply configuration:" << reason);
     }
+}
+
+/// @brief Signals handler for DHCPv6 server.
+///
+/// This signal handler handles the following signals received by the DHCPv6
+/// server process:
+/// - SIGHUP - triggers server's dynamic reconfiguration.
+/// - SIGTERM - triggers server's shut down.
+/// - SIGINT - triggers server's shut down.
+///
+/// @param signo Signal number received.
+void signalHandler(int signo) {
+    // SIGHUP signals a request to reconfigure the server.
+    if (signo == SIGHUP) {
+        // Get configuration file name.
+        std::string file = ControlledDhcpv6Srv::getInstance()->getConfigFile();
+        try {
+            LOG_INFO(dhcp6_logger, DHCP6_DYNAMIC_RECONFIGURATION).arg(file);
+            configure(file);
+        } catch (const std::exception& ex) {
+            // Log the unsuccessful reconfiguration. The reason for failure
+            // should be already logged. Don't rethrow an exception so as
+            // the server keeps working.
+            LOG_ERROR(dhcp6_logger, DHCP6_DYNAMIC_RECONFIGURATION_FAIL)
+                .arg(file);
+        }
+    } else if ((signo == SIGTERM) || (signo == SIGINT)) {
+        isc::data::ElementPtr params(new isc::data::MapElement());
+        ControlledDhcpv6Srv::processCommand("shutdown", params);
+    }
+}
+
+}
+
+namespace isc {
+namespace dhcp {
+
+void
+ControlledDhcpv6Srv::init(const std::string& file_name) {
+    // Call parent class's init to initialize file name.
+    Daemon::init(file_name);
+
+    // Configure the server using JSON file.
+    configure(file_name);
 
     // We don't need to call openActiveSockets() or startD2() as these
     // methods are called in processConfig() which is called by
     // processCommand("reload-config", ...)
+
+    // Set signal handlers. When the SIGHUP is received by the process
+    // the server reconfiguration will be triggered. When SIGTERM or
+    // SIGINT will be received, the server will start shutting down.
+    signal(SIGHUP, signalHandler);
+    signal(SIGTERM, signalHandler);
+    signal(SIGINT, signalHandler);
 }
 
 void ControlledDhcpv6Srv::cleanup() {
@@ -129,7 +172,7 @@ void ControlledDhcpv6Srv::cleanup() {
 /// This is a logger initialization for JSON file backend.
 /// For now, it's just setting log messages to be printed on stdout.
 /// @todo: Implement this properly (see #3427)
-void Daemon::loggerInit(const char*, bool verbose, bool ) {
+void Daemon::loggerInit(const char*, bool verbose) {
 
     setenv("B10_LOCKFILE_DIR_FROM_BUILD", "/tmp", 1);
     setenv("B10_LOGGER_ROOT", "kea", 0);

+ 20 - 28
src/bin/dhcp6/main.cc

@@ -43,9 +43,8 @@ const char* const DHCP6_LOGGER_NAME = "kea";
 
 void
 usage() {
-    cerr << "Usage: " << DHCP6_NAME << " [-v] [-s] [-p port_number] [-c cfgfile]" << endl;
+    cerr << "Usage: " << DHCP6_NAME << " [-v] [-p port_number] [-c cfgfile]" << endl;
     cerr << "  -v: verbose output" << endl;
-    cerr << "  -s: skip configuration (don't connect to BIND10 or don't read config file)" << endl;
     cerr << "  -p number: specify non-standard port number 1-65535 "
          << "(useful for testing only)" << endl;
     cerr << "  -c file: specify configuration file" << endl;
@@ -58,22 +57,17 @@ main(int argc, char* argv[]) {
     int ch;
     int port_number = DHCP6_SERVER_PORT; // The default. Any other values are
                                          // useful for testing only.
-    bool stand_alone = false;  // Should be connect to BIND10 msgq?
     bool verbose_mode = false; // Should server be verbose?
 
     // The standard config file
     std::string config_file("");
 
-    while ((ch = getopt(argc, argv, "vsp:c:")) != -1) {
+    while ((ch = getopt(argc, argv, "vp:c:")) != -1) {
         switch (ch) {
         case 'v':
             verbose_mode = true;
             break;
 
-        case 's': // stand-alone
-            stand_alone = true;
-            break;
-
         case 'p': // port number
             try {
                 port_number = boost::lexical_cast<int>(optarg);
@@ -107,39 +101,37 @@ main(int argc, char* argv[]) {
     int ret = EXIT_SUCCESS;
     try {
         // Initialize logging.  If verbose, we'll use maximum verbosity.
-        // If standalone is enabled, do not buffer initial log messages
-        Daemon::loggerInit(DHCP6_LOGGER_NAME, verbose_mode, stand_alone);
+        Daemon::loggerInit(DHCP6_LOGGER_NAME, verbose_mode);
 
         LOG_DEBUG(dhcp6_logger, DBG_DHCP6_START, DHCP6_START_INFO)
-            .arg(getpid()).arg(port_number).arg(verbose_mode ? "yes" : "no")
-            .arg(stand_alone ? "yes" : "no" );
+            .arg(getpid()).arg(port_number).arg(verbose_mode ? "yes" : "no");
 
         LOG_INFO(dhcp6_logger, DHCP6_STARTING);
 
+        // Create the server instance.
         ControlledDhcpv6Srv server(port_number);
 
-        if (!stand_alone) {
-            try {
-                // Initialize the server, i.e. establish control session
-                // if BIND10 backend is used or read a configuration file
-                server.init(config_file);
+        try {
+            // Initialize the server, i.e. establish control session
+            // if BIND10 backend is used or read a configuration file
+            // if Kea backend is used.
+            server.init(config_file);
 
-            } catch (const std::exception& ex) {
-                LOG_ERROR(dhcp6_logger, DHCP6_INIT_FAIL).arg(ex.what());
+        } catch (const std::exception& ex) {
+            LOG_ERROR(dhcp6_logger, DHCP6_INIT_FAIL).arg(ex.what());
 
-                // We should not continue if were told to configure (either read
-                // config file or establish BIND10 control session).
-                isc::log::LoggerManager log_manager;
-                log_manager.process();
+            // We should not continue if were told to configure (either read
+            // config file or establish BIND10 control session).
+            isc::log::LoggerManager log_manager;
+            log_manager.process();
 
-                cerr << "Failed to initialize server: " << ex.what() << endl;
-                return (EXIT_FAILURE);
-            }
-        } else {
-            LOG_DEBUG(dhcp6_logger, DBG_DHCP6_START, DHCP6_STANDALONE);
+            cerr << "Failed to initialize server: " << ex.what() << endl;
+            return (EXIT_FAILURE);
         }
 
+        // And run the main loop of the server.
         server.run();
+
         LOG_INFO(dhcp6_logger, DHCP6_SHUTDOWN);
 
     } catch (const std::exception& ex) {

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

@@ -1,6 +1,14 @@
 PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
 PYTESTS = dhcp6_test.py
-EXTRA_DIST = $(PYTESTS)
+SHTESTS =
+# The test of dynamic reconfiguration based on signals will work only
+# if we are using file based configuration approach.
+if CONFIG_BACKEND_JSON
+SHTESTS += dhcp6_reconfigure_test.sh
+SHTESTS += dhcp6_sigterm_test.sh
+SHTESTS += dhcp6_sigint_test.sh
+endif
+EXTRA_DIST = $(PYTESTS) $(SHTESTS)
 
 # Explicitly specify paths to dynamic libraries required by loadable python
 # modules. That is required on Mac OS systems. Otherwise we will get exception
@@ -20,6 +28,12 @@ check-local:
 		$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
 	done
 
+	for shtest in $(SHTESTS) ; do \
+	echo Running test: $$shtest ; \
+	export B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir); \
+	$(abs_srcdir)/$$shtest || exit ; \
+	done
+
 
 AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
 AM_CPPFLAGS += -I$(top_builddir)/src/bin # for generated spec_config.h header
@@ -30,6 +44,7 @@ AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
 
 CLEANFILES  = $(builddir)/interfaces.txt $(builddir)/logger_lockfile
 CLEANFILES += $(builddir)/load_marker.txt $(builddir)/unload_marker.txt
+CLEANFILES += *.json *.log
 
 AM_CXXFLAGS = $(B10_CXXFLAGS)
 if USE_CLANGPP

+ 4 - 0
src/bin/dhcp6/tests/bundy_controller_unittest.cc

@@ -18,6 +18,10 @@
 
 namespace {
 
+/// As of May 2014, maintaining or extending Bundy support is very low
+/// prority for Kea team. We are looking for contributors, who would
+/// like to maintain this backend.
+
 // Bundy framework specific tests should be added here.
 TEST(BundyBackendTest, dummy) {
 

+ 164 - 0
src/bin/dhcp6/tests/dhcp6_reconfigure_test.sh

@@ -0,0 +1,164 @@
+# Copyright (C) 2014 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.
+
+# Test name
+TEST_NAME="DynamicReconfiguration"
+# Path to the temporary configuration file.
+CFG_FILE="test_config.json"
+# Path to the Kea log file.
+LOG_FILE="test.log"
+# Kea configuration to be stored in the configuration file.
+CONFIG="{
+    \"Dhcp6\":
+    {
+        \"interfaces\": [ ],
+        \"preferred-lifetime\": 3000,
+        \"valid-lifetime\": 4000,
+        \"renew-timer\": 1000,
+        \"rebind-timer\": 2000,
+        \"lease-database\":
+        {
+            \"type\": \"memfile\",
+            \"persist\": false
+        },
+        \"subnet6\": [
+        {
+            \"subnet\": \"2001:db8:1::/64\",
+            \"pool\": [ \"2001:db8:1::10-2001:db8:1::100\" ]
+        } ]
+    }
+}"
+# Invalid configuration (negative preferred-lifetime) to check that Kea
+# gracefully handles reconfiguration errors.
+CONFIG_INVALID="{
+    \"Dhcp6\":
+    {
+        \"interfaces\": [ ],
+        \"preferred-lifetime\": -3,
+        \"valid-lifetime\": 4000,
+        \"renew-timer\": 1000,
+        \"rebind-timer\": 2000,
+        \"lease-database\":
+        {
+            \"type\": \"memfile\",
+            \"persist\": false
+        },
+        \"subnet6\": [
+        {
+            \"subnet\": \"2001:db8:1::/64\",
+            \"pool\": [ \"2001:db8:1::10-2001:db8:1::100\" ]
+        } ]
+    }
+}"
+
+# Set the location of the executable.
+BIN="b10-dhcp6"
+BIN_PATH=".."
+
+# Import common test library.
+. $(dirname $0)/../../../lib/testutils/dhcp_test_lib.sh
+
+# Log the start of the test and print test name.
+test_start
+# Remove dangling Kea instances and remove log files.
+cleanup
+# Create new configuration file.
+create_config "${CONFIG}"
+# Instruct Kea to log to the specific file.
+set_logger
+# Start Kea.
+start_kea
+# Wait up to 20s for Kea to start.
+wait_for_kea 20
+if [ ${_WAIT_FOR_KEA} -eq 0 ]; then
+    printf "ERROR: timeout waiting for Kea to start.\n"
+    clean_exit 1
+fi
+
+# Check if it is still running. It could have terminated (e.g. as a result
+# of configuration failure).
+get_pids
+if [ ${_GET_PIDS_NUM} -ne 1 ]; then
+    printf "ERROR: expected one Kea process to be started. Found %d processes started.\n" ${_GET_PIDS_NUM}
+    clean_exit 1
+fi
+
+# Check in the log file, how many times server has been configured. It should
+# be just once on startup.
+get_reconfigs
+if [ ${_GET_RECONFIGS} -ne 1 ]; then
+    printf "ERROR: server hasn't been configured.\n"
+    clean_exit 1
+else
+    printf "Server successfully configured.\n"
+fi
+
+# Now use invalid configuration.
+create_config "${CONFIG_INVALID}"
+
+# Try to reconfigure by sending SIGHUP
+send_signal 1
+
+# The configuration should fail and the error message should be there.
+wait_for_message 10 "DHCP6_CONFIG_LOAD_FAIL" 1
+
+# After receiving SIGHUP the server should try to reconfigure itself.
+# The configuration provided is invalid so it should result in
+# reconfiguration failure but the server should still be running.
+get_reconfigs
+if [ ${_GET_RECONFIGS} -ne 1 ]; then
+    printf "ERROR: server has been reconfigured despite bogus configuration.\n"
+    clean_exit 1
+elif [ ${_GET_RECONFIG_ERRORS} -ne 1 ]; then
+    printf "ERROR: server did not report reconfiguration error despite attempt" \
+        " to configure it with invalid configuration.\n"
+    clean_exit 1
+fi
+
+# Make sure the server is still operational.
+get_pids
+if [ ${_GET_PIDS_NUM} -ne 1 ]; then
+    printf "ERROR: Kea process was killed when attempting reconfiguration.\n"
+    clean_exit 1
+fi
+
+# Restore the good configuration.
+create_config "${CONFIG}"
+
+# Reconfigure the server with SIGHUP.
+send_signal 1
+
+# There should be two occurrences of the DHCP6_CONFIG_COMPLETE messages.
+# Wait for it up to 10s.
+wait_for_message 10 "DHCP6_CONFIG_COMPLETE" 2
+
+# After receiving SIGHUP the server should get reconfigured and the
+# reconfiguration should be noted in the log file. We should now
+# have two configurations logged in the log file.
+if [ ${_WAIT_FOR_MESSAGE} -eq 0 ]; then
+    printf "ERROR: server hasn't been reconfigured.\n"
+    clean_exit 1
+else
+    printf "Server successfully reconfigured.\n"
+fi
+
+# Make sure the server is still operational.
+get_pids
+if [ ${_GET_PIDS_NUM} -ne 1 ]; then
+    printf "ERROR: Kea process was killed when attempting reconfiguration.\n"
+    clean_exit 1
+fi
+
+# All ok. Shut down Kea and exit.
+clean_exit 0

+ 110 - 0
src/bin/dhcp6/tests/dhcp6_shutdown_test.sh

@@ -0,0 +1,110 @@
+# Copyright (C) 2014 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.
+
+if [ $# -ne 2 ]; then
+    printf "USAGE: dhcp6_shutdown_test.sh <test_name> <signal_num>\n"
+    exit 1
+fi
+
+# Test name
+TEST_NAME=$1
+# Signal number to be used for this test.
+SIG_NUM=$2
+# Path to the temporary configuration file.
+CFG_FILE="test_config.json"
+# Path to the Kea log file.
+LOG_FILE="test.log"
+# Kea configuration to be stored in the configuration file.
+CONFIG="{
+    \"Dhcp6\":
+    {
+        \"interfaces\": [ ],
+        \"preferred-lifetime\": 3000,
+        \"valid-lifetime\": 4000,
+        \"renew-timer\": 1000,
+        \"rebind-timer\": 2000,
+        \"lease-database\":
+        {
+            \"type\": \"memfile\",
+            \"persist\": false
+        },
+        \"subnet6\": [
+        {
+            \"subnet\": \"2001:db8:1::/64\",
+            \"pool\": [ \"2001:db8:1::10-2001:db8:1::100\" ]
+        } ]
+    }
+}"
+
+# Set the location of the executable.
+BIN="b10-dhcp6"
+BIN_PATH=".."
+
+# Import common test library.
+. $(dirname $0)/../../../lib/testutils/dhcp_test_lib.sh
+
+# Log the start of the test and print test name.
+test_start
+# Remove dangling Kea instances and remove log files.
+cleanup
+# Create new configuration file.
+create_config "${CONFIG}"
+# Instruct Kea to log to the specific file.
+set_logger
+# Start Kea.
+start_kea
+# Wait up to 20s for Kea to start.
+wait_for_kea 20
+if [ ${_WAIT_FOR_KEA} -eq 0 ]; then
+    printf "ERROR: timeout waiting for Kea to start.\n"
+    clean_exit 1
+fi
+
+# Check if it is still running. It could have terminated (e.g. as a result
+# of configuration failure).
+get_pids
+if [ ${_GET_PIDS_NUM} -ne 1 ]; then
+    printf "ERROR: expected one Kea process to be started. Found %d processes started.\n" ${_GET_PIDS_NUM}
+    clean_exit 1
+fi
+
+# Check in the log file, how many times server has been configured. It should
+# be just once on startup.
+get_reconfigs
+if [ ${_GET_RECONFIGS} -ne 1 ]; then
+    printf "ERROR: server hasn't been configured.\n"
+    clean_exit 1
+else
+    printf "Server successfully configured.\n"
+fi
+
+# Send signal to Kea (SIGTERM, SIGINT etc.)
+send_signal ${SIG_NUM}
+
+# Wait up to 10s for the server's graceful shutdown. The graceful shut down
+# should be recorded in the log file with the appropriate message.
+wait_for_message 10 "DHCP6_SHUTDOWN" 1
+if [ ${_WAIT_FOR_MESSAGE} -eq 0 ]; then
+    printf "ERROR: Server did not record shutdown in the log.\n"
+    clean_exit 1
+fi
+
+# Server should have shut down.
+get_pids
+if [ ${_GET_PIDS_NUM} -ne 0 ]; then
+    printf "ERROR: Kea did not shut down after receiving signal.\n" ${_GET_PIDS_NUM}
+    clean_exit 1
+fi
+
+clean_exit 0

+ 16 - 0
src/bin/dhcp6/tests/dhcp6_sigint_test.sh

@@ -0,0 +1,16 @@
+# Copyright (C) 2014 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.
+
+# Run a test that sends SIGINT to Kea and checks if it shuts down gracefully.
+$(dirname $0)/dhcp6_shutdown_test.sh "Sigint" 2

+ 16 - 0
src/bin/dhcp6/tests/dhcp6_sigterm_test.sh

@@ -0,0 +1,16 @@
+# Copyright (C) 2014 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.
+
+# Run a test that sends SIGTERM to Kea and checks if it shuts down gracefully.
+$(dirname $0)/dhcp6_shutdown_test.sh "Sigterm" 15

+ 0 - 17
src/bin/dhcp6/tests/dhcp6_test.py

@@ -210,22 +210,5 @@ class TestDhcpv6Daemon(unittest.TestCase):
         # Check that there is an error message about invalid port number printed on stderr
         self.assertEqual( str(error).count("Failed to parse port number"), 1)
 
-    def test_portnumber_nonroot(self):
-        print("Check that specifying unprivileged port number will work.")
-
-        # Check that there is a message about running with an unprivileged port
-        (returncode, output, error) = self.runCommand(['../b10-dhcp6', '-v', '-s', '-p', '10547'])
-        output_text = str(output) + str(error)
-        self.assertEqual(output_text.count("DHCP6_OPEN_SOCKET opening sockets on port 10547"), 1)
-
-    def test_skip_msgq(self):
-        print("Check that connection to BIND10 msgq can be disabled.")
-
-        # Check that the system outputs a message on one of its streams about running
-        # standalone.
-        (returncode, output, error) = self.runCommand(['../b10-dhcp6', '-v', '-s', '-p', '10547'])
-        output_text = str(output) + str(error)
-        self.assertEqual(output_text.count("DHCP6_STANDALONE"), 1)
-
 if __name__ == '__main__':
     unittest.main()

+ 2 - 2
src/lib/cc/data.cc

@@ -736,8 +736,8 @@ Element::fromJSONFile(const std::string& file_name,
     if (!infile.is_open())
     {
         const char* error = strerror(errno);
-        isc_throw(InvalidOperation, "Failed to read file " << file_name
-                  << ",error:" << error);
+        isc_throw(InvalidOperation, "Failed to read file '" << file_name
+                  << "', error:" << error);
     }
 
     return (fromJSON(infile, file_name, preproc));

+ 5 - 1
src/lib/dhcpsrv/daemon.cc

@@ -24,10 +24,14 @@
 namespace isc {
 namespace dhcp {
 
+// This is an initial config file location.
+std::string Daemon::config_file_ = "";
+
 Daemon::Daemon() {
 }
 
-void Daemon::init(const std::string&) {
+void Daemon::init(const std::string& config_file) {
+    config_file_ = config_file;
 }
 
 void Daemon::cleanup() {

+ 31 - 7
src/lib/dhcpsrv/daemon.h

@@ -34,6 +34,16 @@ namespace dhcp {
 /// Dhcpv6Srv) in tests, without going through the hassles of implemeting stub
 /// methods.
 ///
+/// This class comprises a static object holding a location of the configuration
+/// file. The object must be static because it is instantiated by the signal
+/// handler functions, which are static by their nature. The signal handlers
+/// are used to reconfigure a running server and they need access to the
+/// configuration file location. They get this access by calling
+/// @c Daemon::getConfigFile function.
+///
+/// By default, the configuration file location is empty and its actual value
+/// is assigned to the static object in @c Daemon::init function.
+///
 /// @note Only one instance of this class is instantiated as it encompasses
 ///       the whole operation of the server.  Nothing, however, enforces the
 ///       singleton status of the object.
@@ -44,13 +54,11 @@ public:
     ///
     /// Currently it does nothing.
     Daemon();
-    
+
     /// @brief Initializes the server.
     ///
     /// Depending on the configuration backend, it establishes msgq session,
-    /// or reads the 
-    /// Creates session that will be used to receive commands and updated
-    /// configuration from cfgmgr (or indirectly from user via bindctl).
+    /// or reads the configuration file.
     ///
     /// Note: This function may throw to report enountered problems. It may
     /// also return false if the initialization was skipped. That may seem
@@ -62,6 +70,8 @@ public:
     /// decide that it is not needed and will shut down.
     ///
     /// @note this method may throw
+    ///
+    /// @param config_file Config file name (may be empty if unused).
     virtual void init(const std::string& config_file);
 
     /// @brief Performs final deconfiguration.
@@ -83,10 +93,24 @@ public:
     /// virtual destructor as well.
     virtual ~Daemon();
 
-    /// Initializez logger
+    /// @brief Returns config file name.
+    static std::string getConfigFile() {
+        return (config_file_);
+    }
+
+    /// Initializes logger
+    ///
+    /// This method initializes logger. Currently its implementation is specific
+    /// to each configuration backend.
     ///
-    /// This method initializes logger. I
-    static void loggerInit(const char* log_name, bool verbose, bool stand_alone);
+    /// @param log_name name used in logger initialization
+    /// @param verbose verbose mode (true usually enables DEBUG messages)
+    static void loggerInit(const char* log_name, bool verbose);
+
+private:
+
+    /// @brief Config file name or empty if config file not used.
+    static std::string config_file_;
 };
 
 }; // end of isc::dhcp namespace

+ 5 - 0
src/lib/dns/tsig.h

@@ -16,6 +16,7 @@
 #define TSIG_H 1
 
 #include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
 
 #include <exceptions/exceptions.h>
 
@@ -430,6 +431,10 @@ private:
     struct TSIGContextImpl;
     TSIGContextImpl* impl_;
 };
+
+typedef boost::shared_ptr<TSIGContext> TSIGContextPtr;
+typedef boost::shared_ptr<TSIGKey> TSIGKeyPtr;
+
 }
 }
 

+ 4 - 1
src/lib/testutils/Makefile.am

@@ -15,4 +15,7 @@ libkea_testutils_la_LIBADD  = $(top_builddir)/src/lib/asiolink/libkea-asiolink.l
 libkea_testutils_la_LIBADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
 endif
 
-EXTRA_DIST = portconfig.h socket_request.h
+# Include common libraries being used by shell-based tests.
+SHLIBS = dhcp_test_lib.sh
+
+EXTRA_DIST = portconfig.h socket_request.h $(SHLIBS)

+ 191 - 0
src/lib/testutils/dhcp_test_lib.sh

@@ -0,0 +1,191 @@
+# Copyright (C) 2014 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.
+
+# The following two parameters must to be specified in a script
+# including this library.
+# - BIN - Name of the Kea executable (excluding a path), e.g. b10-dhcp6
+# - BIN_PATH - Path to the Kea executable (excluding an executable name),
+#              e.g. ../
+
+# Begins a test by prining its name.
+# It requires the ${TEST_NAME} variable to hold the test name.
+test_start() {
+    printf "\nSTART TEST ${TEST_NAME}\n"
+}
+
+# Stores the configuration specified as a parameter in the configuration
+# file which name has been set in the ${CFG_FILE} variable.
+create_config() {
+    printf "Creating Kea configuration file: %s.\n" ${CFG_FILE}
+    printf "%b" ${1} > ${CFG_FILE}
+}
+
+# Sets Kea logger to write to the file specified by the global value
+# ${LOG_FILE}.
+set_logger() {
+    printf "Kea log will be stored in %s.\n" ${LOG_FILE}
+    export B10_LOGGER_DESTINATION=${LOG_FILE}
+}
+
+# Returns the number of running process pids and the list of pids.
+_GET_PIDS=     # Return value: holds space separated list of DHCPv6 pids.
+_GET_PIDS_NUM= # Return value: holds the number of DHCPv6 server pids.
+get_pids() {
+    _GET_PIDS=`ps axwwo pid,command | grep ${BIN} | grep -v grep | awk '{print $1}'`
+    _GET_PIDS_NUM=`printf "%s" "${_GET_PIDS}" | wc -w | awk '{print $1}'`
+}
+
+# Returns the number of occurrences of the Kea log message in the
+# log file.
+_GET_LOG_MESSAGES= # Holds the number of log message occurrences.
+get_log_messages() {
+    # Grep log file for the logger message occurrences.
+    _GET_LOG_MESSAGES=`grep -o ${1} ${LOG_FILE} | wc -w`
+    # Remove whitespaces.
+    ${_GET_LOG_MESSAGES##*[! ]}
+}
+
+# Returns the number of server configurations performed so far. Also
+# returns the number of configuration errors.
+_GET_RECONFIGS=        # Return value: number of configurations so far.
+_GET_RECONFIG_ERRORS=  # Return value: number of configuration errors.
+get_reconfigs() {
+    # Grep log file for DHCP6_CONFIG_COMPLETE occurences. There should
+    # be one occurence per (re)configuration.
+    _GET_RECONFIGS=`grep -o DHCP6_CONFIG_COMPLETE ${LOG_FILE} | wc -w`
+    # Grep log file for DHCP6_CONFIG_LOAD_FAIL to check for configuration
+    # failures.
+    _GET_RECONFIG_ERRORS=`grep -o DHCP6_CONFIG_LOAD_FAIL ${LOG_FILE} | wc -w`
+    # Remove whitespaces
+    ${_GET_RECONFIGS##*[! ]}
+    ${_GET_RECONFIG_ERRORS##*[! ]}
+}
+
+# Performs cleanup for a test.
+# It shuts down running Kea processes and removes temporary files.
+# The location of the log file and the configuration file should be set
+# in the ${LOG_FILE} and ${CFG_FILE} variables recpectively, prior to
+# calling this function.
+cleanup() {
+    get_pids
+    # Shut down running Kea processes.
+    for pid in ${_GET_PIDS}
+    do
+        printf "Shutting down Kea proccess having pid %d.\n" ${pid}
+        kill -9 ${pid}
+    done
+    # Remove temporary files.
+    rm -rf ${LOG_FILE}
+    rm -rf ${CFG_FILE}
+}
+
+# Exists the test in the clean way.
+# It peformes the cleanup and prints whether the test has passed or failed.
+# If a test fails, the Kea log is dumped.
+clean_exit() {
+    exit_code=${1}  # Exit code to be returned by the exit function.
+    if [ ${exit_code} -eq 0 ]; then
+        cleanup
+        printf "PASSED ${TEST_NAME}\n\n"
+    else
+        # Dump log file if exists for debugging purposes.
+        if [ -s ${LOG_FILE} ]; then
+            printf "Log file dump:\n"
+            cat ${LOG_FILE}
+        fi
+        cleanup
+        printf "FAILED ${TEST_NAME}\n\n"
+    fi
+    exit ${exit_code}
+}
+
+# Starts Kea process in background using a configuration file specified
+# in the global variable ${CFG_FILE}
+start_kea() {
+    printf "Running command %s.\n" "\"${BIN_PATH}/${BIN} -c ${CFG_FILE}\""
+    ${BIN_PATH}/$BIN -c ${CFG_FILE} &
+}
+
+# Waits for Kea to startup with timeout.
+# This function repeatedly checs if the Kea log file has been created
+# and is non-empty. If it is, the function assumes that Kea has started.
+# It doesn't check the contents of the log file though.
+# If the log file doesn't exist the function sleeps for a second and
+# checks again. This is repeated until timeout is reached or non-empty
+# log file is found. If timeout is reached, the function reports an
+# error.
+_WAIT_FOR_KEA=0  # Return value: Holds 0 if Kea hasn't started, 1 otherwise
+wait_for_kea() {
+    timeout=${1} # Desired timeout in seconds.
+    loops=0 # Loops counter
+    _WAIT_FOR_KEA=0
+    while [ ! -s ${LOG_FILE} ] && [ ${loops} -le ${timeout} ]; do
+        printf "."
+        sleep 1
+        loops=`expr $loops + 1`
+    done
+    printf "\n"
+    if [ ${loops} -le ${timeout} ]; then
+        _WAIT_FOR_KEA=1
+    fi
+}
+
+# Waits for a specific message to occur in the Kea log file.
+# This function is called when the test expects specific message
+# to show up in the log file as a result of some action that has
+# been taken. Typically, the test expects that the message
+# is logged when the SIGHUP or SIGTERM signal has been sent to the
+# Kea process.
+# This function waits a specified number of seconds for the number
+# of message occurrences to show up. If the expected number of
+# message doesn't occur, the error status is returned.
+_WAIT_FOR_MESSAGE=0  # Return value: holds 0 if the message hasn't occured,
+                     # 1 otherwise.
+wait_for_message() {
+    timeout=${1}     # Expecte timeout value in seconds.
+    message=${2}     # Expected message id.
+    occurrences=${3} # Number of expected occurrences.
+    loops=0          # Number of loops performed so far.
+    _WAIT_FOR_MESSAGE=0
+    # Check if log file exists and if we reached timeout.
+    while [ ! -s {LOG_FILE} ] && [ ${loops} -le ${timeout} ]; do
+        printf "."
+        # Check if the message has been logged.
+        get_log_messages ${message}
+        if [ ${_GET_LOG_MESSAGES} -eq ${occurrences} ]; then
+            printf "\n"
+            _WAIT_FOR_MESSAGE=1
+            return
+        fi
+        # Message not recorded. Keep going.
+        sleep 1
+        loops=`expr ${loops} + 1`
+    done
+    printf "\n"
+    # Timeout.
+}
+
+# Sends specified signal to the Kea process.
+send_signal() {
+    sig=${1}  # Signal number.
+    # Get Kea pid.
+    get_pids
+    if [ ${_GET_PIDS_NUM} -ne 1 ]; then
+        printf "ERROR: expected one Kea process to be started. Found %d processes started.\n" ${_GET_PIDS_NUM}
+        clean_exit 1
+    fi
+    printf "Sending signal ${sig} to Kea process (pid=%s).\n" ${_GET_PIDS}
+    # Actually send a signal.
+    kill -${sig} ${_GET_PIDS}
+}