Browse Source

[master] DDNS parameter, replace-client-name, now supports multiple modes

    Merges in trac4259.
Thomas Markwalder 9 years ago
parent
commit
45e56d7aa0

+ 44 - 20
doc/guide/dhcp4-srv.xml

@@ -129,7 +129,7 @@ strings <userinput>path</userinput>/kea-dhcp4 | sed -n 's/;;;; //p'
         If the file already exists and contains the PID of a live process,
         the server will issue a DHCP4_ALREADY_RUNNING log message and exit. It
         is possible, though unlikely, that the file is a remnant of a system crash
-        and the process to which the PID belongs is unrelated to Kea.  In such a 
+        and the process to which the PID belongs is unrelated to Kea.  In such a
         case it would be necessary to manually delete the PID file.
       </para>
 
@@ -1777,7 +1777,7 @@ It is merely echoed by the server
     </section>
 
     <section id="dhcp4-ddns-config">
-      <title>Configuring DHCPv4 for DDNS</title>
+      <title>DDNS for DHCPv4</title>
       <para>
       As mentioned earlier, kea-dhcp4 can be configured to generate requests to the
       DHCP-DDNS server (referred to here as "D2" ) to update DNS entries.  These requests are known as
@@ -1852,7 +1852,7 @@ It is merely echoed by the server
       <command>"override-client-update": false</command>
       </simpara></listitem>
       <listitem><simpara>
-      <command>"replace-client-name": false</command>
+      <command>"replace-client-name": "never"</command>
       </simpara></listitem>
       <listitem><simpara>
       <command>"generated-prefix": "myhost"</command>
@@ -2078,38 +2078,62 @@ It is merely echoed by the server
       supply a portion or all of that name based upon what it receives from
       the client in the DHCP REQUEST.</para>
       <para>
-      The rules for determining the FQDN option are as follows:
+       The default rules for constructing the FQDN that will be used for DNS
+       entries are:
       <orderedlist>
       <listitem><para>
-      If configured to do, so ignore the DHCPREQUEST contents and generate a
-      FQDN using a configurable prefix and suffix.
+        If the DHCPREQUEST contains the client FQDN option, the candidate name
+        is taken from there, otherwise it is taken from the Host Name option.
       </para></listitem>
       <listitem><para>
-      If the DHCPREQUEST contains the client FQDN option, the candidate
-      name is taken from there, otherwise it is taken from the Host Name option.
-      The candidate name may then be modified:
-      <orderedlist>
-      <listitem><para>
-      If the candidate name is a fully qualified domain name, use it.
+        If the candidate name is a partial (i.e. unqualified) name then add a
+        configurable suffix to the name and use the result as the FQDN.
       </para></listitem>
       <listitem><para>
-      If the candidate name is a partial (i.e. unqualified) name then
-      add a configurable suffix to the name and use the result as the FQDN.
+        If the candidate name provided is empty, generate a FQDN using a
+        configurable prefix and suffix.
       </para></listitem>
       <listitem><para>
-      If the candidate name is a empty, generate a FQDN using a
-      configurable prefix and suffix.
+        If the client provided neither option, then no DNS action will be taken.
       </para></listitem>
       </orderedlist>
+        These rules can amended by setting the
+        <command>replace-client-name</command> parameter which provides the
+        following modes of behavior:
+      <itemizedlist>
+      <listitem><para>
+        <command>never</command> - Use the name the client sent.  If the client
+        sent no name, do not generate one.  This is the default mode.
       </para></listitem>
-      </orderedlist>
-      To instruct kea-dhcp4 to always generate the FQDN for a client, set the
-      parameter <command>replace-client-name</command> to true as follows:
+      <listitem><para>
+        <command>always</command> - Replace the name the client sent. If the
+        client sent no name, generate one for the client.
+      </para></listitem>
+      <listitem><para>
+        <command>when-present</command> - Replace the name the client sent.
+        If the client sent no name, do not generate one.
+      </para></listitem>
+      <listitem><para>
+        <command>when-not-present</command> - Use the name the client sent.
+        If the client sent no name, generate one for the client.
+      </para></listitem>
+      </itemizedlist>
+    <note>
+    Note that formerly, this parameter was a boolean and permitted only values
+    of <command>true</command> and <command>false</command>.  Boolean values
+    will still be accepted but may eventually be deprecated. A value of
+    <command>true</command> equates to <command>when-present</command>,
+    <command>false</command> equates to <command>never</command>.
+    </note>
+
+      For example, To instruct kea-dhcp4 to always generate the FQDN for a
+      client, set the parameter <command>replace-client-name</command> to
+      <command>always</command> as follows:
       </para>
 <screen>
 "Dhcp4": {
     "dhcp-ddns": {
-        <userinput>"replace-client-name": true</userinput>,
+        <userinput>"replace-client-name": "always"</userinput>,
         ...
     },
     ...

+ 51 - 26
doc/guide/dhcp6-srv.xml

@@ -1744,7 +1744,7 @@ should include options from the isc option space:
     </section>
 
     <section id="dhcp6-ddns-config">
-      <title>Configuring DHCPv6 for DDNS</title>
+      <title>DDNS for DHCPv6</title>
       <para>
       As mentioned earlier, kea-dhcp6 can be configured to generate requests to
       the DHCP-DDNS server (referred to here as "D2") to update
@@ -1820,7 +1820,7 @@ should include options from the isc option space:
       <command>"override-client-update": false</command>
       </simpara></listitem>
       <listitem><simpara>
-      <command>"replace-client-name": false</command>
+      <command>"replace-client-name": "never"</command>
       </simpara></listitem>
       <listitem><simpara>
       <command>"generated-prefix": "myhost"</command>
@@ -1926,14 +1926,14 @@ should include options from the isc option space:
       In general, kea-dhcp6 will generate DDNS update requests when:
       <orderedlist>
       <listitem><para>
-      A new lease is granted in response to a DHCP REQUEST
+      A new lease is granted in response to a REQUEST
       </para></listitem>
       <listitem><para>
       An existing lease is renewed but the FQDN associated with it has
       changed.
       </para></listitem>
       <listitem><para>
-      An existing lease is released in response to a DHCP RELEASE
+      An existing lease is released in response to a RELEASE
       </para></listitem>
       </orderedlist>
       In the second case, lease renewal, two  DDNS requests will be issued: one
@@ -1944,7 +1944,7 @@ should include options from the isc option space:
       discussed next.
       </para>
       <para>
-      kea-dhcp6 will generate a DDNS update request only if the DHCP REQUEST
+      kea-dhcp6 will generate a DDNS update request only if the REQUEST
       contains the FQDN option (code 39). By default kea-dhcp6 will
       respect the FQDN N and S flags specified by the client as shown in the
       following table:
@@ -2041,42 +2041,67 @@ should include options from the isc option space:
       <section id="dhcpv6-fqdn-name-generation">
       <title>kea-dhcp6 name generation for DDNS update requests</title>
 
-      <para>Each NameChangeRequest must of course include the fully qualified domain
-      name whose DNS entries are to be affected.  kea-dhcp6 can be configured to
-      supply a portion or all of that name based upon what it receives from
-      the client in the DHCP REQUEST.</para>
-
-      <para>The rules for determining the FQDN option are as follows:
+      <para>Each NameChangeRequest must of course include the fully qualified
+      domain name whose DNS entries are to be affected.  kea-dhcp6 can be
+      configured to supply a portion or all of that name based upon what it
+      receives from the client.</para>
+      <para>
+       The default rules for constructing the FQDN that will be used for DNS
+       entries are:
       <orderedlist>
       <listitem><para>
-      If configured to do so ignore the REQUEST contents and generate a
-      FQDN using a configurable prefix and suffix.
+        If the DHCPREQUEST contains the client FQDN option, the candidate name
+        is taken from there.
       </para></listitem>
       <listitem><para>
-      Otherwise, using the domain name value from the client FQDN option as
-      the candidate name:
-      <orderedlist>
-      <listitem><para>
-      If the candidate name is a fully qualified domain name then use it.
+        If the candidate name is a partial (i.e. unqualified) name then add a
+        configurable suffix to the name and use the result as the FQDN.
       </para></listitem>
       <listitem><para>
-      If the candidate name is a partial (i.e. unqualified) name then
-      add a configurable suffix to the name and use the result as the FQDN.
+        If the candidate name provided is empty, generate a FQDN using a
+        configurable prefix and suffix.
       </para></listitem>
       <listitem><para>
-      If the candidate name is a empty then generate a FQDN using a
-      configurable prefix and suffix.
+        If the client provided neither option, then no DNS action will be taken.
       </para></listitem>
       </orderedlist>
+        These rules can amended by setting the
+        <command>replace-client-name</command> parameter which provides the
+        following modes of behavior:
+      <itemizedlist>
+      <listitem><para>
+        <command>never</command> - Use the name the client sent.  If the client
+        sent no name, do not generate one.  This is the default mode.
       </para></listitem>
-      </orderedlist>
-      To instruct kea-dhcp6 to always generate a FQDN, set the parameter
-      "replace-client-name" to true:
+      <listitem><para>
+        <command>always</command> - Replace the name the client sent. If the
+        client sent no name, generate one for the client.
+      </para></listitem>
+      <listitem><para>
+        <command>when-present</command> - Replace the name the client sent.
+        If the client sent no name, do not generate one.
+      </para></listitem>
+      <listitem><para>
+        <command>when-not-present</command> - Use the name the client sent.
+        If the client sent no name, generate one for the client.
+      </para></listitem>
+      </itemizedlist>
+    <note>
+    Note that formerly, this parameter was a boolean and permitted only values
+    of <command>true</command> and <command>false</command>.  Boolean values
+    will still be accepted but may eventually be deprecated. A value of
+    <command>true</command> equates to <command>when-present</command>,
+    <command>false</command> equates to <command>never</command>.
+    </note>
+
+      For example, To instruct kea-dhcp6 to always generate the FQDN for a
+      client, set the parameter <command>replace-client-name</command> to
+      <command>always</command> as follows:
       </para>
 <screen>
 "Dhcp6": {
     "dhcp-ddns": {
-        <userinput>"replace-client-name": true</userinput>,
+        <userinput>"replace-client-name": "always"</userinput>,
         ...
     },
     ...

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

@@ -607,9 +607,9 @@
             },
             {
                 "item_name": "replace-client-name",
-                "item_type": "boolean",
+                "item_type": "string",
                 "item_optional": true,
-                "item_default": false,
+                "item_default": "never",
                 "item_description": "Should server replace the domain-name supplied by the client"
             },
             {

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

@@ -1,4 +1,4 @@
-# Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC")
+# Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
 #
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -217,6 +217,11 @@ from a client. Server does not process empty Hostname options and therefore
 option is skipped. The argument holds the client and transaction identification
 information.
 
+% DHCP4_GENERATE_FQDN %1: client did not send a FQDN or hostname; FQDN will be be generated for the client
+This debug message is issued when the server did not receive a Hostname option
+from the client and hostname generation is enabled.  This provides a means to
+create DNS entries for unsophisticated clients.
+
 % DHCP4_HANDLE_SIGNAL_EXCEPTION An exception was thrown while handing signal: %1
 This error message is printed when an ISC or standard exception was raised during signal
 processing. This likely indicates a coding error and should be reported to ISC.

+ 38 - 17
src/bin/dhcp4/dhcp4_srv.cc

@@ -1073,13 +1073,10 @@ Dhcpv4Srv::processClientName(Dhcpv4Exchange& ex) {
             processClientFqdnOption(ex);
 
         } else {
-            OptionStringPtr hostname = boost::dynamic_pointer_cast<OptionString>
-                (ex.getQuery()->getOption(DHO_HOST_NAME));
-            if (hostname) {
-                LOG_DEBUG(ddns4_logger, DBG_DHCP4_DETAIL, DHCP4_CLIENT_HOSTNAME_PROCESS)
+            LOG_DEBUG(ddns4_logger, DBG_DHCP4_DETAIL,
+                      DHCP4_CLIENT_HOSTNAME_PROCESS)
                     .arg(ex.getQuery()->getLabel());
-                processHostnameOption(ex);
-            }
+            processHostnameOption(ex);
         }
     } catch (const Exception& e) {
         // In some rare cases it is possible that the client's name processing
@@ -1150,14 +1147,6 @@ Dhcpv4Srv::processClientFqdnOption(Dhcpv4Exchange& ex) {
 
 void
 Dhcpv4Srv::processHostnameOption(Dhcpv4Exchange& ex) {
-    // Obtain the Hostname option from the client's message.
-    OptionStringPtr opt_hostname = boost::dynamic_pointer_cast<OptionString>
-        (ex.getQuery()->getOption(DHO_HOST_NAME));
-
-    LOG_DEBUG(ddns4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_CLIENT_HOSTNAME_DATA)
-        .arg(ex.getQuery()->getLabel())
-        .arg(opt_hostname->getValue());
-
     // Fetch D2 configuration.
     D2ClientMgr& d2_mgr = CfgMgr::instance().getD2ClientMgr();
 
@@ -1166,6 +1155,36 @@ Dhcpv4Srv::processHostnameOption(Dhcpv4Exchange& ex) {
         return;
     }
 
+    D2ClientConfig::ReplaceClientNameMode replace_name_mode =
+            d2_mgr.getD2ClientConfig()->getReplaceClientNameMode();
+
+    // Obtain the Hostname option from the client's message.
+    OptionStringPtr opt_hostname = boost::dynamic_pointer_cast<OptionString>
+        (ex.getQuery()->getOption(DHO_HOST_NAME));
+
+    // If we don't have a hostname then either we'll supply it or do nothing.
+    if (!opt_hostname) {
+        // If we're configured to supply it then add it to the response.
+        // Use the root domain to signal later on that we should replace it.
+        if (replace_name_mode == D2ClientConfig::RCM_ALWAYS ||
+            replace_name_mode == D2ClientConfig::RCM_WHEN_NOT_PRESENT) {
+            LOG_DEBUG(ddns4_logger, DBG_DHCP4_DETAIL_DATA,
+                      DHCP4_GENERATE_FQDN)
+                .arg(ex.getQuery()->getLabel());
+            OptionStringPtr opt_hostname_resp(new OptionString(Option::V4,
+                                                               DHO_HOST_NAME,
+                                                               "."));
+            ex.getResponse()->addOption(opt_hostname_resp);
+        }
+
+        return;
+    }
+
+    // Client sent us a hostname option so figure out what to do with it.
+    LOG_DEBUG(ddns4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_CLIENT_HOSTNAME_DATA)
+        .arg(ex.getQuery()->getLabel())
+        .arg(opt_hostname->getValue());
+
     std::string hostname = isc::util::str::trim(opt_hostname->getValue());
     unsigned int label_count = OptionDataTypeUtil::getLabelCount(hostname);
     // The hostname option sent by the client should be at least 1 octet long.
@@ -1195,14 +1214,16 @@ Dhcpv4Srv::processHostnameOption(Dhcpv4Exchange& ex) {
         opt_hostname_resp->setValue(d2_mgr.qualifyName(ex.getContext()->host_->getHostname(),
                                                        false));
 
-    } else if ((d2_mgr.getD2ClientConfig()->getReplaceClientName()) ||
-               (label_count < 2)) {
+    } else if ((replace_name_mode == D2ClientConfig::RCM_ALWAYS ||
+               replace_name_mode == D2ClientConfig::RCM_WHEN_PRESENT)
+               || label_count < 2) {
         // Set to root domain to signal later on that we should replace it.
         // DHO_HOST_NAME is a string option which cannot be empty.
         /// @todo We may want to reconsider whether it is appropriate for the
         /// client to send a root domain name as a Hostname. There are
         /// also extensions to the auto generation of the client's name,
-        /// e.g. conversion to the puny code which may be considered at some point.
+        /// e.g. conversion to the puny code which may be considered at some
+        /// point.
         /// For now, we just remain liberal and expect that the DNS will handle
         /// conversion if needed and possible.
         opt_hostname_resp->setValue(".");

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

@@ -3176,7 +3176,7 @@ TEST_F(Dhcp4ParserTest, d2ClientConfig) {
         "     \"allow-client-update\" : true, "
         "     \"override-no-update\" : true, "
         "     \"override-client-update\" : true, "
-        "     \"replace-client-name\" : true, "
+        "     \"replace-client-name\" : \"when-present\", "
         "     \"generated-prefix\" : \"test.prefix\", "
         "     \"qualifying-suffix\" : \"test.suffix.\" },"
         "\"valid-lifetime\": 4000 }";
@@ -3210,7 +3210,7 @@ TEST_F(Dhcp4ParserTest, d2ClientConfig) {
     EXPECT_TRUE(d2_client_config->getAlwaysIncludeFqdn());
     EXPECT_TRUE(d2_client_config->getOverrideNoUpdate());
     EXPECT_TRUE(d2_client_config->getOverrideClientUpdate());
-    EXPECT_TRUE(d2_client_config->getReplaceClientName());
+    EXPECT_EQ(D2ClientConfig::RCM_WHEN_PRESENT, d2_client_config->getReplaceClientNameMode());
     EXPECT_EQ("test.prefix", d2_client_config->getGeneratedPrefix());
     EXPECT_EQ("test.suffix.", d2_client_config->getQualifyingSuffix());
 }
@@ -3238,7 +3238,7 @@ TEST_F(Dhcp4ParserTest, invalidD2ClientConfig) {
         "     \"allow-client-update\" : true, "
         "     \"override-no-update\" : true, "
         "     \"override-client-update\" : true, "
-        "     \"replace-client-name\" : true, "
+        "     \"replace-client-name\" : \"when-present\", "
         "     \"generated-prefix\" : \"test.prefix\", "
         "     \"qualifying-suffix\" : \"test.suffix.\" },"
         "\"valid-lifetime\": 4000 }";

+ 183 - 13
src/bin/dhcp4/tests/fqdn_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -128,6 +128,20 @@ public:
     static const uint16_t OVERRIDE_CLIENT_UPDATE = 4;
     static const uint16_t REPLACE_CLIENT_NAME = 8;
 
+    // Enum used to specify whether a client (packet) should include
+    // the hostname option
+    enum ClientNameFlag {
+        CLIENT_NAME_PRESENT,
+        CLIENT_NAME_NOT_PRESENT
+    };
+
+    // Enum used to specify whether the server should replace/supply
+    // the hostname or not
+    enum ReplacementFlag {
+        NAME_REPLACED,
+        NAME_NOT_REPLACED
+    };
+
     NameDhcpv4SrvTest()
         : Dhcpv4SrvTest(),
           d2_mgr_(CfgMgr::instance().getD2ClientMgr()),
@@ -172,7 +186,9 @@ public:
                                   (mask & ALWAYS_INCLUDE_FQDN),
                                   (mask & OVERRIDE_NO_UPDATE),
                                   (mask & OVERRIDE_CLIENT_UPDATE),
-                                  (mask & REPLACE_CLIENT_NAME),
+                                  ((mask & REPLACE_CLIENT_NAME) ?
+                                    D2ClientConfig::RCM_WHEN_PRESENT
+                                   : D2ClientConfig::RCM_NEVER),
                                   "myhost", "example.com")));
         ASSERT_NO_THROW(CfgMgr::instance().setD2ClientConfig(cfg));
         ASSERT_NO_THROW(srv_->startD2());
@@ -184,7 +200,8 @@ public:
                           const bool fqdn_fwd,
                           const bool fqdn_rev) {
         const uint8_t hwaddr_data[] = { 0, 1, 2, 3, 4, 5, 6 };
-        HWAddrPtr hwaddr(new HWAddr(hwaddr_data, sizeof(hwaddr_data), HTYPE_ETHER));
+        HWAddrPtr hwaddr(new HWAddr(hwaddr_data, sizeof(hwaddr_data),
+                                    HTYPE_ETHER));
         Lease4Ptr lease(new Lease4(addr, hwaddr,
                                    &generateClientId()->getData()[0],
                                    generateClientId()->getData().size(),
@@ -309,6 +326,20 @@ public:
 
     }
 
+    // Create a message holding of a given type
+    Pkt4Ptr generatePkt(const uint8_t msg_type) {
+        Pkt4Ptr pkt = Pkt4Ptr(new Pkt4(msg_type, 1234));
+        pkt->setRemoteAddr(IOAddress("192.0.2.3"));
+        // For DISCOVER we don't include server id, because client broadcasts
+        // the message to all servers.
+        if (msg_type != DHCPDISCOVER) {
+            pkt->addOption(srv_->getServerID());
+        }
+
+        pkt->addOption(generateClientId());
+        return (pkt);
+    }
+
     // Test that server generates the appropriate FQDN option in response to
     // client's FQDN option.
     void testProcessFqdn(const Pkt4Ptr& query, const uint8_t exp_flags,
@@ -338,6 +369,79 @@ public:
 
     }
 
+    // Test that the server's processes the hostname (or lack thereof)
+    // in a client request correctly, according to the replace-client-name
+    // mode configuration parameter.
+    //
+    // @param mode - value to use client-name-replacment parameter - for
+    // mode labels such as NEVER and ALWAYS must incluce enclosing quotes:
+    // "\"NEVER\"".  This allows us to also pass in boolean literals which
+    // are unquoted.
+    // @param client_name_flag - specifies whether or not the client request
+    // should contain a hostname option
+    // @param exp_replacement_flag - specifies whether or not the server is
+    // expected to replace (or supply) the hostname in its response
+    void testReplaceClientNameMode(const char* mode,
+                                   enum ClientNameFlag client_name_flag,
+                                   enum ReplacementFlag exp_replacement_flag) {
+        // Configuration "template" with a replaceable mode parameter
+        const char* config_template =
+            "{ \"interfaces-config\": {"
+            "      \"interfaces\": [ \"*\" ]"
+            "},"
+            "\"valid-lifetime\": 3000,"
+            "\"subnet4\": [ { "
+            "    \"subnet\": \"10.0.0.0/24\", "
+            "    \"id\": 1,"
+            "    \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.10\" } ]"
+            " }],"
+            "\"dhcp-ddns\": {"
+            "\"enable-updates\": true,"
+            "\"qualifying-suffix\": \"fake-suffix.isc.org.\","
+            "\"replace-client-name\": %s"
+            "}}";
+
+        // Create the configuration and configure the server
+        char config_buf[1024];
+        sprintf(config_buf, config_template, mode);
+        ASSERT_NO_THROW(configure(config_buf, srv_)) << "configuration failed";
+
+        // Build our client packet
+        Pkt4Ptr query;
+        if (client_name_flag == CLIENT_NAME_PRESENT) {
+            ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST,
+                                                            "my.example.com."));
+        } else {
+            ASSERT_NO_THROW(query = generatePkt(DHCPREQUEST));
+        }
+
+        // Run the packet through the server, extracting the hostname option
+        // from the response.  If the option isn't present the returned pointer
+        // will be null.
+        OptionStringPtr hostname;
+        ASSERT_NO_THROW(
+            hostname = processHostname(query,
+                                       client_name_flag == CLIENT_NAME_PRESENT)
+        ) << "processHostname throw an exception";
+
+        // Verify the contents (or lack thereof) of the hostname
+        if (exp_replacement_flag == NAME_REPLACED) {
+            ASSERT_TRUE(hostname)
+                << "No host name, it should have the replacement name \".\"";
+            EXPECT_EQ(".", hostname->getValue());
+        } else {
+            if (client_name_flag == CLIENT_NAME_PRESENT) {
+                ASSERT_TRUE(hostname)
+                    << "No host name, expected original from client";
+                EXPECT_EQ("my.example.com.", hostname->getValue());
+            } else {
+                ASSERT_FALSE(hostname)
+                    << "Host name is: " << hostname
+                    << ", it should have been null";
+            }
+        }
+    }
+
     /// @brief Checks the packet's FQDN option flags against a given mask
     ///
     /// @param pkt IPv4 packet whose FQDN flags should be checked.
@@ -358,12 +462,20 @@ public:
     }
 
 
-    // Processes the Hostname option in the client's message and returns
-    // the hostname option which would be sent to the client. It will
-    // throw NULL pointer if the hostname option is not to be included
-    // in the response.
-    OptionStringPtr processHostname(const Pkt4Ptr& query) {
-        if (!getHostnameOption(query)) {
+    /// @brief  Invokes Dhcpv4Srv::processHostname on the given packet
+    ///
+    /// Processes the Hostname option in the client's message and returns
+    /// the hostname option which would be sent to the client. It will
+    /// return empty if the hostname option is not to be included
+    /// server's response.
+    /// @param query - client packet to process
+    /// @param must_have_host - flag indicating whether or not the client
+    /// packet must contain the hostname option
+    ///
+    /// @return a pointer to the hostname option constructed by the server
+    OptionStringPtr processHostname(const Pkt4Ptr& query,
+                                    bool must_have_host = true) {
+        if (!getHostnameOption(query) && must_have_host) {
             ADD_FAILURE() << "Hostname option not carried in the query";
         }
 
@@ -383,7 +495,26 @@ public:
 
     }
 
-    // Verify that NameChangeRequest holds valid values.
+    ///@brief Verify that NameChangeRequest holds valid values.
+    ///
+    /// Pulls the NCR from the top of the send queue and checks it's content
+    ///  against a number of expected parameters.
+    ///
+    /// @param type - expected NCR change type, CHG_ADD or CHG_REMOVE
+    /// @param reverse - flag indicating whether or not the NCR specifies
+    /// reverse change
+    /// @param forward - flag indication whether or not the NCR specifies
+    /// forward change
+    /// @param addr  - expected lease address in the NCR
+    /// @param fqdn  - expected FQDN in the NCR
+    /// @param dhcid - expected DHCID in the NCR (comparison is performed only
+    /// if the value supplied is not empty):w
+    /// @param cltt - cltt value from the lease the NCR for which the NCR
+    /// was generated expected value for
+    /// @param len - expected lease length in the NCR
+    /// @param not_strict_expire_check - when true the comparison of the NCR
+    /// lease expiration time is conducted as greater than or equal to rather
+    /// equal to CLTT plus lease lenght.
     void verifyNameChangeRequest(const isc::dhcp_ddns::NameChangeType type,
                                  const bool reverse, const bool forward,
                                  const std::string& addr,
@@ -408,9 +539,9 @@ public:
         }
         // In some cases, the test doesn't have access to the last transmission
         // time for the particular client. In such cases, the test can use the
-        // current time as cltt but the it may not check the lease expiration time
-        // for equality but rather check that the lease expiration time is not
-        // greater than the current time + lease lifetime.
+        // current time as cltt but the it may not check the lease expiration
+        // time for equality but rather check that the lease expiration time
+        // is not greater than the current time + lease lifetime.
         if (not_strict_expire_check) {
             EXPECT_GE(cltt + len, ncr->getLeaseExpiresOn());
         } else {
@@ -1281,4 +1412,43 @@ TEST_F(NameDhcpv4SrvTest, emptyFqdn) {
 
 }
 
+// Verifies that the replace-client-name behavior is correct for each of
+// the supported modes.
+TEST_F(NameDhcpv4SrvTest, replaceClientNameModeTest) {
+
+    // We pass mode labels in with enclosing quotes so we can also test
+    // unquoted boolean literals true/false
+    testReplaceClientNameMode("\"never\"",
+                              CLIENT_NAME_NOT_PRESENT, NAME_NOT_REPLACED);
+    testReplaceClientNameMode("\"never\"",
+                              CLIENT_NAME_PRESENT, NAME_NOT_REPLACED);
+
+    testReplaceClientNameMode("\"always\"",
+                              CLIENT_NAME_NOT_PRESENT, NAME_REPLACED);
+    testReplaceClientNameMode("\"always\"",
+                              CLIENT_NAME_PRESENT, NAME_REPLACED);
+
+    testReplaceClientNameMode("\"when-present\"",
+                              CLIENT_NAME_NOT_PRESENT, NAME_NOT_REPLACED);
+    testReplaceClientNameMode("\"when-present\"",
+                              CLIENT_NAME_PRESENT, NAME_REPLACED);
+
+    testReplaceClientNameMode("\"when-not-present\"",
+                              CLIENT_NAME_NOT_PRESENT, NAME_REPLACED);
+    testReplaceClientNameMode("\"when-not-present\"",
+                              CLIENT_NAME_PRESENT, NAME_NOT_REPLACED);
+
+    // Verify that boolean false produces the same result as RCM_NEVER
+    testReplaceClientNameMode("false",
+                              CLIENT_NAME_NOT_PRESENT, NAME_NOT_REPLACED);
+    testReplaceClientNameMode("false",
+                              CLIENT_NAME_PRESENT, NAME_NOT_REPLACED);
+
+    // Verify that boolean true produces the same result as RCM_WHEN_PRESENT
+    testReplaceClientNameMode("true",
+                              CLIENT_NAME_NOT_PRESENT, NAME_NOT_REPLACED);
+    testReplaceClientNameMode("true",
+                              CLIENT_NAME_PRESENT, NAME_REPLACED);
+}
+
 } // end of anonymous namespace

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

@@ -711,9 +711,9 @@
             },
             {
                 "item_name": "replace-client-name",
-                "item_type": "boolean",
+                "item_type": "string",
                 "item_optional": true,
-                "item_default": false,
+                "item_default": "never",
                 "item_description": "Should server replace the domain-name supplied by the client"
             },
             {

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

@@ -1,4 +1,4 @@
-# Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC")
+# Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
 #
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -145,6 +145,12 @@ to the server. The first argument includes the client and
 transaction identification information. The second argument includes
 the generated FQDN.
 
+% DHCP6_DDNS_GENERATE_FQDN %1: client did not send a FQDN option; FQDN will be
+generated for the client.
+This debug message is issued when the server did not receive a FQDN option
+from the client and client name replacement is enabled.  This provides a means
+to create DNS entries for unsophisticated clients.
+
 % DHCP6_DDNS_GENERATED_FQDN_UPDATE_FAIL %1: failed to update the lease using address %2, after generating FQDN for a client, reason: %3
 This message indicates the failure when trying to update the lease and/or
 options in the server's response with the hostname generated by the server

+ 23 - 7
src/bin/dhcp6/dhcp6_srv.cc

@@ -1061,17 +1061,35 @@ Dhcpv6Srv::assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer,
 void
 Dhcpv6Srv::processClientFqdn(const Pkt6Ptr& question, const Pkt6Ptr& answer,
                              AllocEngine::ClientContext6& ctx) {
+    D2ClientMgr& d2_mgr = CfgMgr::instance().getD2ClientMgr();
+
     // Get Client FQDN Option from the client's message. If this option hasn't
     // been included, do nothing.
     Option6ClientFqdnPtr fqdn = boost::dynamic_pointer_cast<
         Option6ClientFqdn>(question->getOption(D6O_CLIENT_FQDN));
     if (!fqdn) {
-        // No FQDN so lease hostname comes from host reservation if one
-        if (ctx.host_) {
-            ctx.hostname_ = ctx.host_->getHostname();
-        }
+        D2ClientConfig::ReplaceClientNameMode replace_name_mode =
+            d2_mgr.getD2ClientConfig()->getReplaceClientNameMode();
+        if (d2_mgr.ddnsEnabled() &&
+            (replace_name_mode == D2ClientConfig::RCM_ALWAYS ||
+             replace_name_mode == D2ClientConfig::RCM_WHEN_NOT_PRESENT)) {
+            // Fabricate an empty "client" FQDN with flags requesting
+            // the server do all the updates.  The flags will get modified
+            // below according the configuration options, the name will
+            // be supplied later on.
+            fqdn.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_S, "",
+                                             Option6ClientFqdn::PARTIAL));
+            LOG_DEBUG(ddns6_logger, DBG_DHCP6_DETAIL, DHCP6_DDNS_GENERATE_FQDN)
+                .arg(question->getLabel());
+        } else {
+            // No FQDN so get the lease hostname from the host reservation if
+            // there is one.
+            if (ctx.host_) {
+                ctx.hostname_ = ctx.host_->getHostname();
+            }
 
-        return;
+            return;
+        }
     }
 
     LOG_DEBUG(ddns6_logger, DBG_DHCP6_DETAIL, DHCP6_DDNS_RECEIVE_FQDN)
@@ -1084,12 +1102,10 @@ Dhcpv6Srv::processClientFqdn(const Pkt6Ptr& question, const Pkt6Ptr& answer,
 
     // Set the server S, N, and O flags based on client's flags and
     // current configuration.
-    D2ClientMgr& d2_mgr = CfgMgr::instance().getD2ClientMgr();
     d2_mgr.adjustFqdnFlags<Option6ClientFqdn>(*fqdn, *fqdn_resp);
 
     // If there's a reservation and it has a hostname specified, use it!
     if (ctx.host_ && !ctx.host_->getHostname().empty()) {
-        D2ClientMgr& d2_mgr = CfgMgr::instance().getD2ClientMgr();
         // Add the qualifying suffix.
         // After #3765, this will only occur if the suffix is not empty.
         fqdn_resp->setDomainName(d2_mgr.qualifyName(ctx.host_->getHostname(),

+ 3 - 3
src/bin/dhcp6/tests/config_parser_unittest.cc

@@ -3410,7 +3410,7 @@ TEST_F(Dhcp6ParserTest, d2ClientConfig) {
         "     \"allow-client-update\" : true, "
         "     \"override-no-update\" : true, "
         "     \"override-client-update\" : true, "
-        "     \"replace-client-name\" : true, "
+        "     \"replace-client-name\" : \"when-present\", "
         "     \"generated-prefix\" : \"test.prefix\", "
         "     \"qualifying-suffix\" : \"test.suffix.\" },"
         "\"valid-lifetime\": 4000 }";
@@ -3444,7 +3444,7 @@ TEST_F(Dhcp6ParserTest, d2ClientConfig) {
     EXPECT_TRUE(d2_client_config->getAlwaysIncludeFqdn());
     EXPECT_TRUE(d2_client_config->getOverrideNoUpdate());
     EXPECT_TRUE(d2_client_config->getOverrideClientUpdate());
-    EXPECT_TRUE(d2_client_config->getReplaceClientName());
+    EXPECT_EQ(D2ClientConfig::RCM_WHEN_PRESENT, d2_client_config->getReplaceClientNameMode());
     EXPECT_EQ("test.prefix", d2_client_config->getGeneratedPrefix());
     EXPECT_EQ("test.suffix.", d2_client_config->getQualifyingSuffix());
 }
@@ -3472,7 +3472,7 @@ TEST_F(Dhcp6ParserTest, invalidD2ClientConfig) {
         "     \"allow-client-update\" : true, "
         "     \"override-no-update\" : true, "
         "     \"override-client-update\" : true, "
-        "     \"replace-client-name\" : true, "
+        "     \"replace-client-name\" : \"when-present\", "
         "     \"generated-prefix\" : \"test.prefix\", "
         "     \"qualifying-suffix\" : \"test.suffix.\" },"
         "\"valid-lifetime\": 4000 }";

+ 137 - 3
src/bin/dhcp6/tests/fqdn_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -53,6 +53,20 @@ public:
     static const uint16_t OVERRIDE_CLIENT_UPDATE = 4;
     static const uint16_t REPLACE_CLIENT_NAME = 8;
 
+    // Enum used to specify whether a client (packet) should include
+    // the hostname option
+    enum ClientNameFlag {
+        CLIENT_NAME_PRESENT,
+        CLIENT_NAME_NOT_PRESENT
+    };
+
+    // Enum used to specify whether the server should replace/supply
+    // the hostname or not
+    enum ReplacementFlag {
+        NAME_REPLACED,
+        NAME_NOT_REPLACED
+    };
+
     // Type used to indicate whether or not forward updates are expected
     struct ExpFwd {
         ExpFwd(bool exp_fwd) : value_(exp_fwd){};
@@ -112,7 +126,9 @@ public:
                                   (mask & ALWAYS_INCLUDE_FQDN),
                                   (mask & OVERRIDE_NO_UPDATE),
                                   (mask & OVERRIDE_CLIENT_UPDATE),
-                                  (mask & REPLACE_CLIENT_NAME),
+                                  ((mask & REPLACE_CLIENT_NAME) ?
+                                   D2ClientConfig::RCM_WHEN_PRESENT
+                                   : D2ClientConfig::RCM_NEVER),
                                   "myhost", "example.com")));
         ASSERT_NO_THROW(CfgMgr::instance().setD2ClientConfig(cfg));
         ASSERT_NO_THROW(srv_->startD2());
@@ -367,6 +383,84 @@ public:
         }
     }
 
+    // Test that the server processes the FQDN option (or lack thereof)
+    // in a client request correctly, according to the replace-client-name
+    // mode configuration parameter.
+    //
+    // @param mode - value to use client-name-replacment parameter - for
+    // mode labels such as NEVER and ALWAYS must incluce enclosing quotes:
+    // "\"NEVER\"".  This allows us to also pass in boolean literals which
+    // are unquoted.
+    // @param client_name_flag - specifies whether or not the client request
+    // should contain a hostname option
+    // @param exp_replacement_flag - specifies whether or not the server is
+    // expected to replace (or supply) the FQDN/name in its response
+    void testReplaceClientNameMode(const char* mode,
+                                   enum ClientNameFlag client_name_flag,
+                                   enum ReplacementFlag exp_replacement_flag) {
+        // Configuration "template" with a replaceable mode parameter
+        const char* config_template =
+            "{ \"interfaces-config\": { \n"
+            "  \"interfaces\": [ \"eth0\" ] \n"
+            "}, \n"
+            "\"valid-lifetime\": 4000,  \n"
+            "\"preferred-lifetime\": 3000, \n"
+            "\"rebind-timer\": 2000,  \n"
+            "\"renew-timer\": 1000,  \n"
+            "\"subnet6\": [ {  \n"
+            "    \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ], \n"
+            "    \"subnet\": \"2001:db8:1::/48\",  \n"
+            "    \"interface-id\": \"\", \n"
+            "    \"interface\": \"eth0\" \n"
+            " } ], \n"
+            "\"dhcp-ddns\": { \n"
+            "\"enable-updates\": true, \n"
+            "\"qualifying-suffix\": \"fake-suffix.isc.org.\", \n"
+            "\"replace-client-name\": %s \n"
+            "}} \n";
+
+        // Create the configuration and configure the server
+        char config_buf[1024];
+        sprintf(config_buf, config_template, mode);
+        configure(config_buf, *srv_);
+
+        // Build our client packet
+        Pkt6Ptr query;
+        if (client_name_flag == CLIENT_NAME_PRESENT) {
+            query = generateMessage(DHCPV6_SOLICIT, Option6ClientFqdn::FLAG_S,
+                                    "my.example.com.", Option6ClientFqdn::FULL,
+                                    true);
+        } else {
+            query = generateMessageWithIds(DHCPV6_SOLICIT);
+        }
+
+        Pkt6Ptr answer = generateMessageWithIds(DHCPV6_ADVERTISE);
+
+        AllocEngine::ClientContext6 ctx;
+        ASSERT_NO_THROW(srv_->processClientFqdn(query, answer, ctx));
+
+        Option6ClientFqdnPtr answ_fqdn = boost::dynamic_pointer_cast<
+            Option6ClientFqdn>(answer->getOption(D6O_CLIENT_FQDN));
+
+        // Verify the contents (or lack thereof) of the FQDN
+        if (exp_replacement_flag == NAME_REPLACED) {
+            ASSERT_TRUE(answ_fqdn);
+            EXPECT_TRUE(answ_fqdn->getDomainName().empty());
+            EXPECT_EQ(Option6ClientFqdn::PARTIAL,
+                      answ_fqdn->getDomainNameType());
+        } else {
+            if (client_name_flag == CLIENT_NAME_PRESENT) {
+                ASSERT_TRUE(answ_fqdn);
+                EXPECT_EQ("my.example.com.", answ_fqdn->getDomainName());
+                EXPECT_EQ(Option6ClientFqdn::FULL,
+                          answ_fqdn->getDomainNameType());
+            } else {
+                ASSERT_FALSE(answ_fqdn);
+            }
+        }
+    }
+
+
     /// @brief Tests that the client's message holding an FQDN is processed
     /// and that lease is acquired.
     ///
@@ -731,7 +825,7 @@ TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestFwdRev) {
 }
 
 // Checks that calling queueNCR would not result in error if DDNS updates are
-// disabled. 
+// disabled.
 TEST_F(FqdnDhcpv6SrvTest, noRemovalsWhenDisabled) {
     // Disable DDNS updates.
     disableD2();
@@ -1233,4 +1327,44 @@ TEST_F(FqdnDhcpv6SrvTest, hostnameReservationDdnsDisabled) {
                        IOAddress("2001:db8:1:1::babe"));
 }
 
+// Verifies that the replace-client-name behavior is correct for each of
+// the supported modes.
+TEST_F(FqdnDhcpv6SrvTest, replaceClientNameModeTest) {
+    isc::dhcp::test::IfaceMgrTestConfig test_config(true);
+
+    // We pass mode labels in with enclosing quotes so we can also test
+    // unquoted boolean literals true/false
+    testReplaceClientNameMode("\"never\"",
+                              CLIENT_NAME_NOT_PRESENT, NAME_NOT_REPLACED);
+    testReplaceClientNameMode("\"never\"",
+                              CLIENT_NAME_PRESENT, NAME_NOT_REPLACED);
+
+    testReplaceClientNameMode("\"always\"",
+                              CLIENT_NAME_NOT_PRESENT, NAME_REPLACED);
+    testReplaceClientNameMode("\"always\"",
+                              CLIENT_NAME_PRESENT, NAME_REPLACED);
+
+    testReplaceClientNameMode("\"when-present\"",
+                              CLIENT_NAME_NOT_PRESENT, NAME_NOT_REPLACED);
+    testReplaceClientNameMode("\"when-present\"",
+                              CLIENT_NAME_PRESENT, NAME_REPLACED);
+
+    testReplaceClientNameMode("\"when-not-present\"",
+                              CLIENT_NAME_NOT_PRESENT, NAME_REPLACED);
+    testReplaceClientNameMode("\"when-not-present\"",
+                              CLIENT_NAME_PRESENT, NAME_NOT_REPLACED);
+
+    // Verify that boolean false produces the same result as RCM_NEVER
+    testReplaceClientNameMode("false",
+                              CLIENT_NAME_NOT_PRESENT, NAME_NOT_REPLACED);
+    testReplaceClientNameMode("false",
+                              CLIENT_NAME_PRESENT, NAME_NOT_REPLACED);
+
+    // Verify that boolean true produces the same result as RCM_WHEN_PRESENT
+    testReplaceClientNameMode("true",
+                              CLIENT_NAME_NOT_PRESENT, NAME_NOT_REPLACED);
+    testReplaceClientNameMode("true",
+                              CLIENT_NAME_PRESENT, NAME_REPLACED);
+}
+
 }   // end of anonymous namespace

+ 61 - 16
src/lib/dhcpsrv/d2_client_cfg.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -10,6 +10,8 @@
 #include <dhcpsrv/d2_client_cfg.h>
 #include <dhcpsrv/dhcpsrv_log.h>
 
+#include <boost/algorithm/string/predicate.hpp>
+
 #include <string>
 
 using namespace std;
@@ -17,19 +19,62 @@ using namespace std;
 namespace isc {
 namespace dhcp {
 
-const char *D2ClientConfig::DFT_SERVER_IP = "127.0.0.1";
+const char* D2ClientConfig::DFT_SERVER_IP = "127.0.0.1";
 const size_t D2ClientConfig::DFT_SERVER_PORT = 53001;
-const char *D2ClientConfig::DFT_V4_SENDER_IP = "0.0.0.0";
-const char *D2ClientConfig::DFT_V6_SENDER_IP = "::";
+const char* D2ClientConfig::DFT_V4_SENDER_IP = "0.0.0.0";
+const char* D2ClientConfig::DFT_V6_SENDER_IP = "::";
 const size_t D2ClientConfig::DFT_SENDER_PORT = 0;
 const size_t D2ClientConfig::DFT_MAX_QUEUE_SIZE = 1024;
-const char *D2ClientConfig::DFT_NCR_PROTOCOL = "UDP";
-const char *D2ClientConfig::DFT_NCR_FORMAT = "JSON";
+const char* D2ClientConfig::DFT_NCR_PROTOCOL = "UDP";
+const char* D2ClientConfig::DFT_NCR_FORMAT = "JSON";
 const bool D2ClientConfig::DFT_ALWAYS_INCLUDE_FQDN = false;
 const bool D2ClientConfig::DFT_OVERRIDE_NO_UPDATE = false;
 const bool D2ClientConfig::DFT_OVERRIDE_CLIENT_UPDATE = false;
-const bool D2ClientConfig::DFT_REPLACE_CLIENT_NAME = false;
-const char *D2ClientConfig::DFT_GENERATED_PREFIX = "myhost";
+const char* D2ClientConfig::DFT_REPLACE_CLIENT_NAME_MODE = "NEVER";
+const char* D2ClientConfig::DFT_GENERATED_PREFIX = "myhost";
+
+
+D2ClientConfig::ReplaceClientNameMode
+D2ClientConfig::stringToReplaceClientNameMode(const std::string& mode_str) {
+    if (boost::iequals(mode_str, "never")) {
+        return (D2ClientConfig::RCM_NEVER);
+    }
+
+    if (boost::iequals(mode_str, "always")) {
+        return (D2ClientConfig::RCM_ALWAYS);
+    }
+
+    if (boost::iequals(mode_str, "when-present")) {
+        return (D2ClientConfig::RCM_WHEN_PRESENT);
+    }
+
+    if (boost::iequals(mode_str, "when-not-present")) {
+        return (D2ClientConfig::RCM_WHEN_NOT_PRESENT);
+    }
+
+    isc_throw(BadValue,
+              "Invalid ReplaceClientNameMode: " << mode_str);
+}
+
+std::string
+D2ClientConfig::replaceClientNameModeToString(const ReplaceClientNameMode& mode) {
+    switch (mode) {
+    case D2ClientConfig::RCM_NEVER:
+        return ("never");
+    case D2ClientConfig::RCM_ALWAYS:
+        return ("always");
+    case D2ClientConfig::RCM_WHEN_PRESENT:
+        return ("when-present");
+    case D2ClientConfig::RCM_WHEN_NOT_PRESENT:
+        return ("when-not-present");
+    default:
+        break;
+    }
+
+    std::ostringstream stream;
+    stream  << "unknown(" << mode << ")";
+    return (stream.str());
+}
 
 D2ClientConfig::D2ClientConfig(const  bool enable_updates,
                                const isc::asiolink::IOAddress& server_ip,
@@ -44,7 +89,7 @@ D2ClientConfig::D2ClientConfig(const  bool enable_updates,
                                const bool always_include_fqdn,
                                const bool override_no_update,
                                const bool override_client_update,
-                               const bool replace_client_name,
+                               const ReplaceClientNameMode replace_client_name_mode,
                                const std::string& generated_prefix,
                                const std::string& qualifying_suffix)
     : enable_updates_(enable_updates),
@@ -58,7 +103,7 @@ D2ClientConfig::D2ClientConfig(const  bool enable_updates,
       always_include_fqdn_(always_include_fqdn),
       override_no_update_(override_no_update),
       override_client_update_(override_client_update),
-      replace_client_name_(replace_client_name),
+      replace_client_name_mode_(replace_client_name_mode),
       generated_prefix_(generated_prefix),
       qualifying_suffix_(qualifying_suffix) {
     validateContents();
@@ -76,7 +121,7 @@ D2ClientConfig::D2ClientConfig()
       always_include_fqdn_(DFT_ALWAYS_INCLUDE_FQDN),
       override_no_update_(DFT_OVERRIDE_NO_UPDATE),
       override_client_update_(DFT_OVERRIDE_CLIENT_UPDATE),
-      replace_client_name_(DFT_REPLACE_CLIENT_NAME),
+      replace_client_name_mode_(stringToReplaceClientNameMode(DFT_REPLACE_CLIENT_NAME_MODE)),
       generated_prefix_(DFT_GENERATED_PREFIX),
       qualifying_suffix_("") {
     validateContents();
@@ -134,7 +179,7 @@ D2ClientConfig::operator == (const D2ClientConfig& other) const {
             (always_include_fqdn_ == other.always_include_fqdn_) &&
             (override_no_update_ == other.override_no_update_) &&
             (override_client_update_ == other.override_client_update_) &&
-            (replace_client_name_ == other.replace_client_name_) &&
+            (replace_client_name_mode_ == other.replace_client_name_mode_) &&
             (generated_prefix_ == other.generated_prefix_) &&
             (qualifying_suffix_ == other.qualifying_suffix_));
 }
@@ -155,16 +200,16 @@ D2ClientConfig::toText() const {
                << ", sender_ip: " << sender_ip_.toText()
                << ", sender_port: " << sender_port_
                << ", max_queue_size: " << max_queue_size_
-               << ", ncr_protocol: " << ncr_protocol_
-               << ", ncr_format: " << ncr_format_
+               << ", ncr_protocol: " << ncrProtocolToString(ncr_protocol_)
+               << ", ncr_format: " << ncrFormatToString(ncr_format_)
                << ", always_include_fqdn: " << (always_include_fqdn_ ?
                                                 "yes" : "no")
                << ", override_no_update: " << (override_no_update_ ?
                                                "yes" : "no")
                << ", override_client_update: " << (override_client_update_ ?
                                                    "yes" : "no")
-               << ", replace_client_name: " << (replace_client_name_ ?
-                                                "yes" : "no")
+               << ", replace_client_name: "
+               << replaceClientNameModeToString(replace_client_name_mode_)
                << ", generated_prefix: [" << generated_prefix_ << "]"
                << ", qualifying_suffix: [" << qualifying_suffix_ << "]";
     }

+ 44 - 17
src/lib/dhcpsrv/d2_client_cfg.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -39,6 +39,7 @@ public:
         : isc::Exception(file, line, what) {}
 };
 
+
 /// @brief Acts as a storage vault for D2 client configuration
 ///
 /// A simple container class for storing and retrieving the configuration
@@ -47,24 +48,30 @@ public:
 ///
 class D2ClientConfig {
 public:
-
-
     /// @brief Default configuration constants.
     /// @todo For now these are hard-coded as configuraiton layer cannot
     /// readily provide them (see Trac #3358).
-    static const char *DFT_SERVER_IP;
+    static const char* DFT_SERVER_IP;
     static const size_t DFT_SERVER_PORT;
-    static const char *DFT_V4_SENDER_IP;
-    static const char *DFT_V6_SENDER_IP;
+    static const char* DFT_V4_SENDER_IP;
+    static const char* DFT_V6_SENDER_IP;
     static const size_t DFT_SENDER_PORT;
     static const size_t DFT_MAX_QUEUE_SIZE;
-    static const char *DFT_NCR_PROTOCOL;
-    static const char *DFT_NCR_FORMAT;
+    static const char* DFT_NCR_PROTOCOL;
+    static const char* DFT_NCR_FORMAT;
     static const bool DFT_ALWAYS_INCLUDE_FQDN;
     static const bool DFT_OVERRIDE_NO_UPDATE;
     static const bool DFT_OVERRIDE_CLIENT_UPDATE;
-    static const bool DFT_REPLACE_CLIENT_NAME;
-    static const char *DFT_GENERATED_PREFIX;
+    static const char* DFT_REPLACE_CLIENT_NAME_MODE;
+    static const char* DFT_GENERATED_PREFIX;
+
+    /// @brief Defines the client name replacement modes.
+    enum ReplaceClientNameMode  {
+        RCM_NEVER,
+        RCM_ALWAYS,
+        RCM_WHEN_PRESENT,
+        RCM_WHEN_NOT_PRESENT
+    };
 
     /// @brief Constructor
     ///
@@ -84,7 +91,7 @@ public:
     /// updates.
     /// @param override_client_update Perform updates, even if client requested
     /// delegation.
-    /// @param replace_client_name enables replacement of the domain-name
+    /// @param replace_client_name_mode enables replacement of the domain-name
     /// supplied by the client with a generated name.
     /// @param generated_prefix Prefix to use when generating domain-names.
     /// @param qualifying_suffix Suffix to use to qualify partial domain-names.
@@ -104,7 +111,7 @@ public:
                    const bool always_include_fqdn,
                    const bool override_no_update,
                    const bool override_client_update,
-                   const bool replace_client_name,
+                   const ReplaceClientNameMode replace_client_name_mode,
                    const std::string& generated_prefix,
                    const std::string& qualifying_suffix);
 
@@ -170,9 +177,9 @@ public:
         return(override_client_update_);
     }
 
-    /// @brief Return whether or not client's domain-name is always replaced.
-    bool getReplaceClientName() const {
-        return(replace_client_name_);
+    /// @brief Return mode of replacement to use regarding client's client's domain-name
+    ReplaceClientNameMode getReplaceClientNameMode() const {
+        return(replace_client_name_mode_);
     }
 
     /// @brief Return the prefix to use when generating domain-names.
@@ -203,6 +210,26 @@ public:
     /// @param enable boolean value to assign to the enable-updates flag
     void enableUpdates(bool enable);
 
+    /// @brief Converts labels to  ReplaceClientNameMode enum values.
+    ///
+    /// @param mode_str text to convert to an enum.
+    /// Valid string values: "never", "always", "when-present",
+    /// "when-not-present" (case insensistive)
+    ///
+    /// @return NameChangeFormat value which maps to the given string.
+    ///
+    /// @throw isc::BadValue if given a string value which does not map to an
+    /// enum value.
+    static ReplaceClientNameMode stringToReplaceClientNameMode(const std::string& mode_str);
+
+    /// @brief Converts NameChangeFormat enums to text labels.
+    ///
+    /// @param mode enum value to convert to label
+    ///
+    /// @return std:string containing the text label if the value is valid, or
+    /// "unknown" if not.
+    static std::string replaceClientNameModeToString(const ReplaceClientNameMode& mode);
+
 protected:
     /// @brief Validates member values.
     ///
@@ -248,8 +275,8 @@ private:
     /// @brief Should Kea perform updates, even if client requested delegation.
     bool override_client_update_;
 
-    /// @brief Should Kea replace the domain-name supplied by the client.
-    bool replace_client_name_;
+    /// @brief How Kea should handle the domain-name supplied by the client.
+    ReplaceClientNameMode replace_client_name_mode_;
 
     /// @brief Prefix Kea should use when generating domain-names.
     std::string generated_prefix_;

+ 3 - 2
src/lib/dhcpsrv/d2_client_mgr.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2014-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2016 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -457,7 +457,8 @@ void
 D2ClientMgr::adjustDomainName(const T& fqdn, T& fqdn_resp) {
     // If we're configured to replace it or the supplied name is blank
     // set the response name to blank.
-    if (d2_client_config_->getReplaceClientName() ||
+    if ((d2_client_config_->getReplaceClientNameMode() == D2ClientConfig::RCM_ALWAYS ||
+         d2_client_config_->getReplaceClientNameMode() == D2ClientConfig::RCM_WHEN_PRESENT) ||
         fqdn.getDomainName().empty()) {
         fqdn_resp.setDomainName("", T::PARTIAL);
     } else {

+ 23 - 9
src/lib/dhcpsrv/parsers/dhcp_parsers.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -1406,10 +1406,24 @@ D2ClientConfigParser::build(isc::data::ConstElementPtr client_config) {
                                               D2ClientConfig::
                                               DFT_OVERRIDE_CLIENT_UPDATE);
 
-        bool replace_client_name =
-            boolean_values_->getOptionalParam("replace-client-name",
-                                              D2ClientConfig::
-                                              DFT_REPLACE_CLIENT_NAME);
+        // Formerly, replace-client-name was boolean, so for now we'll support boolean
+        // values by mapping them to the appropriate mode
+        D2ClientConfig::ReplaceClientNameMode replace_client_name_mode;
+        std::string mode_str;
+        mode_str  = string_values_->getOptionalParam("replace-client-name",
+                                                     D2ClientConfig::
+                                                     DFT_REPLACE_CLIENT_NAME_MODE);
+        if (boost::iequals(mode_str, "FALSE")) {
+            // @todo add a debug log
+            replace_client_name_mode = D2ClientConfig::RCM_NEVER;
+        }
+        else if (boost::iequals(mode_str, "TRUE")) {
+            // @todo add a debug log
+            replace_client_name_mode = D2ClientConfig::RCM_WHEN_PRESENT;
+        } else {
+            replace_client_name_mode = D2ClientConfig::
+                                       stringToReplaceClientNameMode(mode_str);
+        }
 
         // Attempt to create the new client config.
         local_client_config_.reset(new D2ClientConfig(enable_updates,
@@ -1423,7 +1437,7 @@ D2ClientConfigParser::build(isc::data::ConstElementPtr client_config) {
                                                       always_include_fqdn,
                                                       override_no_update,
                                                       override_client_update,
-                                                      replace_client_name,
+                                                      replace_client_name_mode,
                                                       generated_prefix,
                                                       qualifying_suffix));
 
@@ -1445,14 +1459,14 @@ D2ClientConfigParser::createConfigParser(const std::string& config_id) {
         (config_id.compare("ncr-format") == 0) ||
         (config_id.compare("generated-prefix") == 0) ||
         (config_id.compare("sender-ip") == 0) ||
-        (config_id.compare("qualifying-suffix") == 0)) {
+        (config_id.compare("qualifying-suffix") == 0) ||
+        (config_id.compare("replace-client-name") == 0)) {
         parser = new StringParser(config_id, string_values_);
     } else if ((config_id.compare("enable-updates") == 0) ||
         (config_id.compare("always-include-fqdn") == 0) ||
         (config_id.compare("allow-client-update") == 0) ||
         (config_id.compare("override-no-update") == 0) ||
-        (config_id.compare("override-client-update") == 0) ||
-        (config_id.compare("replace-client-name") == 0)) {
+        (config_id.compare("override-client-update") == 0)) {
         parser = new BooleanParser(config_id, boolean_values_);
     } else {
         isc_throw(NotImplemented,

+ 2 - 2
src/lib/dhcpsrv/tests/cfgmgr_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -405,7 +405,7 @@ TEST_F(CfgMgrTest, d2ClientConfig) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  true, true, true, true,
+                                  true, true, true, D2ClientConfig::RCM_ALWAYS,
                                   "pre-fix", "suf-fix")));
 
     // Verify that we can assign a new, non-empty configuration.

+ 70 - 41
src/lib/dhcpsrv/tests/d2_client_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -20,6 +20,34 @@ using namespace isc;
 
 namespace {
 
+/// @brief Tests conversion of NameChangeFormat between enum and strings.
+TEST(ReplaceClientNameModeTest, formatEnumConversion){
+    ASSERT_EQ(D2ClientConfig::stringToReplaceClientNameMode("never"),
+              D2ClientConfig::RCM_NEVER);
+    ASSERT_EQ(D2ClientConfig::stringToReplaceClientNameMode("always"),
+              D2ClientConfig::RCM_ALWAYS);
+    ASSERT_EQ(D2ClientConfig::stringToReplaceClientNameMode("when-present"),
+              D2ClientConfig::RCM_WHEN_PRESENT);
+    ASSERT_EQ(D2ClientConfig::stringToReplaceClientNameMode("when-not-present"),
+              D2ClientConfig::RCM_WHEN_NOT_PRESENT);
+    ASSERT_THROW(D2ClientConfig::stringToReplaceClientNameMode("BOGUS"),
+                 isc::BadValue);
+
+    ASSERT_EQ(D2ClientConfig::
+              replaceClientNameModeToString(D2ClientConfig::RCM_NEVER),
+              "never");
+    ASSERT_EQ(D2ClientConfig::
+              replaceClientNameModeToString(D2ClientConfig::RCM_ALWAYS),
+              "always");
+    ASSERT_EQ(D2ClientConfig::
+              replaceClientNameModeToString(D2ClientConfig::RCM_WHEN_PRESENT),
+              "when-present");
+    ASSERT_EQ(D2ClientConfig::
+              replaceClientNameModeToString(D2ClientConfig::
+                                            RCM_WHEN_NOT_PRESENT),
+              "when-not-present");
+}
+
 /// @brief Checks constructors and accessors of D2ClientConfig.
 TEST(D2ClientConfigTest, constructorsAndAccessors) {
     D2ClientConfigPtr d2_client_config;
@@ -47,7 +75,8 @@ TEST(D2ClientConfigTest, constructorsAndAccessors) {
     bool always_include_fqdn = true;
     bool override_no_update = true;
     bool override_client_update = true;
-    bool replace_client_name = true;
+    D2ClientConfig::ReplaceClientNameMode replace_client_name_mode = D2ClientConfig::
+                                                                     RCM_WHEN_PRESENT;
     std::string generated_prefix = "the_prefix";
     std::string qualifying_suffix = "the.suffix.";
 
@@ -63,8 +92,8 @@ TEST(D2ClientConfigTest, constructorsAndAccessors) {
                                                           ncr_format,
                                                           always_include_fqdn,
                                                           override_no_update,
-                                                         override_client_update,
-                                                          replace_client_name,
+                                                          override_client_update,
+                                                          replace_client_name_mode,
                                                           generated_prefix,
                                                           qualifying_suffix)));
 
@@ -84,7 +113,7 @@ TEST(D2ClientConfigTest, constructorsAndAccessors) {
     EXPECT_EQ(d2_client_config->getOverrideNoUpdate(), override_no_update);
     EXPECT_EQ(d2_client_config->getOverrideClientUpdate(),
               override_client_update);
-    EXPECT_EQ(d2_client_config->getReplaceClientName(), replace_client_name);
+    EXPECT_EQ(d2_client_config->getReplaceClientNameMode(), replace_client_name_mode);
     EXPECT_EQ(d2_client_config->getGeneratedPrefix(), generated_prefix);
     EXPECT_EQ(d2_client_config->getQualifyingSuffix(), qualifying_suffix);
 
@@ -106,7 +135,7 @@ TEST(D2ClientConfigTest, constructorsAndAccessors) {
                                                        always_include_fqdn,
                                                        override_no_update,
                                                        override_client_update,
-                                                       replace_client_name,
+                                                       replace_client_name_mode,
                                                        generated_prefix,
                                                        qualifying_suffix)),
                  D2ClientError);
@@ -127,7 +156,7 @@ TEST(D2ClientConfigTest, equalityOperator) {
     ASSERT_NO_THROW(ref_config.reset(new D2ClientConfig(true,
                     ref_address, 477, ref_address, 478, 1024,
                     dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                    true, true, true, true,
+                    true, true, true, D2ClientConfig::RCM_WHEN_PRESENT,
                     "pre-fix", "suf-fix")));
     ASSERT_TRUE(ref_config);
 
@@ -135,7 +164,7 @@ TEST(D2ClientConfigTest, equalityOperator) {
     ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
                     ref_address, 477, ref_address, 478, 1024,
                     dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                    true, true, true, true,
+                    true, true, true, D2ClientConfig::RCM_WHEN_PRESENT,
                     "pre-fix", "suf-fix")));
     ASSERT_TRUE(test_config);
     EXPECT_TRUE(*ref_config == *test_config);
@@ -145,7 +174,7 @@ TEST(D2ClientConfigTest, equalityOperator) {
     ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(false,
                     ref_address, 477, ref_address, 478, 1024,
                     dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                    true, true, true, true,
+                    true, true, true, D2ClientConfig::RCM_WHEN_PRESENT,
                     "pre-fix", "suf-fix")));
     ASSERT_TRUE(test_config);
     EXPECT_FALSE(*ref_config == *test_config);
@@ -155,7 +184,7 @@ TEST(D2ClientConfigTest, equalityOperator) {
     ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
                     test_address, 477, ref_address, 478, 1024,
                     dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                    true, true, true, true,
+                    true, true, true, D2ClientConfig::RCM_WHEN_PRESENT,
                     "pre-fix", "suf-fix")));
     ASSERT_TRUE(test_config);
     EXPECT_FALSE(*ref_config == *test_config);
@@ -165,7 +194,7 @@ TEST(D2ClientConfigTest, equalityOperator) {
     ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
                     ref_address, 333, ref_address, 478, 1024,
                     dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                    true, true, true, true,
+                    true, true, true, D2ClientConfig::RCM_WHEN_PRESENT,
                     "pre-fix", "suf-fix")));
     ASSERT_TRUE(test_config);
     EXPECT_FALSE(*ref_config == *test_config);
@@ -175,7 +204,7 @@ TEST(D2ClientConfigTest, equalityOperator) {
     ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
                     ref_address, 477, test_address, 478, 1024,
                     dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                    true, true, true, true,
+                    true, true, true, D2ClientConfig::RCM_WHEN_PRESENT,
                     "pre-fix", "suf-fix")));
     ASSERT_TRUE(test_config);
     EXPECT_FALSE(*ref_config == *test_config);
@@ -185,7 +214,7 @@ TEST(D2ClientConfigTest, equalityOperator) {
     ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
                     ref_address, 477, ref_address, 333, 1024,
                     dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                    true, true, true, true,
+                    true, true, true, D2ClientConfig::RCM_WHEN_PRESENT,
                     "pre-fix", "suf-fix")));
     ASSERT_TRUE(test_config);
     EXPECT_FALSE(*ref_config == *test_config);
@@ -195,7 +224,7 @@ TEST(D2ClientConfigTest, equalityOperator) {
     ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
                     ref_address, 477, ref_address, 478, 2048,
                     dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                    true, true, true, true,
+                    true, true, true, D2ClientConfig::RCM_WHEN_PRESENT,
                     "pre-fix", "suf-fix")));
     ASSERT_TRUE(test_config);
     EXPECT_FALSE(*ref_config == *test_config);
@@ -205,7 +234,7 @@ TEST(D2ClientConfigTest, equalityOperator) {
     ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
                     ref_address, 477, ref_address, 478, 1024,
                     dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                    false, true, true, true,
+                    false, true, true, D2ClientConfig::RCM_WHEN_PRESENT,
                     "pre-fix", "suf-fix")));
     ASSERT_TRUE(test_config);
     EXPECT_FALSE(*ref_config == *test_config);
@@ -215,7 +244,7 @@ TEST(D2ClientConfigTest, equalityOperator) {
     ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
                     ref_address, 477, ref_address, 478, 1024,
                     dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                    true, false, true, true,
+                    true, false, true, D2ClientConfig::RCM_WHEN_PRESENT,
                     "pre-fix", "suf-fix")));
     ASSERT_TRUE(test_config);
     EXPECT_FALSE(*ref_config == *test_config);
@@ -225,7 +254,7 @@ TEST(D2ClientConfigTest, equalityOperator) {
     ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
                     ref_address, 477, ref_address, 478, 1024,
                     dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                    true, true, false, true,
+                    true, true, false, D2ClientConfig::RCM_WHEN_PRESENT,
                     "pre-fix", "suf-fix")));
     ASSERT_TRUE(test_config);
     EXPECT_FALSE(*ref_config == *test_config);
@@ -235,7 +264,7 @@ TEST(D2ClientConfigTest, equalityOperator) {
     ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
                     ref_address, 477, ref_address, 478, 1024,
                     dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                    true, true, true, false,
+                    true, true, true, D2ClientConfig::RCM_NEVER,
                     "pre-fix", "suf-fix")));
     ASSERT_TRUE(test_config);
     EXPECT_FALSE(*ref_config == *test_config);
@@ -245,7 +274,7 @@ TEST(D2ClientConfigTest, equalityOperator) {
     ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
                     ref_address, 477, ref_address, 478, 1024,
                     dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                    true, true, true, true,
+                    true, true, true, D2ClientConfig::RCM_WHEN_PRESENT,
                     "bogus", "suf-fix")));
     ASSERT_TRUE(test_config);
     EXPECT_FALSE(*ref_config == *test_config);
@@ -255,7 +284,7 @@ TEST(D2ClientConfigTest, equalityOperator) {
     ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
                     ref_address, 477, ref_address, 478, 1024,
                     dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                    true, true, true, true,
+                    true, true, true, D2ClientConfig::RCM_WHEN_PRESENT,
                     "pre-fix", "bogus")));
     ASSERT_TRUE(test_config);
     EXPECT_FALSE(*ref_config == *test_config);
@@ -300,7 +329,7 @@ TEST(D2ClientMgr, validConfig) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  true, true, true, true,
+                                  true, true, true, D2ClientConfig::RCM_WHEN_PRESENT,
                                   "pre-fix", "suf-fix")));
 
     // Verify that we can assign a new, non-empty configuration.
@@ -344,7 +373,7 @@ TEST(D2ClientMgr, analyzeFqdnInvalidCombination) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, false, false, false,
+                                  false, false, false, D2ClientConfig::RCM_NEVER,
                                   "pre-fix", "suf-fix")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
     ASSERT_TRUE(mgr.ddnsEnabled());
@@ -368,7 +397,7 @@ TEST(D2ClientMgr, analyzeFqdnEnabledNoOverrides) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, false, false, false,
+                                  false, false, false, D2ClientConfig::RCM_NEVER,
                                   "pre-fix", "suf-fix")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
     ASSERT_TRUE(mgr.ddnsEnabled());
@@ -412,7 +441,7 @@ TEST(D2ClientMgr, analyzeFqdnEnabledOverrideNoUpdate) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, true, false, false,
+                                  false, true, false, D2ClientConfig::RCM_NEVER,
                                   "pre-fix", "suf-fix")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
     ASSERT_TRUE(mgr.ddnsEnabled());
@@ -455,7 +484,7 @@ TEST(D2ClientMgr, analyzeFqdnEnabledOverrideClientUpdate) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, false, true, false,
+                                  false, false, true, D2ClientConfig::RCM_NEVER,
                                   "pre-fix", "suf-fix")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
     ASSERT_TRUE(mgr.ddnsEnabled());
@@ -499,7 +528,7 @@ TEST(D2ClientMgr, adjustFqdnFlagsV4) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, true, false, false,
+                                  false, true, false, D2ClientConfig::RCM_NEVER,
                                   "pre-fix", "suf-fix")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
     ASSERT_TRUE(mgr.ddnsEnabled());
@@ -600,7 +629,7 @@ TEST(D2ClientMgr, qualifyName) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, false, true, false,
+                                  false, false, true, D2ClientConfig::RCM_NEVER,
                                   "prefix", "suffix.com")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
 
@@ -616,7 +645,7 @@ TEST(D2ClientMgr, qualifyName) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, false, true, false,
+                                  false, false, true, D2ClientConfig::RCM_NEVER,
                                   "prefix", "suffix.com")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
     partial_name = "somehost";
@@ -630,7 +659,7 @@ TEST(D2ClientMgr, qualifyName) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, false, true, false,
+                                  false, false, true, D2ClientConfig::RCM_NEVER,
                                   "prefix", ""))); //empty suffix
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
     partial_name = "somehost";
@@ -643,7 +672,7 @@ TEST(D2ClientMgr, qualifyName) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, false, true, false,
+                                  false, false, true, D2ClientConfig::RCM_NEVER,
                                   "prefix", "hasdot.com.")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
 
@@ -662,7 +691,7 @@ TEST(D2ClientMgr, qualifyName) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, false, true, false,
+                                  false, false, true, D2ClientConfig::RCM_NEVER,
                                   "prefix", "")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
 
@@ -700,7 +729,7 @@ TEST(D2ClientMgr, generateFqdn) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, false, true, false,
+                                  false, false, true, D2ClientConfig::RCM_NEVER,
                                   "prefix", "suffix.com")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
 
@@ -734,10 +763,10 @@ TEST(D2ClientMgr, adjustDomainNameV4) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, false, false, false,
+                                  false, false, false, D2ClientConfig::RCM_NEVER,
                                   "prefix", "suffix.com")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
-    ASSERT_FALSE(cfg->getReplaceClientName());
+    ASSERT_EQ(D2ClientConfig::RCM_NEVER, cfg->getReplaceClientNameMode());
 
     // replace-client-name is false, client passes in empty fqdn
     // reponse domain should be empty/partial.
@@ -777,10 +806,10 @@ TEST(D2ClientMgr, adjustDomainNameV4) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, false, false, true,
+                                  false, false, false, D2ClientConfig::RCM_WHEN_PRESENT,
                                   "prefix", "suffix.com")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
-    ASSERT_TRUE(cfg->getReplaceClientName());
+    ASSERT_EQ(D2ClientConfig::RCM_WHEN_PRESENT, cfg->getReplaceClientNameMode());
 
     // replace-client-name is true, client passes in empty fqdn
     // reponse domain should be empty/partial.
@@ -827,10 +856,10 @@ TEST(D2ClientMgr, adjustDomainNameV6) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, false, false, false,
+                                  false, false, false, D2ClientConfig::RCM_NEVER,
                                   "prefix", "suffix.com")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
-    ASSERT_FALSE(cfg->getReplaceClientName());
+    ASSERT_EQ(D2ClientConfig::RCM_NEVER, cfg->getReplaceClientNameMode());
 
     // replace-client-name is false, client passes in empty fqdn
     // reponse domain should be empty/partial.
@@ -867,10 +896,10 @@ TEST(D2ClientMgr, adjustDomainNameV6) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, false, false, true,
+                                  false, false, false, D2ClientConfig::RCM_WHEN_PRESENT,
                                   "prefix", "suffix.com")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
-    ASSERT_TRUE(cfg->getReplaceClientName());
+    ASSERT_EQ(D2ClientConfig::RCM_WHEN_PRESENT, cfg->getReplaceClientNameMode());
 
     // replace-client-name is true, client passes in empty fqdn
     // reponse domain should be empty/partial.
@@ -917,7 +946,7 @@ TEST(D2ClientMgr, adjustFqdnFlagsV6) {
                                   isc::asiolink::IOAddress("127.0.0.1"), 478,
                                   1024,
                                   dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
-                                  false, true, false, false,
+                                  false, true, false, D2ClientConfig::RCM_NEVER,
                                   "pre-fix", "suf-fix")));
     ASSERT_NO_THROW(mgr.setD2ClientConfig(cfg));
     ASSERT_TRUE(mgr.ddnsEnabled());

+ 1 - 1
src/lib/dhcpsrv/tests/d2_udp_unittest.cc

@@ -75,7 +75,7 @@ public:
                                   sender_ip, D2ClientConfig::DFT_SENDER_PORT,
                                   D2ClientConfig::DFT_MAX_QUEUE_SIZE,
                                   protocol, dhcp_ddns::FMT_JSON,
-                                  true, true, true, true,
+                                  true, true, true, D2ClientConfig::RCM_WHEN_PRESENT,
                                   "myhost", ".example.com.")));
 
         ASSERT_NO_THROW(setD2ClientConfig(new_cfg));

+ 45 - 6
src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc

@@ -144,6 +144,24 @@ TEST_F(DhcpParserTest, stringParserTest) {
     parser.commit();
     EXPECT_NO_THROW((actual_value = storage->getParam(name)));
     EXPECT_EQ(test_value, actual_value);
+
+    // Verify that parser with accepts a boolean true element.
+    element = Element::create(true);
+    EXPECT_NO_THROW(parser.build(element));
+
+    // Verify that commit updates storage.
+    parser.commit();
+    EXPECT_NO_THROW((actual_value = storage->getParam(name)));
+    EXPECT_EQ("true", actual_value);
+
+    // Verify that parser with accepts a boolean true element.
+    element = Element::create(false);
+    EXPECT_NO_THROW(parser.build(element));
+
+    // Verify that commit updates storage.
+    parser.commit();
+    EXPECT_NO_THROW((actual_value = storage->getParam(name)));
+    EXPECT_EQ("false", actual_value);
 }
 
 /// @brief Check Uint32Parser basic functionality
@@ -1373,7 +1391,7 @@ TEST_F(ParseConfigTest, validD2Config) {
         "     \"always-include-fqdn\" : true, "
         "     \"override-no-update\" : true, "
         "     \"override-client-update\" : true, "
-        "     \"replace-client-name\" : true, "
+        "     \"replace-client-name\" : \"when-present\", "
         "     \"generated-prefix\" : \"test.prefix\", "
         "     \"qualifying-suffix\" : \"test.suffix.\" "
         "    }"
@@ -1398,7 +1416,7 @@ TEST_F(ParseConfigTest, validD2Config) {
     EXPECT_TRUE(d2_client_config->getAlwaysIncludeFqdn());
     EXPECT_TRUE(d2_client_config->getOverrideNoUpdate());
     EXPECT_TRUE(d2_client_config->getOverrideClientUpdate());
-    EXPECT_TRUE(d2_client_config->getReplaceClientName());
+    EXPECT_EQ(D2ClientConfig::RCM_WHEN_PRESENT, d2_client_config->getReplaceClientNameMode());
     EXPECT_EQ("test.prefix", d2_client_config->getGeneratedPrefix());
     EXPECT_EQ("test.suffix.", d2_client_config->getQualifyingSuffix());
 
@@ -1419,7 +1437,7 @@ TEST_F(ParseConfigTest, validD2Config) {
         "     \"always-include-fqdn\" : false, "
         "     \"override-no-update\" : false, "
         "     \"override-client-update\" : false, "
-        "     \"replace-client-name\" : false, "
+        "     \"replace-client-name\" : \"never\", "
         "     \"generated-prefix\" : \"\", "
         "     \"qualifying-suffix\" : \"\" "
         "    }"
@@ -1443,7 +1461,7 @@ TEST_F(ParseConfigTest, validD2Config) {
     EXPECT_FALSE(d2_client_config->getAlwaysIncludeFqdn());
     EXPECT_FALSE(d2_client_config->getOverrideNoUpdate());
     EXPECT_FALSE(d2_client_config->getOverrideClientUpdate());
-    EXPECT_FALSE(d2_client_config->getReplaceClientName());
+    EXPECT_EQ(D2ClientConfig::RCM_NEVER, d2_client_config->getReplaceClientNameMode());
     EXPECT_EQ("", d2_client_config->getGeneratedPrefix());
     EXPECT_EQ("", d2_client_config->getQualifyingSuffix());
 }
@@ -1515,8 +1533,10 @@ TEST_F(ParseConfigTest, parserDefaultsD2Config) {
               d2_client_config->getOverrideNoUpdate());
     EXPECT_EQ(D2ClientConfig::DFT_OVERRIDE_CLIENT_UPDATE,
               d2_client_config->getOverrideClientUpdate());
-    EXPECT_EQ(D2ClientConfig::DFT_REPLACE_CLIENT_NAME,
-              d2_client_config->getReplaceClientName());
+    EXPECT_EQ(D2ClientConfig::
+              stringToReplaceClientNameMode(D2ClientConfig::
+                                            DFT_REPLACE_CLIENT_NAME_MODE),
+              d2_client_config->getReplaceClientNameMode());
     EXPECT_EQ(D2ClientConfig::DFT_GENERATED_PREFIX,
               d2_client_config->getGeneratedPrefix());
     EXPECT_EQ("test.suffix.",
@@ -1656,6 +1676,25 @@ TEST_F(ParseConfigTest, invalidD2Config) {
         "     \"qualifying-suffix\" : \"test.suffix.\" "
         "    }"
         "}",
+        // Invalid replace-client-name value
+        "{ \"dhcp-ddns\" :"
+        "    {"
+        "     \"enable-updates\" : true, "
+        "     \"server-ip\" : \"3001::5\", "
+        "     \"server-port\" : 3433, "
+        "     \"sender-ip\" : \"3001::5\", "
+        "     \"sender-port\" : 3434, "
+        "     \"max-queue-size\" : 2048, "
+        "     \"ncr-protocol\" : \"UDP\", "
+        "     \"ncr-format\" : \"JSON\", "
+        "     \"always-include-fqdn\" : true, "
+        "     \"override-no-update\" : true, "
+        "     \"override-client-update\" : true, "
+        "     \"replace-client-name\" : \"BOGUS\", "
+        "     \"generated-prefix\" : \"test.prefix\", "
+        "     \"qualifying-suffix\" : \"test.suffix.\" "
+        "    }"
+        "}",
         // stop
         ""
     };