Browse Source

[master] Merge branch 'trac3360'

Conflicts:
	doc/guide/bind10-guide.xml
Marcin Siodelski 11 years ago
parent
commit
09e6e71abf
42 changed files with 3826 additions and 104 deletions
  1. 70 2
      doc/guide/bind10-guide.xml
  2. 1 1
      src/bin/dhcp4/config_parser.cc
  3. 6 0
      src/bin/dhcp4/dhcp4.spec
  4. 1 1
      src/bin/dhcp4/dhcp4_srv.h
  5. 2 1
      src/bin/dhcp4/tests/dhcp4_test_utils.h
  6. 1 1
      src/bin/dhcp6/config_parser.cc
  7. 6 0
      src/bin/dhcp6/dhcp6.spec
  8. 1 1
      src/bin/dhcp6/tests/dhcp6_test_utils.h
  9. 52 2
      src/lib/dhcp/duid.cc
  10. 46 5
      src/lib/dhcp/duid.h
  11. 45 3
      src/lib/dhcp/hwaddr.cc
  12. 31 3
      src/lib/dhcp/hwaddr.h
  13. 65 1
      src/lib/dhcp/tests/duid_unittest.cc
  14. 32 1
      src/lib/dhcp/tests/hwaddr_unittest.cc
  15. 4 0
      src/lib/dhcpsrv/Makefile.am
  16. 178 0
      src/lib/dhcpsrv/csv_lease_file4.cc
  17. 153 0
      src/lib/dhcpsrv/csv_lease_file4.h
  18. 176 0
      src/lib/dhcpsrv/csv_lease_file6.cc
  19. 170 0
      src/lib/dhcpsrv/csv_lease_file6.h
  20. 25 12
      src/lib/dhcpsrv/dbaccess_parser.cc
  21. 11 4
      src/lib/dhcpsrv/dbaccess_parser.h
  22. 25 7
      src/lib/dhcpsrv/dhcpsrv_messages.mes
  23. 270 2
      src/lib/dhcpsrv/memfile_lease_mgr.cc
  24. 152 12
      src/lib/dhcpsrv/memfile_lease_mgr.h
  25. 5 1
      src/lib/dhcpsrv/tests/Makefile.am
  26. 2 2
      src/lib/dhcpsrv/tests/alloc_engine_unittest.cc
  27. 182 0
      src/lib/dhcpsrv/tests/csv_lease_file4_unittest.cc
  28. 223 0
      src/lib/dhcpsrv/tests/csv_lease_file6_unittest.cc
  29. 70 20
      src/lib/dhcpsrv/tests/dbaccess_parser_unittest.cc
  30. 149 3
      src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.cc
  31. 23 1
      src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.h
  32. 69 0
      src/lib/dhcpsrv/tests/lease_file_io.cc
  33. 65 0
      src/lib/dhcpsrv/tests/lease_file_io.h
  34. 1 1
      src/lib/dhcpsrv/tests/lease_mgr_unittest.cc
  35. 190 14
      src/lib/dhcpsrv/tests/memfile_lease_mgr_unittest.cc
  36. 22 1
      src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc
  37. 4 1
      src/lib/dhcpsrv/tests/pgsql_lease_mgr_unittest.cc
  38. 2 1
      src/lib/util/Makefile.am
  39. 374 0
      src/lib/util/csv_file.cc
  40. 480 0
      src/lib/util/csv_file.h
  41. 3 0
      src/lib/util/tests/Makefile.am
  42. 439 0
      src/lib/util/tests/csv_file_unittest.cc

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

@@ -3836,10 +3836,44 @@ Dhcp4/dhcp-ddns/qualifying-suffix	"example.com"	string
       </para>
 
       <section>
+      <title>Default storage for leases</title>
+      <para>
+        The server is able to store lease data in different repositories. Larger deployments
+        may elect to store leases in a database.
+        <xref linkend="database-configuration4"/> describes one way to do it.
+        By default, the server will use a CSV file rather than a database to store
+        lease information. One of the advantages of using a file is that it eliminates
+        dependency on third party database software.
+      </para>
+      <para>
+        The configuration of the file backend (Memfile)
+        is controlled through the Dhcp4/lease-database parameters. When default
+        parameters are used, the Memfile backend will write leases to a disk in the
+        [bind10-install-dir]/var/bind10/kea-leases4.csv.
+      </para>
+      <para>
+        It is possible to alter the default location of the lease file. The following
+        configuration:
+<screen>
+&gt; <userinput>config set Dhcp4/lease-database/type "memfile"</userinput>
+&gt; <userinput>config set Dhcp4/lease-database/persist true</userinput>
+&gt; <userinput>config set Dhcp4/lease-database/name "/tmp/kea-leases4.csv"</userinput>
+&gt; <userinput>config commit</userinput>
+</screen>
+        will change the default location of the lease file to /tmp/kea-leases4.csv.
+      </para>
+      <para>
+        The "persist" parameter controls whether the leases are written to disk.
+        It is strongly recommended that this parameter is set to "true" at all times
+        during the normal operation of the server
+      </para>
+      </section>
+
+      <section id="database-configuration4">
       <title>Database Configuration</title>
       <para>
       All leases issued by the server are stored in the lease database.  Currently
-      there are 3 database backends available: MySQL, PostgreSQL and experimental memfile.
+      there are 3 database backends available: MySQL, PostgreSQL and memfile.
       <footnote>
       <para>
       The server comes with an in-memory database ("memfile") configured as the default
@@ -5313,10 +5347,44 @@ Dhcp6/dhcp-ddns/qualifying-suffix   "example.com"   string
       </note>
 
       <section>
+      <title>Default storage for leases</title>
+      <para>
+        The server is able to store lease data in different repositories. Larger deployments
+        may elect to store leases in a database.
+        <xref linkend="database-configuration6"/> describes one way to do it.
+        By default, the server will use a CSV file rather than a database to store
+        lease information. One of the advantages of using a file is that it eliminates
+        dependency on third party database software.
+      </para>
+      <para>
+        The configuration of the file backend (Memfile)
+        is controlled through the Dhcp6/lease-database parameters. When default
+        parameters are left, the Memfile backend will write leases to a disk in the
+        [bind10-install-dir]/var/bind10/kea-leases6.csv.
+      </para>
+      <para>
+        It is possible to alter the default location of the lease file. The following
+        configuration:
+<screen>
+&gt; <userinput>config set Dhcp4/lease-database/type "memfile"</userinput>
+&gt; <userinput>config set Dhcp4/lease-database/persist true</userinput>
+&gt; <userinput>config set Dhcp4/lease-database/leasefile "/tmp/kea-leases6.csv"</userinput>
+&gt; <userinput>config commit</userinput>
+</screen>
+        will change the default location of the lease file to /tmp/kea-leases6.csv.
+      </para>
+      <para>
+        The "persist" parameter controls whether the leases are written to disk.
+        It is strongly recommended that this parameter is set to "true" at all times
+        during the normal operation of the server.
+      </para>
+      </section>
+
+      <section id="database-configuration6">
       <title>Database Configuration</title>
       <para>
       All leases issued by the server are stored in the lease database. Currently
-      there are 3 database backends available: MySQL, PostgreSQL and experimental memfile.
+      there are 3 database backends available: MySQL, PostgreSQL and memfile.
       <footnote>
       <para>
       The server comes with an in-memory database ("memfile") configured as the default

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

@@ -412,7 +412,7 @@ DhcpConfigParser* createGlobalDhcp4ConfigParser(const std::string& config_id) {
         parser  = new StringParser(config_id,
                                     globalContext()->string_values_);
     } else if (config_id.compare("lease-database") == 0) {
-        parser = new DbAccessParser(config_id);
+        parser = new DbAccessParser(config_id, *globalContext());
     } else if (config_id.compare("hooks-libraries") == 0) {
         parser = new HooksLibrariesParser(config_id);
     } else if (config_id.compare("echo-client-id") == 0) {

+ 6 - 0
src/bin/dhcp4/dhcp4.spec

@@ -191,6 +191,12 @@
                 "item_type": "string",
                 "item_optional": true,
                 "item_default": ""
+            },
+            {
+                "item_name": "persist",
+                "item_type": "boolean",
+                "item_optional": true,
+                "item_default": true
             }
         ]
       },

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

@@ -88,7 +88,7 @@ public:
     /// @param direct_response_desired specifies if it is desired to
     /// use direct V4 traffic.
     Dhcpv4Srv(uint16_t port = DHCP4_SERVER_PORT,
-              const char* dbconfig = "type=memfile",
+              const char* dbconfig = "type=memfile universe=4",
               const bool use_bcast = true,
               const bool direct_response_desired = true);
 

+ 2 - 1
src/bin/dhcp4/tests/dhcp4_test_utils.h

@@ -117,7 +117,8 @@ public:
     /// @param port port number to listen on; the default value 0 indicates
     /// that sockets should not be opened.
     NakedDhcpv4Srv(uint16_t port = 0)
-        : Dhcpv4Srv(port, "type=memfile", false, false) {
+        : Dhcpv4Srv(port, "type=memfile universe=4 persist=false",
+                    false, false) {
         // Create fixed server id.
         server_id_.reset(new Option4AddrLst(DHO_DHCP_SERVER_IDENTIFIER,
                                             asiolink::IOAddress("192.0.3.1")));

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

@@ -643,7 +643,7 @@ DhcpConfigParser* createGlobal6DhcpConfigParser(const std::string& config_id) {
         parser  = new StringParser(config_id,
                                    globalContext()->string_values_);
     } else if (config_id.compare("lease-database") == 0) {
-        parser = new DbAccessParser(config_id);
+        parser = new DbAccessParser(config_id, *globalContext());
     } else if (config_id.compare("hooks-libraries") == 0) {
         parser = new HooksLibrariesParser(config_id);
     } else if (config_id.compare("dhcp-ddns") == 0) {

+ 6 - 0
src/bin/dhcp6/dhcp6.spec

@@ -185,6 +185,12 @@
                 "item_type": "string",
                 "item_optional": true,
                 "item_default": ""
+            },
+            {
+                "item_name": "persist",
+                "item_type": "boolean",
+                "item_optional": true,
+                "item_default": true
             }
         ]
       },

+ 1 - 1
src/bin/dhcp6/tests/dhcp6_test_utils.h

@@ -45,7 +45,7 @@ class NakedDhcpv6Srv: public isc::dhcp::Dhcpv6Srv {
 public:
     NakedDhcpv6Srv(uint16_t port) : isc::dhcp::Dhcpv6Srv(port) {
         // Open the "memfile" database for leases
-        std::string memfile = "type=memfile";
+        std::string memfile = "type=memfile universe=6 persist=false";
         isc::dhcp::LeaseMgrFactory::create(memfile);
     }
 

+ 52 - 2
src/lib/dhcp/duid.cc

@@ -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
@@ -14,8 +14,11 @@
 
 #include <dhcp/duid.h>
 #include <exceptions/exceptions.h>
+#include <util/encode/hex.h>
 #include <util/io_utilities.h>
-
+#include <boost/algorithm/string/classification.hpp>
+#include <boost/algorithm/string/constants.hpp>
+#include <boost/algorithm/string/split.hpp>
 #include <iomanip>
 #include <sstream>
 #include <vector>
@@ -46,6 +49,41 @@ DUID::DUID(const uint8_t* data, size_t len) {
     duid_ = std::vector<uint8_t>(data, data + len);
 }
 
+std::vector<uint8_t>
+DUID::decode(const std::string& text) {
+    /// @todo optimize stream operations here.
+    std::vector<std::string> split_text;
+    boost::split(split_text, text, boost::is_any_of(":"),
+                 boost::algorithm::token_compress_off);
+
+    std::ostringstream s;
+    for (size_t i = 0; i < split_text.size(); ++i) {
+        // If there are multiple tokens and the current one is empty, it
+        // means that two consecutive colons were specified. This is not
+        // allowed for client identifier.
+        if ((split_text.size() > 1) && split_text[i].empty()) {
+            isc_throw(isc::BadValue, "invalid identifier '" << text << "': "
+                      << " tokens must be separated with a single colon");
+
+        } else if (split_text[i].size() == 1) {
+            s << "0";
+
+        } else if (split_text[i].size() > 2) {
+            isc_throw(isc::BadValue, "invalid identifier '" << text << "'");
+        }
+        s << split_text[i];
+    }
+
+    std::vector<uint8_t> binary;
+    try {
+        util::encode::decodeHex(s.str(), binary);
+    } catch (const Exception& ex) {
+        isc_throw(isc::BadValue, "failed to create identifier from text '"
+                  << text << "': " << ex.what());
+    }
+    return (binary);
+}
+
 const std::vector<uint8_t>& DUID::getDuid() const {
     return (duid_);
 }
@@ -62,6 +100,12 @@ DUID::DUIDType DUID::getType() const {
     }
 }
 
+DUID
+DUID::fromText(const std::string& text) {
+    std::vector<uint8_t> binary = decode(text);
+    return DUID(binary);
+}
+
 std::string DUID::toText() const {
     std::stringstream tmp;
     tmp << std::hex;
@@ -118,6 +162,12 @@ std::string ClientId::toText() const {
     return (DUID::toText());
 }
 
+ClientIdPtr
+ClientId::fromText(const std::string& text) {
+    std::vector<uint8_t> binary = decode(text);
+    return (ClientIdPtr(new ClientId(binary)));
+}
+
 // Compares two client-ids
 bool ClientId::operator==(const ClientId& other) const {
     return (this->duid_ == other.duid_);

+ 46 - 5
src/lib/dhcp/duid.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
@@ -69,6 +69,17 @@ class DUID {
     /// @brief Returns the DUID type
     DUIDType getType() const;
 
+    /// @brief Create DUID from the textual format.
+    ///
+    /// This static function parses a DUID specified in the textual format.
+    /// Internally it uses @c DUID::decode to parse the DUID.
+    ///
+    /// @param text DUID in the hexadecimal format with digits representing
+    /// individual bytes separated by colons.
+    ///
+    /// @throw isc::BadValue if parsing the DUID failed.
+    static DUID fromText(const std::string& text);
+
     /// @brief Returns textual representation of a DUID (e.g. 00:01:02:03:ff)
     std::string toText() const;
 
@@ -79,6 +90,23 @@ class DUID {
     bool operator!=(const DUID& other) const;
 
  protected:
+
+    /// @brief Decodes the textual format of the DUID.
+    ///
+    /// The format being parsed should match the DUID representation returned
+    /// by the @c DUID::toText method, i.e. the pairs of hexadecimal digits
+    /// representing bytes of DUID must be separated by colons. Usually the
+    /// single byte is represented by two hexadecimal digits. However, this
+    /// function allows one digit per byte. In this case, a zero is prepended
+    /// before the conversion. For example, a DUID 0:1:2::4:5 equals to
+    /// 00:01:02:00:04:05.
+    ///
+    /// @param text DUID in the hexadecimal format with digits representing
+    /// individual bytes separated by colons.
+    ///
+    /// @throw isc::BadValue if parsing the DUID failed.
+    static std::vector<uint8_t> decode(const std::string& text);
+
     /// The actual content of the DUID
     std::vector<uint8_t> duid_;
 };
@@ -86,7 +114,10 @@ class DUID {
 /// @brief Shared pointer to a DUID
 typedef boost::shared_ptr<DUID> DuidPtr;
 
-
+/// @brief Forward declaration to the @c ClientId class.
+class ClientId;
+/// @brief Shared pointer to a Client ID.
+typedef boost::shared_ptr<ClientId> ClientIdPtr;
 
 /// @brief Holds Client identifier or client IPv4 address
 ///
@@ -130,6 +161,19 @@ public:
     /// @brief Returns textual representation of a DUID (e.g. 00:01:02:03:ff)
     std::string toText() const;
 
+    /// @brief Create client identifier from the textual format.
+    ///
+    /// This static function creates the instance of the @c ClientId from the
+    /// textual format. Internally it calls @c DUID::fromText. The format of
+    /// the input must match the format of the DUID in @c DUID::fromText.
+    ///
+    /// @param text Client identifier in the textual format.
+    ///
+    /// @return Pointer to the instance of the @c ClientId.
+    /// @throw isc::BadValue if parsing the client identifier failed.
+    /// @throw isc::OutOfRange if the client identifier is truncated.
+    static ClientIdPtr fromText(const std::string& text);
+
     /// @brief Compares two client-ids for equality
     bool operator==(const ClientId& other) const;
 
@@ -137,9 +181,6 @@ public:
     bool operator!=(const ClientId& other) const;
 };
 
-/// @brief Shared pointer to a Client ID.
-typedef boost::shared_ptr<ClientId> ClientIdPtr;
-
 }; // end of isc::dhcp namespace
 }; // end of isc namespace
 

+ 45 - 3
src/lib/dhcp/hwaddr.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012 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
@@ -15,6 +15,10 @@
 #include <dhcp/hwaddr.h>
 #include <dhcp/dhcp4.h>
 #include <exceptions/exceptions.h>
+#include <util/encode/hex.h>
+#include <boost/algorithm/string/classification.hpp>
+#include <boost/algorithm/string/constants.hpp>
+#include <boost/algorithm/string/split.hpp>
 #include <iomanip>
 #include <sstream>
 #include <vector>
@@ -42,9 +46,11 @@ HWAddr::HWAddr(const std::vector<uint8_t>& hwaddr, uint8_t htype)
     }
 }
 
-std::string HWAddr::toText() const {
+std::string HWAddr::toText(bool include_htype) const {
     std::stringstream tmp;
-    tmp << "hwtype=" << static_cast<int>(htype_) << " ";
+    if (include_htype) {
+        tmp << "hwtype=" << static_cast<int>(htype_) << " ";
+    }
     tmp << std::hex;
     bool delim = false;
     for (std::vector<uint8_t>::const_iterator it = hwaddr_.begin();
@@ -58,6 +64,42 @@ std::string HWAddr::toText() const {
     return (tmp.str());
 }
 
+HWAddr
+HWAddr::fromText(const std::string& text, const uint8_t htype) {
+    /// @todo optimize stream operations here.
+    std::vector<std::string> split_text;
+    boost::split(split_text, text, boost::is_any_of(":"),
+                 boost::algorithm::token_compress_off);
+
+    std::ostringstream s;
+    for (size_t i = 0; i < split_text.size(); ++i) {
+        // If there are multiple tokens and the current one is empty, it
+        // means that two consecutive colons were specified. This is not
+        // allowed for hardware address.
+        if ((split_text.size() > 1) && split_text[i].empty()) {
+            isc_throw(isc::BadValue, "failed to create hardware address"
+                      " from text '" << text << "': tokens of the hardware"
+                      " address must be separated with a single colon");
+
+        } else  if (split_text[i].size() == 1) {
+            s << "0";
+
+        } else if (split_text[i].size() > 2) {
+            isc_throw(isc::BadValue, "invalid hwaddr '" << text << "'");
+        }
+        s << split_text[i];
+    }
+
+    std::vector<uint8_t> binary;
+    try {
+        util::encode::decodeHex(s.str(), binary);
+    } catch (const Exception& ex) {
+        isc_throw(isc::BadValue, "failed to create hwaddr from text '"
+                  << text << "': " << ex.what());
+    }
+    return (HWAddr(binary, htype));
+}
+
 bool HWAddr::operator==(const HWAddr& other) const {
     return ((this->htype_  == other.htype_) &&
             (this->hwaddr_ == other.hwaddr_));

+ 31 - 3
src/lib/dhcp/hwaddr.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
@@ -54,8 +54,36 @@ public:
     // Hardware type
     uint8_t htype_;
 
-    /// @brief Returns textual representation of a client-id (e.g. 00:01:02:03)
-    std::string toText() const;
+    /// @brief Returns textual representation of a hardware address
+    /// (e.g. 00:01:02:03:04:05)
+    ///
+    /// @param include_htype Boolean value which controls whether the hardware
+    /// type is included in the returned string (true), or not (false).
+    ///
+    /// @return Hardware address in the textual format.
+    std::string toText(bool include_htype = true) const;
+
+    /// @brief Creates instance of the hardware address from textual format.
+    ///
+    /// This function parses HW address specified as text and creates the
+    /// corresponding @c HWAddr instance. The hexadecimal digits representing
+    /// individual bytes of the hardware address should be separated with
+    /// colons. Typically, two digits per byte are used. However, this function
+    /// allows for 1 digit per HW address byte. In this case, the digit is
+    /// prepended with '0' during conversion to binary value.
+    ///
+    /// This function can be used to perform a reverse operation to the
+    /// @c HWAddr::toText(false).
+    ///
+    /// The instance created by this function sets HTYPE_ETHER as a hardware
+    /// type.
+    ///
+    /// @param text HW address in the textual format.
+    /// @param htype Hardware type.
+    ///
+    /// @return Instance of the HW address created from text.
+    static HWAddr fromText(const std::string& text,
+                           const uint8_t htype = HTYPE_ETHER);
 
     /// @brief Compares two hardware addresses for equality
     bool operator==(const HWAddr& other) const;

+ 65 - 1
src/lib/dhcp/tests/duid_unittest.cc

@@ -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
@@ -126,6 +126,37 @@ TEST(DuidTest, getType) {
     EXPECT_EQ(DUID::DUID_UNKNOWN, duid_invalid->getType());
 }
 
+// This test checks that the DUID instance can be created from the textual
+// format and that error is reported if the textual format is invalid.
+TEST(DuidTest, fromText) {
+    scoped_ptr<DUID> duid;
+    // DUID with only decimal digits.
+    ASSERT_NO_THROW(
+        duid.reset(new DUID(DUID::fromText("00:01:02:03:04:05:06")))
+    );
+    EXPECT_EQ("00:01:02:03:04:05:06", duid->toText());
+    // DUID with some hexadecimal digits (upper case and lower case).
+    ASSERT_NO_THROW(
+        duid.reset(new DUID(DUID::fromText("00:aa:bb:CD:ee:EF:ab")))
+    );
+    EXPECT_EQ("00:aa:bb:cd:ee:ef:ab", duid->toText());
+    // DUID with one digit for a particular byte.
+    ASSERT_NO_THROW(
+        duid.reset(new DUID(DUID::fromText("00:a:bb:D:ee:EF:ab")))
+    );
+    EXPECT_EQ("00:0a:bb:0d:ee:ef:ab", duid->toText());
+    // Repeated colon sign is not allowed.
+    EXPECT_THROW(
+        duid.reset(new DUID(DUID::fromText("00::bb:D:ee:EF:ab"))),
+        isc::BadValue
+    );
+    // DUID with excessive number of digits for one of the bytes.
+    EXPECT_THROW(
+       duid.reset(new DUID(DUID::fromText("00:01:021:03:04:05:06"))),
+       isc::BadValue
+    );
+}
+
 // Test checks if the toText() returns valid texual representation
 TEST(DuidTest, toText) {
     uint8_t data1[] = {0, 1, 2, 3, 4, 0xff, 0xfe};
@@ -249,4 +280,37 @@ TEST(ClientIdTest, toText) {
     EXPECT_EQ("00:01:02:03:04:ff:fe", clientid.toText());
 }
 
+// This test checks that the ClientId instance can be created from the textual
+// format and that error is reported if the textual format is invalid.
+TEST(ClientIdTest, fromText) {
+    ClientIdPtr cid;
+    // ClientId with only decimal digits.
+    ASSERT_NO_THROW(
+        cid = ClientId::fromText("00:01:02:03:04:05:06")
+    );
+    EXPECT_EQ("00:01:02:03:04:05:06", cid->toText());
+    // ClientId with some hexadecimal digits (upper case and lower case).
+    ASSERT_NO_THROW(
+        cid = ClientId::fromText("00:aa:bb:CD:ee:EF:ab")
+    );
+    EXPECT_EQ("00:aa:bb:cd:ee:ef:ab", cid->toText());
+    // ClientId with one digit for a particular byte.
+    ASSERT_NO_THROW(
+        cid = ClientId::fromText("00:a:bb:D:ee:EF:ab")
+    );
+    EXPECT_EQ("00:0a:bb:0d:ee:ef:ab", cid->toText());
+    // Repeated colon sign in the ClientId is not allowed.
+    EXPECT_THROW(
+        ClientId::fromText("00::bb:D:ee:EF:ab"),
+        isc::BadValue
+
+    );
+    // ClientId with excessive number of digits for one of the bytes.
+    EXPECT_THROW(
+        ClientId::fromText("00:01:021:03:04:05:06"),
+        isc::BadValue
+    );
+}
+
+
 } // end of anonymous namespace

+ 32 - 1
src/lib/dhcp/tests/hwaddr_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012 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
@@ -108,6 +108,9 @@ TEST(HWAddrTest, toText) {
 
     EXPECT_EQ("hwtype=15 00:01:02:03:04:05", hw->toText());
 
+    // In some cases we don't want htype value to be included. Check that
+    // it can be forced.
+    EXPECT_EQ("00:01:02:03:04:05", hw->toText(false));
 }
 
 TEST(HWAddrTest, stringConversion) {
@@ -131,5 +134,33 @@ TEST(HWAddrTest, stringConversion) {
     EXPECT_EQ(std::string("hwtype=1 c3:07:a2:e8:42"), result);
 }
 
+// Checks that the HW address can be created from the textual format.
+TEST(HWAddrTest, fromText) {
+    scoped_ptr<HWAddr> hwaddr;
+    // Create HWAddr from text.
+    ASSERT_NO_THROW(
+        hwaddr.reset(new HWAddr(HWAddr::fromText("00:01:A:bc:d:67")));
+    );
+    EXPECT_EQ("00:01:0a:bc:0d:67", hwaddr->toText(false));
+
+    // HWAddr class should allow empty address.
+    ASSERT_NO_THROW(
+        hwaddr.reset(new HWAddr(HWAddr::fromText("")));
+    );
+    EXPECT_TRUE(hwaddr->toText(false).empty());
+
+    // HWAddr should not allow multiple consecutive colons.
+    EXPECT_THROW(
+       hwaddr.reset(new HWAddr(HWAddr::fromText("00::01:00:bc:0d:67"))),
+       isc::BadValue
+    );
+
+    // There should be no more than two digits per byte of the HW addr.
+    EXPECT_THROW(
+       hwaddr.reset(new HWAddr(HWAddr::fromText("00:01:00A:bc:0d:67"))),
+       isc::BadValue
+    );
+
+}
 
 } // end of anonymous namespace

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

@@ -36,12 +36,16 @@ AM_CXXFLAGS += $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG)
 
 # Make sure the generated files are deleted in a "clean" operation
 CLEANFILES = *.gcno *.gcda dhcpsrv_messages.h dhcpsrv_messages.cc s-messages
+# Remove CSV files created by the CSVLeaseFile6 and CSVLeaseFile4 unit tests.
+CLEANFILES += *.csv
 
 lib_LTLIBRARIES = libb10-dhcpsrv.la
 libb10_dhcpsrv_la_SOURCES  =
 libb10_dhcpsrv_la_SOURCES += addr_utilities.cc addr_utilities.h
 libb10_dhcpsrv_la_SOURCES += alloc_engine.cc alloc_engine.h
 libb10_dhcpsrv_la_SOURCES += callout_handle_store.h
+libb10_dhcpsrv_la_SOURCES += csv_lease_file4.cc csv_lease_file4.h
+libb10_dhcpsrv_la_SOURCES += csv_lease_file6.cc csv_lease_file6.h
 libb10_dhcpsrv_la_SOURCES += d2_client_cfg.cc d2_client_cfg.h
 libb10_dhcpsrv_la_SOURCES += d2_client_mgr.cc d2_client_mgr.h
 libb10_dhcpsrv_la_SOURCES += dbaccess_parser.cc dbaccess_parser.h

+ 178 - 0
src/lib/dhcpsrv/csv_lease_file4.cc

@@ -0,0 +1,178 @@
+// 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 <dhcpsrv/csv_lease_file4.h>
+
+using namespace isc::asiolink;
+using namespace isc::util;
+
+namespace isc {
+namespace dhcp {
+
+CSVLeaseFile4::CSVLeaseFile4(const std::string& filename)
+    : CSVFile(filename) {
+    initColumns();
+}
+
+void
+CSVLeaseFile4::append(const Lease4& lease) const {
+    CSVRow row(getColumnCount());
+    row.writeAt(getColumnIndex("address"), lease.addr_.toText());
+    HWAddr hwaddr(lease.hwaddr_, HTYPE_ETHER);
+    row.writeAt(getColumnIndex("hwaddr"), hwaddr.toText(false));
+    // Client id may be unset (NULL).
+    if (lease.client_id_) {
+        row.writeAt(getColumnIndex("client_id"), lease.client_id_->toText());
+    }
+    row.writeAt(getColumnIndex("valid_lifetime"), lease.valid_lft_);
+    row.writeAt(getColumnIndex("expire"), lease.cltt_ + lease.valid_lft_);
+    row.writeAt(getColumnIndex("subnet_id"), lease.subnet_id_);
+    row.writeAt(getColumnIndex("fqdn_fwd"), lease.fqdn_fwd_);
+    row.writeAt(getColumnIndex("fqdn_rev"), lease.fqdn_rev_);
+    row.writeAt(getColumnIndex("hostname"), lease.hostname_);
+    CSVFile::append(row);
+}
+
+bool
+CSVLeaseFile4::next(Lease4Ptr& lease) {
+    // Read the CSV row and try to create a lease from the values read.
+    // This may easily result in exception. We don't want this function
+    // to throw exceptions, so we catch them all and rather return the
+    // false value.
+    try {
+        // Get the row of CSV values.
+        CSVRow row;
+        CSVFile::next(row);
+        // The empty row signals EOF.
+        if (row == CSVFile::EMPTY_ROW()) {
+            lease.reset();
+            return (true);
+        }
+
+        // Get client id. It is possible that the client id is empty and the
+        // returned pointer is NULL. This is ok, but if the client id is NULL,
+        // we need to be careful to not use the NULL pointer.
+        ClientIdPtr client_id = readClientId(row);
+        std::vector<uint8_t> client_id_vec;
+        if (client_id) {
+            client_id_vec = client_id->getClientId();
+        }
+        size_t client_id_len = client_id_vec.size();
+
+        // Get the HW address. It should never be empty and the readHWAddr checks
+        // that.
+        HWAddr hwaddr = readHWAddr(row);
+        lease.reset(new Lease4(readAddress(row),
+                               &hwaddr.hwaddr_[0], hwaddr.hwaddr_.size(),
+                               client_id_vec.empty() ? NULL : &client_id_vec[0],
+                               client_id_len,
+                               readValid(row),
+                               0, 0, // t1, t2 = 0
+                               readCltt(row),
+                               readSubnetID(row),
+                               readFqdnFwd(row),
+                               readFqdnRev(row),
+                               readHostname(row)));
+
+    } catch (std::exception& ex) {
+        // The lease might have been created, so let's set it back to NULL to
+        // signal that lease hasn't been parsed.
+        lease.reset();
+        setReadMsg(ex.what());
+        return (false);
+    }
+    return (true);
+}
+
+void
+CSVLeaseFile4::initColumns() {
+    addColumn("address");
+    addColumn("hwaddr");
+    addColumn("client_id");
+    addColumn("valid_lifetime");
+    addColumn("expire");
+    addColumn("subnet_id");
+    addColumn("fqdn_fwd");
+    addColumn("fqdn_rev");
+    addColumn("hostname");
+}
+
+IOAddress
+CSVLeaseFile4::readAddress(const CSVRow& row) {
+    IOAddress address(row.readAt(getColumnIndex("address")));
+    return (address);
+}
+
+HWAddr
+CSVLeaseFile4::readHWAddr(const CSVRow& row) {
+    HWAddr hwaddr = HWAddr::fromText(row.readAt(getColumnIndex("hwaddr")));
+    if (hwaddr.hwaddr_.empty()) {
+        isc_throw(isc::BadValue, "hardware address in the lease file"
+                  " must not be empty");
+    }
+    return (hwaddr);
+}
+
+ClientIdPtr
+CSVLeaseFile4::readClientId(const CSVRow& row) {
+    std::string client_id = row.readAt(getColumnIndex("client_id"));
+    // NULL client ids are allowed in DHCPv4.
+    if (client_id.empty()) {
+        return (ClientIdPtr());
+    }
+    ClientIdPtr cid = ClientId::fromText(client_id);
+    return (cid);
+}
+
+uint32_t
+CSVLeaseFile4::readValid(const CSVRow& row) {
+    uint32_t valid =
+        row.readAndConvertAt<uint32_t>(getColumnIndex("valid_lifetime"));
+    return (valid);
+}
+
+time_t
+CSVLeaseFile4::readCltt(const CSVRow& row) {
+    uint32_t cltt = row.readAndConvertAt<uint32_t>(getColumnIndex("expire"))
+        - readValid(row);
+    return (cltt);
+}
+
+SubnetID
+CSVLeaseFile4::readSubnetID(const CSVRow& row) {
+    SubnetID subnet_id =
+        row.readAndConvertAt<SubnetID>(getColumnIndex("subnet_id"));
+    return (subnet_id);
+}
+
+bool
+CSVLeaseFile4::readFqdnFwd(const CSVRow& row) {
+    bool fqdn_fwd = row.readAndConvertAt<bool>(getColumnIndex("fqdn_fwd"));
+    return (fqdn_fwd);
+}
+
+bool
+CSVLeaseFile4::readFqdnRev(const CSVRow& row) {
+    bool fqdn_rev = row.readAndConvertAt<bool>(getColumnIndex("fqdn_rev"));
+    return (fqdn_rev);
+}
+
+std::string
+CSVLeaseFile4::readHostname(const CSVRow& row) {
+    std::string hostname = row.readAt(getColumnIndex("hostname"));
+    return (hostname);
+}
+
+} // end of namespace isc::dhcp
+} // end of namespace isc

+ 153 - 0
src/lib/dhcpsrv/csv_lease_file4.h

@@ -0,0 +1,153 @@
+// 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.
+
+#ifndef CSV_LEASE_FILE4_H
+#define CSV_LEASE_FILE4_H
+
+#include <asiolink/io_address.h>
+#include <dhcp/duid.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/subnet.h>
+#include <util/csv_file.h>
+#include <stdint.h>
+#include <string>
+#include <time.h>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Provides methods to access CSV file with DHCPv4 leases.
+///
+/// This class contains methods customized to read and write DHCPv4 leases from
+/// and to the CSV file. It expects that the CSV file being parsed contains a
+/// set of columns with well known names (initialized in the class constructor).
+///
+/// @todo This class doesn't validate the lease values read from the file.
+/// The @c Lease4 is a structure that should be itself responsible for this
+/// validation (see http://bind10.isc.org/ticket/2405). However, when #2405
+/// is implemented, the @c next function may need to be updated to use the
+/// validation capablity of @c Lease4.
+class CSVLeaseFile4 : public isc::util::CSVFile {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Initializes columns of the lease file.
+    ///
+    /// @param filename Name of the lease file.
+    CSVLeaseFile4(const std::string& filename);
+
+    /// @brief Appends the lease record to the CSV file.
+    ///
+    /// This function doesn't throw exceptions itself. In theory, exceptions
+    /// are possible when the index of the indexes of the values being written
+    /// to the file are invalid. However, this would have been a programming
+    /// error.
+    ///
+    /// @param lease Structure representing a DHCPv4 lease.
+    void append(const Lease4& lease) const;
+
+    /// @brief Reads next lease from the CSV file.
+    ///
+    /// If this function hits an error during lease read, it sets the error
+    /// message using @c CSVFile::setReadMsg and returns false. The error
+    /// string may be read using @c CSVFile::getReadMsg.
+    ///
+    /// This function is exception safe.
+    ///
+    /// @param [out] lease Pointer to the lease read from CSV file or
+    /// NULL pointer if lease hasn't been read.
+    ///
+    /// @return Boolean value indicating that the new lease has been
+    /// read from the CSV file (if true), or that the error has occurred
+    /// (false).
+    ///
+    /// @todo Make sure that the values read from the file are correct.
+    /// The appropriate @c Lease4 validation mechanism should be used once
+    /// ticket http://bind10.isc.org/ticket/2405 is implemented.
+    bool next(Lease4Ptr& lease);
+
+private:
+
+    /// @brief Initializes columns of the CSV file holding leases.
+    ///
+    /// This function initializes the following columns:
+    /// - address
+    /// - hwaddr
+    /// - client_id
+    /// - valid_lifetime
+    /// - expire
+    /// - subnet_id
+    /// - fqdn_fwd
+    /// - fqdn_rev
+    /// - hostname
+    void initColumns();
+
+    ///
+    /// @name Methods which read specific lease fields from the CSV row.
+    ///
+    //@{
+    ///
+    /// @brief Reads lease address from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    asiolink::IOAddress readAddress(const util::CSVRow& row);
+
+    /// @brief Reads HW address from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    HWAddr readHWAddr(const util::CSVRow& row);
+
+    /// @brief Reads client identifier from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    ClientIdPtr readClientId(const util::CSVRow& row);
+
+    /// @brief Reads valid lifetime from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    uint32_t readValid(const util::CSVRow& row);
+
+    /// @brief Reads cltt value from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    time_t readCltt(const util::CSVRow& row);
+
+    /// @brief Reads subnet id from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    SubnetID readSubnetID(const util::CSVRow& row);
+
+    /// @brief Reads the FQDN forward flag from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    bool readFqdnFwd(const util::CSVRow& row);
+
+    /// @brief Reads the FQDN reverse flag from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    bool readFqdnRev(const util::CSVRow& row);
+
+    /// @brief Reads hostname from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    std::string readHostname(const util::CSVRow& row);
+    //@}
+
+};
+
+} // namespace isc::dhcp
+} // namespace isc
+
+#endif // CSV_LEASE_FILE4_H

+ 176 - 0
src/lib/dhcpsrv/csv_lease_file6.cc

@@ -0,0 +1,176 @@
+// 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 <dhcpsrv/csv_lease_file6.h>
+
+using namespace isc::asiolink;
+using namespace isc::util;
+
+namespace isc {
+namespace dhcp {
+
+CSVLeaseFile6::CSVLeaseFile6(const std::string& filename)
+    : CSVFile(filename) {
+    initColumns();
+}
+
+void
+CSVLeaseFile6::append(const Lease6& lease) const {
+    CSVRow row(getColumnCount());
+    row.writeAt(getColumnIndex("address"), lease.addr_.toText());
+    row.writeAt(getColumnIndex("duid"), lease.duid_->toText());
+    row.writeAt(getColumnIndex("valid_lifetime"), lease.valid_lft_);
+    row.writeAt(getColumnIndex("expire"), lease.cltt_ + lease.valid_lft_);
+    row.writeAt(getColumnIndex("subnet_id"), lease.subnet_id_);
+    row.writeAt(getColumnIndex("pref_lifetime"), lease.preferred_lft_);
+    row.writeAt(getColumnIndex("lease_type"), lease.type_);
+    row.writeAt(getColumnIndex("iaid"), lease.iaid_);
+    row.writeAt(getColumnIndex("prefix_len"),
+                static_cast<int>(lease.prefixlen_));
+    row.writeAt(getColumnIndex("fqdn_fwd"), lease.fqdn_fwd_);
+    row.writeAt(getColumnIndex("fqdn_rev"), lease.fqdn_rev_);
+    row.writeAt(getColumnIndex("hostname"), lease.hostname_);
+    CSVFile::append(row);
+}
+
+bool
+CSVLeaseFile6::next(Lease6Ptr& lease) {
+    // Read the CSV row and try to create a lease from the values read.
+    // This may easily result in exception. We don't want this function
+    // to throw exceptions, so we catch them all and rather return the
+    // false value.
+    try {
+        // Get the row of CSV values.
+        CSVRow row;
+        CSVFile::next(row);
+        // The empty row signals EOF.
+        if (row == CSVFile::EMPTY_ROW()) {
+            lease.reset();
+            return (true);
+        }
+
+        lease.reset(new Lease6(readType(row), readAddress(row), readDUID(row),
+                               readIAID(row), readPreferred(row),
+                               readValid(row), 0, 0, // t1, t2 = 0
+                               readSubnetID(row),
+                               readPrefixLen(row)));
+        lease->cltt_ = readCltt(row);
+        lease->fqdn_fwd_ = readFqdnFwd(row);
+        lease->fqdn_rev_ = readFqdnRev(row);
+        lease->hostname_ = readHostname(row);
+
+    } catch (std::exception& ex) {
+        // The lease might have been created, so let's set it back to NULL to
+        // signal that lease hasn't been parsed.
+        lease.reset();
+        setReadMsg(ex.what());
+        return (false);
+    }
+    return (true);
+}
+
+void
+CSVLeaseFile6::initColumns() {
+    addColumn("address");
+    addColumn("duid");
+    addColumn("valid_lifetime");
+    addColumn("expire");
+    addColumn("subnet_id");
+    addColumn("pref_lifetime");
+    addColumn("lease_type");
+    addColumn("iaid");
+    addColumn("prefix_len");
+    addColumn("fqdn_fwd");
+    addColumn("fqdn_rev");
+    addColumn("hostname");
+}
+
+Lease::Type
+CSVLeaseFile6::readType(const CSVRow& row) {
+    return (static_cast<Lease::Type>
+            (row.readAndConvertAt<int>(getColumnIndex("lease_type"))));
+}
+
+IOAddress
+CSVLeaseFile6::readAddress(const CSVRow& row) {
+    IOAddress address(row.readAt(getColumnIndex("address")));
+    return (address);
+}
+
+DuidPtr
+CSVLeaseFile6::readDUID(const util::CSVRow& row) {
+    DuidPtr duid(new DUID(DUID::fromText(row.readAt(getColumnIndex("duid")))));
+    return (duid);
+}
+
+uint32_t
+CSVLeaseFile6::readIAID(const CSVRow& row) {
+    uint32_t iaid = row.readAndConvertAt<uint32_t>(getColumnIndex("iaid"));
+    return (iaid);
+}
+
+uint32_t
+CSVLeaseFile6::readPreferred(const CSVRow& row) {
+    uint32_t pref =
+        row.readAndConvertAt<uint32_t>(getColumnIndex("pref_lifetime"));
+    return (pref);
+}
+
+uint32_t
+CSVLeaseFile6::readValid(const CSVRow& row) {
+    uint32_t valid =
+        row.readAndConvertAt<uint32_t>(getColumnIndex("valid_lifetime"));
+    return (valid);
+}
+
+uint32_t
+CSVLeaseFile6::readCltt(const CSVRow& row) {
+    uint32_t cltt = row.readAndConvertAt<uint32_t>(getColumnIndex("expire"))
+        - readValid(row);
+    return (cltt);
+}
+
+SubnetID
+CSVLeaseFile6::readSubnetID(const CSVRow& row) {
+    SubnetID subnet_id =
+        row.readAndConvertAt<SubnetID>(getColumnIndex("subnet_id"));
+    return (subnet_id);
+}
+
+uint8_t
+CSVLeaseFile6::readPrefixLen(const CSVRow& row) {
+    int prefixlen = row.readAndConvertAt<int>(getColumnIndex("prefix_len"));
+    return (static_cast<uint8_t>(prefixlen));
+}
+
+bool
+CSVLeaseFile6::readFqdnFwd(const CSVRow& row) {
+    bool fqdn_fwd = row.readAndConvertAt<bool>(getColumnIndex("fqdn_fwd"));
+    return (fqdn_fwd);
+}
+
+bool
+CSVLeaseFile6::readFqdnRev(const CSVRow& row) {
+    bool fqdn_rev = row.readAndConvertAt<bool>(getColumnIndex("fqdn_rev"));
+    return (fqdn_rev);
+}
+
+std::string
+CSVLeaseFile6::readHostname(const CSVRow& row) {
+    std::string hostname = row.readAt(getColumnIndex("hostname"));
+    return (hostname);
+}
+
+} // end of namespace isc::dhcp
+} // end of namespace isc

+ 170 - 0
src/lib/dhcpsrv/csv_lease_file6.h

@@ -0,0 +1,170 @@
+// 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.
+
+#ifndef CSV_LEASE_FILE6_H
+#define CSV_LEASE_FILE6_H
+
+#include <asiolink/io_address.h>
+#include <dhcp/duid.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/subnet.h>
+#include <util/csv_file.h>
+#include <stdint.h>
+#include <string>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Provides methods to access CSV file with DHCPv6 leases.
+///
+/// This class contains methods customized to read and write DHCPv6 leases from
+/// and to the CSV file. It expects that the CSV file being parsed contains a
+/// set of columns with well known names (initialized in the class constructor).
+///
+/// @todo This class doesn't validate the lease values read from the file.
+/// The @c Lease6 is a structure that should be itself responsible for this
+/// validation (see http://bind10.isc.org/ticket/2405). However, when #2405
+/// is implemented, the @c next function may need to be updated to use the
+/// validation capablity of @c Lease6.
+class CSVLeaseFile6 : public isc::util::CSVFile {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Initializes columns of the lease file.
+    ///
+    /// @param filename Name of the lease file.
+    CSVLeaseFile6(const std::string& filename);
+
+    /// @brief Appends the lease record to the CSV file.
+    ///
+    /// This function doesn't throw exceptions itself. In theory, exceptions
+    /// are possible when the index of the indexes of the values being written
+    /// to the file are invalid. However, this would have been a programming
+    /// error.
+    ///
+    /// @param lease Structure representing a DHCPv6 lease.
+    void append(const Lease6& lease) const;
+
+    /// @brief Reads next lease from the CSV file.
+    ///
+    /// If this function hits an error during lease read, it sets the error
+    /// message using @c CSVFile::setReadMsg and returns false. The error
+    /// string may be read using @c CSVFile::getReadMsg.
+    ///
+    /// This function is exception safe.
+    ///
+    /// @param [out] lease Pointer to the lease read from CSV file or
+    /// NULL pointer if lease hasn't been read.
+    ///
+    /// @return Boolean value indicating that the new lease has been
+    /// read from the CSV file (if true), or that the error has occurred
+    /// (false).
+    ///
+    /// @todo Make sure that the values read from the file are correct.
+    /// The appropriate @c Lease6 validation mechanism should be used once
+    /// ticket http://bind10.isc.org/ticket/2405 is implemented.
+    bool next(Lease6Ptr& lease);
+
+private:
+
+    /// @brief Initializes columns of the CSV file holding leases.
+    ///
+    /// This function initializes the following columns:
+    /// - address
+    /// - duid
+    /// - valid_lifetime
+    /// - expire
+    /// - subnet_id
+    /// - pref_lifetime
+    /// - lease_type
+    /// - iaid
+    /// - prefix_len
+    /// - fqdn_fwd
+    /// - fqdn_rev
+    /// - hostname
+    void initColumns();
+
+    ///
+    /// @name Methods which read specific lease fields from the CSV row.
+    ///
+    //@{
+    ///
+    /// @brief Reads lease type from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    Lease::Type readType(const util::CSVRow& row);
+
+    /// @brief Reads lease address from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    asiolink::IOAddress readAddress(const util::CSVRow& row);
+
+    /// @brief Reads DUID from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    DuidPtr readDUID(const util::CSVRow& row);
+
+    /// @brief Reads IAID from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    uint32_t readIAID(const util::CSVRow& row);
+
+    /// @brief Reads preferred lifetime from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    uint32_t readPreferred(const util::CSVRow& row);
+
+    /// @brief Reads valid lifetime from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    uint32_t readValid(const util::CSVRow& row);
+
+    /// @brief Reads cltt value from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    uint32_t readCltt(const util::CSVRow& row);
+
+    /// @brief Reads subnet id from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    SubnetID readSubnetID(const util::CSVRow& row);
+
+    /// @brief Reads prefix length from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    uint8_t readPrefixLen(const util::CSVRow& row);
+
+    /// @brief Reads the FQDN forward flag from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    bool readFqdnFwd(const util::CSVRow& row);
+
+    /// @brief Reads the FQDN reverse flag from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    bool readFqdnRev(const util::CSVRow& row);
+
+    /// @brief Reads hostname from the CSV file row.
+    ///
+    /// @param row CSV file holding lease values.
+    std::string readHostname(const util::CSVRow& row);
+    //@}
+
+};
+
+} // namespace isc::dhcp
+} // namespace isc
+
+#endif // CSV_LEASE_FILE6_H

+ 25 - 12
src/lib/dhcpsrv/dbaccess_parser.cc

@@ -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
@@ -12,6 +12,7 @@
 // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 // PERFORMANCE OF THIS SOFTWARE.
 
+#include <dhcp/option.h>
 #include <dhcpsrv/dbaccess_parser.h>
 #include <dhcpsrv/dhcpsrv_log.h>
 #include <dhcpsrv/lease_mgr_factory.h>
@@ -30,11 +31,9 @@ namespace dhcp {
 
 
 // Factory function to build the parser
-DbAccessParser::DbAccessParser(const std::string& param_name) : values_()
+DbAccessParser::DbAccessParser(const std::string&, const ParserContext& ctx)
+    : values_(), ctx_(ctx)
 {
-    if (param_name != "lease-database") {
-        LOG_WARN(dhcpsrv_logger, DHCPSRV_UNEXPECTED_NAME).arg(param_name);
-    }
 }
 
 // Parse the configuration and check that the various keywords are consistent.
@@ -43,19 +42,33 @@ DbAccessParser::build(isc::data::ConstElementPtr config_value) {
 
     // To cope with incremental updates, the strategy is:
     // 1. Take a copy of the stored keyword/value pairs.
-    // 2. Update the copy with the passed keywords.
-    // 3. Perform validation checks on the updated keyword/value pairs.
-    // 4. If all is OK, update the stored keyword/value pairs.
+    // 2. Inject the universe parameter.
+    // 3. Update the copy with the passed keywords.
+    // 4. Perform validation checks on the updated keyword/value pairs.
+    // 5. If all is OK, update the stored keyword/value pairs.
 
     // 1. Take a copy of the stored keyword/value pairs.
     std::map<string, string> values_copy = values_;
 
-    // 2. Update the copy with the passed keywords.
+    // 2. Inject the parameter which defines whether we are configuring
+    // DHCPv4 or DHCPv6. Some database backends (e.g. Memfile make
+    // use of it).
+    values_copy["universe"] = ctx_.universe_ == Option::V4 ? "4" : "6";
+
+    // 3. Update the copy with the passed keywords.
     BOOST_FOREACH(ConfigPair param, config_value->mapValue()) {
-        values_copy[param.first] = param.second->stringValue();
+        // The persist parameter is the only boolean parameter at the
+        // moment. It needs special handling.
+        if (param.first != "persist") {
+            values_copy[param.first] = param.second->stringValue();
+
+        } else {
+            values_copy[param.first] = (param.second->boolValue() ?
+                                        "true" : "false");
+        }
     }
 
-    // 3. Perform validation checks on the updated set of keyword/values.
+    // 4. Perform validation checks on the updated set of keyword/values.
     //
     // a. Check if the "type" keyword exists and thrown an exception if not.
     StringPairMap::const_iterator type_ptr = values_copy.find("type");
@@ -71,7 +84,7 @@ DbAccessParser::build(isc::data::ConstElementPtr config_value) {
         isc_throw(BadValue, "unknown backend database type: " << dbtype);
     }
 
-    // 4. If all is OK, update the stored keyword/value pairs.  We do this by
+    // 5. If all is OK, update the stored keyword/value pairs.  We do this by
     // swapping contents - values_copy is destroyed immediately after the
     // operation (when the method exits), so we are not interested in its new
     // value.

+ 11 - 4
src/lib/dhcpsrv/dbaccess_parser.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
@@ -17,6 +17,7 @@
 
 #include <cc/data.h>
 #include <dhcpsrv/dhcp_config_parser.h>
+#include <dhcpsrv/dhcp_parsers.h>
 #include <exceptions/exceptions.h>
 
 #include <string>
@@ -54,7 +55,8 @@ public:
     ///
     /// @param param_name Name of the parameter under which the database
     ///        access details are held.
-    DbAccessParser(const std::string& param_name);
+    /// @param ctx Parser context.
+    DbAccessParser(const std::string& param_name, const ParserContext& ctx);
 
     /// The destructor.
     virtual ~DbAccessParser()
@@ -96,11 +98,13 @@ public:
     ///
     /// @param param_name Name of the parameter used to access the
     /// 	configuration.
+    /// @param ctx Parser context.
     ///
     /// @return Pointer to a DbAccessParser.  The caller is responsible for
     ///         destroying the parser after use.
-    static DhcpConfigParser* factory(const std::string& param_name) {
-        return (new DbAccessParser(param_name));
+    static DhcpConfigParser* factory(const std::string& param_name,
+                                     const ParserContext& ctx) {
+        return (new DbAccessParser(param_name, ctx));
     }
 
 protected:
@@ -124,7 +128,10 @@ protected:
     std::string getDbAccessString() const;
 
 private:
+
     std::map<std::string, std::string> values_; ///< Stored parameter values
+
+    ParserContext ctx_; ///< Parser context
 };
 
 };  // namespace dhcp

+ 25 - 7
src/lib/dhcpsrv/dhcpsrv_messages.mes

@@ -279,6 +279,31 @@ subnet ID and hardware address.
 A debug message issued when the server is about to obtain schema version
 information from the memory file database.
 
+% DHCPSRV_MEMFILE_LEASE_LOAD4 loading lease %1
+A debug message issued when DHCPv4 lease is being loaded from the file to
+memory.
+
+% DHCPSRV_MEMFILE_LEASE_LOAD6 loading lease %1
+A debug message issued when DHCPv6 lease is being loaded from the file to
+memory.
+
+% DHCPSRV_MEMFILE_LEASES_RELOAD4 reloading leases from %1
+An info message issued when server is about to start reading DHCPv4 leases
+from the lease file. All leases currently held in the memory will be
+replaced by those read from the file.
+
+% DHCPSRV_MEMFILE_LEASES_RELOAD6 reloading leases from %1
+An info message issued when server is about to start reading DHCPv6 leases
+from the lease file. All leases currently held in the memory will be
+replaced by those read from the file.
+
+% DHCPSRV_MEMFILE_NO_STORAGE running in non-persistent mode, leases will be lost after restart
+A warning message issued when writes of leases to disk have been disabled
+in the configuration. This mode is useful for some kinds of performance
+testing but should not be enabled in normal circumstances. Non-persistence
+mode is enabled when 'persist4=no persist6=no' parameters are specified
+in the database access string.
+
 % DHCPSRV_MEMFILE_ROLLBACK rolling back memory file database
 The code has issued a rollback call.  For the memory file database, this is
 a no-op.
@@ -291,13 +316,6 @@ lease from the memory file database for the specified address.
 A debug message issued when the server is attempting to update IPv6
 lease from the memory file database for the specified address.
 
-% DHCPSRV_MEMFILE_WARNING using early version of memfile lease database - leases will be lost after a restart
-This warning message is issued when the 'memfile' lease database is
-opened.  The current version of memfile does not store anything
-to disk, so lease information will be lost in the event of a restart.
-Using this version of memfile in a production environment is NOT
-recommended.
-
 % DHCPSRV_MYSQL_ADD_ADDR4 adding IPv4 lease with address %1
 A debug message issued when the server is about to add an IPv4 lease
 with the specified address to the MySQL backend database.

+ 270 - 2
src/lib/dhcpsrv/memfile_lease_mgr.cc

@@ -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
@@ -12,6 +12,7 @@
 // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 // PERFORMANCE OF THIS SOFTWARE.
 
+#include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/dhcpsrv_log.h>
 #include <dhcpsrv/memfile_lease_mgr.h>
 #include <exceptions/exceptions.h>
@@ -22,10 +23,42 @@ using namespace isc::dhcp;
 
 Memfile_LeaseMgr::Memfile_LeaseMgr(const ParameterMap& parameters)
     : LeaseMgr(parameters) {
-    LOG_WARN(dhcpsrv_logger, DHCPSRV_MEMFILE_WARNING);
+    // Check the universe and use v4 file or v6 file.
+    std::string universe = getParameter("universe");
+    if (universe == "4") {
+        std::string file4 = initLeaseFilePath(V4);
+        if (!file4.empty()) {
+            lease_file4_.reset(new CSVLeaseFile4(file4));
+            lease_file4_->open();
+            load4();
+        }
+    } else {
+        std::string file6 = initLeaseFilePath(V6);
+        if (!file6.empty()) {
+            lease_file6_.reset(new CSVLeaseFile6(file6));
+            lease_file6_->open();
+            load6();
+        }
+    }
+
+    // If lease persistence have been disabled for both v4 and v6,
+    // issue a warning. It is ok not to write leases to disk when
+    // doing testing, but it should not be done in normal server
+    // operation.
+    if (!persistLeases(V4) && !persistLeases(V6)) {
+        LOG_WARN(dhcpsrv_logger, DHCPSRV_MEMFILE_NO_STORAGE);
+    }
 }
 
 Memfile_LeaseMgr::~Memfile_LeaseMgr() {
+    if (lease_file4_) {
+        lease_file4_->close();
+        lease_file4_.reset();
+    }
+    if (lease_file6_) {
+        lease_file6_->close();
+        lease_file6_.reset();
+    }
 }
 
 bool
@@ -37,6 +70,14 @@ Memfile_LeaseMgr::addLease(const Lease4Ptr& lease) {
         // there is a lease with specified address already
         return (false);
     }
+
+    // Try to write a lease to disk first. If this fails, the lease will
+    // not be inserted to the memory and the disk and in-memory data will
+    // remain consistent.
+    if (persistLeases(V4)) {
+        lease_file4_->append(*lease);
+    }
+
     storage4_.insert(lease);
     return (true);
 }
@@ -50,6 +91,14 @@ Memfile_LeaseMgr::addLease(const Lease6Ptr& lease) {
         // there is a lease with specified address already
         return (false);
     }
+
+    // Try to write a lease to disk first. If this fails, the lease will
+    // not be inserted to the memory and the disk and in-memory data will
+    // remain consistent.
+    if (persistLeases(V6)) {
+        lease_file6_->append(*lease);
+    }
+
     storage6_.insert(lease);
     return (true);
 }
@@ -249,6 +298,14 @@ Memfile_LeaseMgr::updateLease4(const Lease4Ptr& lease) {
         isc_throw(NoSuchLease, "failed to update the lease with address "
                   << lease->addr_ << " - no such lease");
     }
+
+    // Try to write a lease to disk first. If this fails, the lease will
+    // not be inserted to the memory and the disk and in-memory data will
+    // remain consistent.
+    if (persistLeases(V4)) {
+        lease_file4_->append(*lease);
+    }
+
     **lease_it = *lease;
 }
 
@@ -262,6 +319,14 @@ Memfile_LeaseMgr::updateLease6(const Lease6Ptr& lease) {
         isc_throw(NoSuchLease, "failed to update the lease with address "
                   << lease->addr_ << " - no such lease");
     }
+
+    // Try to write a lease to disk first. If this fails, the lease will
+    // not be inserted to the memory and the disk and in-memory data will
+    // remain consistent.
+    if (persistLeases(V6)) {
+        lease_file6_->append(*lease);
+    }
+
     **lease_it = *lease;
 }
 
@@ -276,6 +341,15 @@ Memfile_LeaseMgr::deleteLease(const isc::asiolink::IOAddress& addr) {
             // No such lease
             return (false);
         } else {
+            if (persistLeases(V4)) {
+                // Copy the lease. The valid lifetime needs to be modified and
+                // we don't modify the original lease.
+                Lease4 lease_copy = **l;
+                // Setting valid lifetime to 0 means that lease is being
+                // removed.
+                lease_copy.valid_lft_ = 0;
+                lease_file4_->append(lease_copy);
+            }
             storage4_.erase(l);
             return (true);
         }
@@ -287,6 +361,16 @@ Memfile_LeaseMgr::deleteLease(const isc::asiolink::IOAddress& addr) {
             // No such lease
             return (false);
         } else {
+            if (persistLeases(V6)) {
+                // Copy the lease. The lifetimes need to be modified and we
+                // don't modify the original lease.
+                Lease6 lease_copy = **l;
+                // Setting lifetimes to 0 means that lease is being removed.
+                lease_copy.valid_lft_ = 0;
+                lease_copy.preferred_lft_ = 0;
+                lease_file6_->append(lease_copy);
+            }
+
             storage6_.erase(l);
             return (true);
         }
@@ -310,3 +394,187 @@ Memfile_LeaseMgr::rollback() {
     LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
               DHCPSRV_MEMFILE_ROLLBACK);
 }
+
+std::string
+Memfile_LeaseMgr::getDefaultLeaseFilePath(Universe u) const {
+    std::ostringstream s;
+    s << CfgMgr::instance().getDataDir() << "/kea-leases";
+    s << (u == V4 ? "4" : "6");
+    s << ".csv";
+    return (s.str());
+}
+
+std::string
+Memfile_LeaseMgr::getLeaseFilePath(Universe u) const {
+    if (u == V4) {
+        return (lease_file4_ ? lease_file4_->getFilename() : "");
+    }
+
+    return (lease_file6_ ? lease_file6_->getFilename() : "");
+}
+
+bool
+Memfile_LeaseMgr::persistLeases(Universe u) const {
+    // Currently, if the lease file IO is not created, it means that writes to
+    // disk have been explicitly disabled by the administrator. At some point,
+    // there may be a dedicated ON/OFF flag implemented to control this.
+    if (u == V4 && lease_file4_) {
+        return (true);
+    }
+
+    return (u == V6 && lease_file6_);
+}
+
+std::string
+Memfile_LeaseMgr::initLeaseFilePath(Universe u) {
+    std::string persist_val;
+    try {
+        persist_val = getParameter("persist");
+    } catch (const Exception& ex) {
+        // If parameter persist hasn't been specified, we use a default value
+        // 'yes'.
+        persist_val = "true";
+    }
+    // If persist_val is 'false' we will not store leases to disk, so let's
+    // return empty file name.
+    if (persist_val == "false") {
+        return ("");
+
+    } else if (persist_val != "true") {
+        isc_throw(isc::BadValue, "invalid value 'persist="
+                  << persist_val << "'");
+    }
+
+    std::string lease_file;
+    try {
+        lease_file = getParameter("name");
+    } catch (const Exception& ex) {
+        lease_file = getDefaultLeaseFilePath(u);
+    }
+    return (lease_file);
+}
+
+void
+Memfile_LeaseMgr::load4() {
+    // If lease file hasn't been opened, we are working in non-persistent mode.
+    // That's fine, just leave.
+    if (!persistLeases(V4)) {
+        return;
+    }
+
+    LOG_INFO(dhcpsrv_logger, DHCPSRV_MEMFILE_LEASES_RELOAD4)
+        .arg(lease_file4_->getFilename());
+
+    // Remove existing leases (if any). We will recreate them based on the
+    // data on disk.
+    storage4_.clear();
+
+    Lease4Ptr lease;
+    do {
+        /// @todo Currently we stop parsing on first failure. It is possible
+        /// that only one (or a few) leases are bad, so in theory we could
+        /// continue parsing but that would require some error counters to
+        /// prevent endless loops. That is enhancement for later time.
+        if (!lease_file4_->next(lease)) {
+            isc_throw(DbOperationError, "Failed to parse the DHCPv6 lease in"
+                      " the lease file: " << lease_file4_->getReadMsg());
+        }
+        // If we got the lease, we update the internal container holding
+        // leases. Otherwise, we reached the end of file and we leave.
+        if (lease) {
+            LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL_DATA,
+                      DHCPSRV_MEMFILE_LEASE_LOAD4)
+                .arg(lease->toText());
+            loadLease4(lease);
+        }
+    } while (lease);
+}
+
+void
+Memfile_LeaseMgr::loadLease4(Lease4Ptr& lease) {
+    // Check if the lease already exists.
+    Lease4Storage::iterator lease_it = storage4_.find(lease->addr_);
+    // Lease doesn't exist.
+    if (lease_it == storage4_.end()) {
+        // Add the lease only if valid lifetime is greater than 0.
+        // We use valid lifetime of 0 to indicate that lease should
+        // be removed.
+        if (lease->valid_lft_ > 0) {
+           storage4_.insert(lease);
+       }
+    } else {
+        // We use valid lifetime of 0 to indicate that the lease is
+        // to be removed. In such case, erase the lease.
+        if (lease->valid_lft_ == 0) {
+            storage4_.erase(lease_it);
+
+        } else {
+            // Update existing lease.
+            **lease_it = *lease;
+        }
+    }
+}
+
+void
+Memfile_LeaseMgr::load6() {
+    // If lease file hasn't been opened, we are working in non-persistent mode.
+    // That's fine, just leave.
+    if (!persistLeases(V6)) {
+        return;
+    }
+
+    LOG_INFO(dhcpsrv_logger, DHCPSRV_MEMFILE_LEASES_RELOAD6)
+        .arg(lease_file6_->getFilename());
+
+    // Remove existing leases (if any). We will recreate them based on the
+    // data on disk.
+    storage6_.clear();
+
+    Lease6Ptr lease;
+    do {
+        /// @todo Currently we stop parsing on first failure. It is possible
+        /// that only one (or a few) leases are bad, so in theory we could
+        /// continue parsing but that would require some error counters to
+        /// prevent endless loops. That is enhancement for later time.
+        if (!lease_file6_->next(lease)) {
+            isc_throw(DbOperationError, "Failed to parse the DHCPv6 lease in"
+                      " the lease file: " << lease_file6_->getReadMsg());
+        }
+        // If we got the lease, we update the internal container holding
+        // leases. Otherwise, we reached the end of file and we leave.
+        if (lease) {
+            LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL_DATA,
+                      DHCPSRV_MEMFILE_LEASE_LOAD6)
+                .arg(lease->toText());
+
+            loadLease6(lease);
+        }
+    } while (lease);
+}
+
+void
+Memfile_LeaseMgr::loadLease6(Lease6Ptr& lease) {
+    // Check if the lease already exists.
+    Lease6Storage::iterator lease_it = storage6_.find(lease->addr_);
+    // Lease doesn't exist.
+    if (lease_it == storage6_.end()) {
+        // Add the lease only if valid lifetime is greater than 0.
+        // We use valid lifetime of 0 to indicate that lease should
+        // be removed.
+        if (lease->valid_lft_ > 0) {
+            storage6_.insert(lease);
+       }
+    } else {
+        // We use valid lifetime of 0 to indicate that the lease is
+        // to be removed. In such case, erase the lease.
+        if (lease->valid_lft_ == 0) {
+            storage6_.erase(lease_it);
+
+        } else {
+            // Update existing lease.
+            **lease_it = *lease;
+        }
+    }
+
+}
+

+ 152 - 12
src/lib/dhcpsrv/memfile_lease_mgr.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
@@ -16,6 +16,8 @@
 #define MEMFILE_LEASE_MGR_H
 
 #include <dhcp/hwaddr.h>
+#include <dhcpsrv/csv_lease_file4.h>
+#include <dhcpsrv/csv_lease_file6.h>
 #include <dhcpsrv/lease_mgr.h>
 
 #include <boost/multi_index/indexed_by.hpp>
@@ -27,17 +29,59 @@
 namespace isc {
 namespace dhcp {
 
-// This is a concrete implementation of a Lease database.
-//
-// It is for testing purposes only. It is NOT a production code.
-//
-// It does not do anything useful now, and is used for abstract LeaseMgr
-// class testing. It may later evolve into more useful backend if the
-// need arises. We can reuse code from memfile benchmark. See code in
-// tests/tools/dhcp-ubench/memfile_bench.{cc|h}
+/// @brief Concrete implementation of a lease database backend using flat file.
+///
+/// This class implements a lease database backend using CSV files to store
+/// DHCPv4 and DHCPv6 leases on disk. The format of the files is determined
+/// by the @c CSVLeaseFile4 and @c CSVLeaseFile6 classes.
+///
+/// The backend stores leases incrementally, i.e. updates to leases are appended
+/// at the end of the lease file. To record the deletion of a lease, the lease
+/// record is appended to the lease file with the valid lifetime set to 0.
+///
+/// When the backend is starting up, it reads leases from the lease file (one
+/// by one) and adds them to the in-memory container as follows:
+/// - if the lease record being parsed identifies a lease which is not present
+/// in the container, and the lease has valid lifetime greater than 0,
+/// the lease is added to the container,
+/// - if the lease record being parsed identifies a lease which is present in
+/// the container, and the valid lifetime of the lease record being parsed is
+/// greater than 0, the lease in the container is updated
+/// - if the lease record being parsed has valid lifetime equal to 0, and the
+/// corresponding lease exists in the container, the lease is removed from
+/// the container.
+///
+/// After the container holding leases is initialized, each subsequent update,
+/// removal or addition of the lease is appended to the lease file
+/// synchronously.
+///
+/// Originally, the Memfile backend didn't write leases to disk. This was
+/// particularly useful for testing server performance in non-disk bound
+/// conditions. In order to preserve this capability, the new parameter
+/// "persist=true|false" has been introduced in the database access string.
+/// For example, database access string: "type=memfile persist=true"
+/// enables writes of leases to a disk.
+///
+/// The lease file locations can be specified with the "name=[path]"
+/// parameter in the database access string. The [path] is the
+/// absolute path to the file (including file name). If this parameter
+/// is not specified, the default location in the installation
+/// directory is used: var/bind10/kea-leases4.csv and
+/// var/bind10/kea-leases6.csv.
 class Memfile_LeaseMgr : public LeaseMgr {
 public:
 
+    /// @brief Specifies universe (V4, V6)
+    ///
+    /// This enumeration is used by various functions in Memfile Lease Manager,
+    /// to identify the lease type referred to. In particular, it is used by
+    /// functions operating on the lease files to distinguish between lease
+    /// files for DHCPv4 and DHCPv6.
+    enum Universe {
+        V4,
+        V6
+    };
+
     /// @brief The sole lease manager constructor
     ///
     /// dbconfig is a generic way of passing parameters. Parameters
@@ -245,8 +289,97 @@ public:
     /// support transactions, this is a no-op.
     virtual void rollback();
 
+    /// @brief Returns default path to the lease file.
+    ///
+    /// @param u Universe (V4 or V6).
+    std::string getDefaultLeaseFilePath(Universe u) const;
+
+    /// @brief Returns an absolute path to the lease file.
+    ///
+    /// @param u Universe (V4 or V6).
+    ///
+    /// @return Absolute path to the lease file or empty string if no lease
+    /// file is used.
+    std::string getLeaseFilePath(Universe u) const;
+
+    /// @brief Specifies whether or not leases are written to disk.
+    ///
+    /// It is possible that leases for DHCPv4 are written to disk whereas leases
+    /// for DHCPv6 are not; or vice versa. The argument of the method specifies
+    /// the type of lease in that respect.
+    ///
+    /// @param u Universe (V4 or V6).
+    ///
+    /// @return true if leases are written to lease file; if false is
+    /// returned, leases will be held in memory and will be lost upon
+    /// server shut down.
+    bool persistLeases(Universe u) const;
+
 protected:
 
+    /// @brief Load all DHCPv4 leases from the file.
+    ///
+    /// This method loads all DHCPv4 leases from a file to memory. It removes
+    /// existing leases before reading a file.
+    ///
+    /// @throw isc::DbOperationError If failed to read a lease from the lease
+    /// file.
+    void load4();
+
+    /// @brief Loads a single DHCPv4 lease from the file.
+    ///
+    /// This method reads a single lease record from the lease file. If the
+    /// corresponding record doesn't exist in the in-memory container, the
+    /// lease is added to the container (except for a lease which valid lifetime
+    /// is 0). If the corresponding lease exists, the lease being read updates
+    /// the existing lease. If the lease being read from the lease file has
+    /// valid lifetime of 0 and the corresponding lease exists in the in-memory
+    /// database, the existing lease is removed.
+    ///
+    /// @param lease Pointer to the lease read from the lease file.
+    void loadLease4(Lease4Ptr& lease);
+
+    /// @brief Load all DHCPv6 leases from the file.
+    ///
+    /// This method loads all DHCPv6 leases from a file to memory. It removes
+    /// existing leases before reading a file.
+    ///
+    /// @throw isc::DbOperationError If failed to read a lease from the lease
+    /// file.
+    void load6();
+
+    /// @brief Loads a single DHCPv6 lease from the file.
+    ///
+    /// This method reads a single lease record from the lease file. If the
+    /// corresponding record doesn't exist in the in-memory container, the
+    /// lease is added to the container (except for a lease which valid lifetime
+    /// is 0). If the corresponding lease exists, the lease being read updates
+    /// the existing lease. If the lease being read from the lease file has
+    /// valid lifetime of 0 and the corresponding lease exists in the in-memory
+    /// database, the existing lease is removed.
+    ///
+    /// @param lease Pointer to the lease read from the lease file.
+    void loadLease6(Lease6Ptr& lease);
+
+    /// @brief Initialize the location of the lease file.
+    ///
+    /// This method uses the parameters passed as a map to the constructor to
+    /// initialize the location of the lease file. If the lease file is not
+    /// specified, the method will use the default location for the universe
+    /// (v4 or v6) selected. If the location is specified in the map as empty
+    /// or the "persist" parameter is set to "no" it will set the empty
+    /// location, which implies that leases belonging to the specified universe
+    /// will not be written to disk.
+    ///
+    /// @param u Universe (v4 or v6)
+    /// @param parameters Map holding parameters of the Lease Manager, passed to
+    /// the constructor.
+    ///
+    /// @return The location of the lease file that should be assigned to the
+    /// lease_file4_ or lease_file6_, depending on the universe specified as an
+    /// argument to this function.
+    std::string initLeaseFilePath(Universe u);
+
     // This is a multi-index container, which holds elements that can
     // be accessed using different search indexes.
     typedef boost::multi_index_container<
@@ -283,7 +416,7 @@ protected:
     // be accessed using different search indexes.
     typedef boost::multi_index_container<
         // It holds pointers to Lease4 objects.
-        Lease4Ptr, 
+        Lease4Ptr,
         // Specification of search indexes starts here.
         boost::multi_index::indexed_by<
             // Specification of the first index starts here.
@@ -314,7 +447,7 @@ protected:
             >,
 
             // Specification of the third index starts here.
-            boost::multi_index::ordered_unique<
+            boost::multi_index::ordered_non_unique<
                 // This is a composite index that uses two values to search for a
                 // lease: client id and subnet id.
                 boost::multi_index::composite_key<
@@ -329,7 +462,7 @@ protected:
             >,
 
             // Specification of the fourth index starts here.
-            boost::multi_index::ordered_unique<
+            boost::multi_index::ordered_non_unique<
                 // This is a composite index that uses two values to search for a
                 // lease: client id and subnet id.
                 boost::multi_index::composite_key<
@@ -354,6 +487,13 @@ protected:
 
     /// @brief stores IPv6 leases
     Lease6Storage storage6_;
+
+    /// @brief Holds the pointer to the DHCPv4 lease file IO.
+    boost::shared_ptr<CSVLeaseFile4> lease_file4_;
+
+    /// @brief Holds the pointer to the DHCPv6 lease file IO.
+    boost::shared_ptr<CSVLeaseFile6> lease_file6_;
+
 };
 
 }; // end of isc::dhcp namespace

+ 5 - 1
src/lib/dhcpsrv/tests/Makefile.am

@@ -2,7 +2,8 @@ SUBDIRS = .
 
 AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
 AM_CPPFLAGS += $(BOOST_INCLUDES)
-AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/lib/dhcp/tests\"
+AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/lib/dhcpsrv/tests\"
+AM_CPPFLAGS += -DDHCP_DATA_DIR=\"$(abs_top_builddir)/src/lib/dhcpsrv/tests\"
 AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
 
 AM_CXXFLAGS = $(B10_CXXFLAGS)
@@ -55,9 +56,12 @@ libdhcpsrv_unittests_SOURCES += addr_utilities_unittest.cc
 libdhcpsrv_unittests_SOURCES += alloc_engine_unittest.cc
 libdhcpsrv_unittests_SOURCES += callout_handle_store_unittest.cc
 libdhcpsrv_unittests_SOURCES += cfgmgr_unittest.cc
+libdhcpsrv_unittests_SOURCES += csv_lease_file4_unittest.cc
+libdhcpsrv_unittests_SOURCES += csv_lease_file6_unittest.cc
 libdhcpsrv_unittests_SOURCES += d2_client_unittest.cc
 libdhcpsrv_unittests_SOURCES += d2_udp_unittest.cc
 libdhcpsrv_unittests_SOURCES += dbaccess_parser_unittest.cc
+libdhcpsrv_unittests_SOURCES += lease_file_io.cc lease_file_io.h
 libdhcpsrv_unittests_SOURCES += lease_unittest.cc
 libdhcpsrv_unittests_SOURCES += lease_mgr_factory_unittest.cc
 libdhcpsrv_unittests_SOURCES += lease_mgr_unittest.cc

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

@@ -101,7 +101,7 @@ public:
 
         initFqdn("", false, false);
 
-        factory_.create("type=memfile");
+        factory_.create("type=memfile universe=6 persist=false");
     }
 
     /// @brief Configures a subnet and adds one pool to it.
@@ -424,7 +424,7 @@ public:
         subnet_->addPool(pool_);
         cfg_mgr.addSubnet4(subnet_);
 
-        factory_.create("type=memfile");
+        factory_.create("type=memfile universe=4 persist=false");
     }
 
     /// @brief checks if Lease4 matches expected configuration

+ 182 - 0
src/lib/dhcpsrv/tests/csv_lease_file4_unittest.cc

@@ -0,0 +1,182 @@
+// 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 <asiolink/io_address.h>
+#include <dhcp/duid.h>
+#include <dhcpsrv/csv_lease_file4.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/tests/lease_file_io.h>
+#include <boost/scoped_ptr.hpp>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+#include <sstream>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::util;
+
+namespace {
+
+// HWADDR values used by unit tests.
+const uint8_t HWADDR0[] = { 0, 1, 2, 3, 4, 5 };
+const uint8_t HWADDR1[] = { 0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf };
+
+const uint8_t CLIENTID0[] = { 1, 2, 3, 4 };
+const uint8_t CLIENTID1[] = { 0xa, 0xb, 0xc, 0xd };
+
+/// @brief Test fixture class for @c CSVLeaseFile4 validation.
+class CSVLeaseFile4Test : public ::testing::Test {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Initializes IO for lease file used by unit tests.
+    CSVLeaseFile4Test();
+
+    /// @brief Prepends the absolute path to the file specified
+    /// as an argument.
+    ///
+    /// @param filename Name of the file.
+    /// @return Absolute path to the test file.
+    static std::string absolutePath(const std::string& filename);
+
+    /// @brief Creates the lease file to be parsed by unit tests.
+    void writeSampleFile() const;
+
+    /// @brief Name of the test lease file.
+    std::string filename_;
+
+    /// @brief Object providing access to lease file IO.
+    LeaseFileIO io_;
+
+};
+
+CSVLeaseFile4Test::CSVLeaseFile4Test()
+    : filename_(absolutePath("leases4.csv")), io_(filename_) {
+}
+
+std::string
+CSVLeaseFile4Test::absolutePath(const std::string& filename) {
+    std::ostringstream s;
+    s << DHCP_DATA_DIR << "/" << filename;
+    return (s.str());
+}
+
+void
+CSVLeaseFile4Test::writeSampleFile() const {
+    io_.writeFile("address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+                  "fqdn_fwd,fqdn_rev,hostname\n"
+                  "192.0.2.1,06:07:08:09:0a:bc,,200,200,8,1,1,"
+                  "host.example.com\n"
+                  "192.0.2.1,,a:11:01:04,200,200,8,1,1,host.example.com\n"
+                  "192.0.3.15,dd:de:ba:0d:1b:2e:3e:4f,0a:00:01:04,100,100,7,"
+                  "0,0,\n");
+}
+
+// This test checks the capability to read and parse leases from the file.
+TEST_F(CSVLeaseFile4Test, parse) {
+    // Create a file to be parsed.
+    writeSampleFile();
+
+    // Open the lease file.
+    boost::scoped_ptr<CSVLeaseFile4> lf(new CSVLeaseFile4(filename_));
+    ASSERT_NO_THROW(lf->open());
+
+    Lease4Ptr lease;
+    // Reading first read should be successful.
+    EXPECT_TRUE(lf->next(lease));
+    ASSERT_TRUE(lease);
+
+    // Verify that the lease attributes are correct.
+    EXPECT_EQ("192.0.2.1", lease->addr_.toText());
+    HWAddr hwaddr1(lease->hwaddr_, HTYPE_ETHER);
+    EXPECT_EQ("06:07:08:09:0a:bc", hwaddr1.toText(false));
+    EXPECT_FALSE(lease->client_id_);
+    EXPECT_EQ(200, lease->valid_lft_);
+    EXPECT_EQ(0, lease->cltt_);
+    EXPECT_EQ(8, lease->subnet_id_);
+    EXPECT_TRUE(lease->fqdn_fwd_);
+    EXPECT_TRUE(lease->fqdn_rev_);
+    EXPECT_EQ("host.example.com", lease->hostname_);
+
+    // Second lease is malformed - HW address is empty.
+    EXPECT_FALSE(lf->next(lease));
+
+    // Even though parsing previous lease failed, reading the next lease should be
+    // successful.
+    EXPECT_TRUE(lf->next(lease));
+    ASSERT_TRUE(lease);
+    // Verify that the third lease is correct.
+    EXPECT_EQ("192.0.3.15", lease->addr_.toText());
+    HWAddr hwaddr3(lease->hwaddr_, HTYPE_ETHER);
+    EXPECT_EQ("dd:de:ba:0d:1b:2e:3e:4f", hwaddr3.toText(false));
+    ASSERT_TRUE(lease->client_id_);
+    EXPECT_EQ("0a:00:01:04", lease->client_id_->toText());
+    EXPECT_EQ(100, lease->valid_lft_);
+    EXPECT_EQ(0, lease->cltt_);
+    EXPECT_EQ(7, lease->subnet_id_);
+    EXPECT_FALSE(lease->fqdn_fwd_);
+    EXPECT_FALSE(lease->fqdn_rev_);
+    EXPECT_TRUE(lease->hostname_.empty());
+
+    // There are no more leases. Reading should cause no error, but the returned
+    // lease pointer should be NULL.
+    EXPECT_TRUE(lf->next(lease));
+    EXPECT_FALSE(lease);
+
+    // We should be able to do it again.
+    EXPECT_TRUE(lf->next(lease));
+    EXPECT_FALSE(lease);
+
+}
+
+// This test checks creation of the lease file and writing leases.
+TEST_F(CSVLeaseFile4Test, recreate) {
+    boost::scoped_ptr<CSVLeaseFile4> lf(new CSVLeaseFile4(filename_));
+    ASSERT_NO_THROW(lf->recreate());
+    ASSERT_TRUE(io_.exists());
+    // Create first lease, with NULL client id.
+    Lease4Ptr lease(new Lease4(IOAddress("192.0.3.2"),
+                               HWADDR0, sizeof(HWADDR0),
+                               NULL, 0,
+                               200, 50, 80, 0, 8, true, true,
+                               "host.example.com"));
+    ASSERT_NO_THROW(lf->append(*lease));
+    // Create second lease, with non-NULL client id.
+    lease.reset(new Lease4(IOAddress("192.0.3.10"),
+                           HWADDR1, sizeof(HWADDR1),
+                           CLIENTID0, sizeof(CLIENTID0),
+                           100, 60, 90, 0, 7));
+    ASSERT_NO_THROW(lf->append(*lease));
+    // Close the lease file.
+    lf->close();
+    // Check that the contents of the csv file are correct.
+    EXPECT_EQ("address,hwaddr,client_id,valid_lifetime,expire,subnet_id,"
+              "fqdn_fwd,fqdn_rev,hostname\n"
+              "192.0.3.2,00:01:02:03:04:05,,200,200,8,1,1,host.example.com\n"
+              "192.0.3.10,0d:0e:0a:0d:0b:0e:0e:0f,01:02:03:04,100,100,7,0,"
+              "0,\n",
+              io_.readFile());
+}
+
+/// @todo Currently we don't check invalid lease attributes, such as invalid
+/// lease type, invalid preferred lifetime vs valid lifetime etc. The Lease6
+/// should be extended with the function that validates lease attributes. Once
+/// this is implemented we should provide more tests for malformed leases
+/// in the CSV file. See http://bind10.isc.org/ticket/2405.
+
+} // end of anonymous namespace

+ 223 - 0
src/lib/dhcpsrv/tests/csv_lease_file6_unittest.cc

@@ -0,0 +1,223 @@
+// 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 <asiolink/io_address.h>
+#include <dhcp/duid.h>
+#include <dhcpsrv/csv_lease_file6.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/tests/lease_file_io.h>
+#include <boost/scoped_ptr.hpp>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+#include <sstream>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::util;
+
+namespace {
+
+// DUID values used by unit tests.
+const uint8_t DUID0[] = { 0, 1, 2, 3, 4, 5, 6, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf };
+const uint8_t DUID1[] = { 1, 1, 1, 1, 0xa, 1, 2, 3, 4, 5 };
+
+/// @brief Test fixture class for @c CSVLeaseFile6 validation.
+class CSVLeaseFile6Test : public ::testing::Test {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Initializes IO for lease file used by unit tests.
+    CSVLeaseFile6Test();
+
+    /// @brief Prepends the absolute path to the file specified
+    /// as an argument.
+    ///
+    /// @param filename Name of the file.
+    /// @return Absolute path to the test file.
+    static std::string absolutePath(const std::string& filename);
+
+    /// @brief Create DUID object from the binary.
+    ///
+    /// @param duid Binary value representing a DUID.
+    /// @param size Size of the DUID.
+    /// @return Pointer to the @c DUID object.
+    DuidPtr makeDUID(const uint8_t* duid, const unsigned int size) const {
+        return (DuidPtr(new DUID(duid, size)));
+    }
+
+    /// @brief Create lease file that can be parsed by unit tests.
+    void writeSampleFile() const;
+
+    /// @brief Name of the test lease file.
+    std::string filename_;
+
+    /// @brief Object providing access to lease file IO.
+    LeaseFileIO io_;
+
+};
+
+CSVLeaseFile6Test::CSVLeaseFile6Test()
+    : filename_(absolutePath("leases6.csv")), io_(filename_) {
+}
+
+std::string
+CSVLeaseFile6Test::absolutePath(const std::string& filename) {
+    std::ostringstream s;
+    s << DHCP_DATA_DIR << "/" << filename;
+    return (s.str());
+}
+
+void
+CSVLeaseFile6Test::writeSampleFile() const {
+    io_.writeFile("address,duid,valid_lifetime,expire,subnet_id,"
+                  "pref_lifetime,lease_type,iaid,prefix_len,fqdn_fwd,"
+                  "fqdn_rev,hostname\n"
+                  "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,"
+                  "200,200,8,100,0,7,0,1,1,host.example.com\n"
+                  "2001:db8:1::1,,200,200,8,100,0,7,0,1,1,host.example.com\n"
+                  "2001:db8:2::10,01:01:01:01:0a:01:02:03:04:05,300,300,6,150,"
+                  "0,8,0,0,0,\n"
+                  "3000:1::,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,0,200,8,0,2,"
+                  "16,64,0,0,\n");
+}
+
+// This test checks the capability to read and parse leases from the file.
+TEST_F(CSVLeaseFile6Test, parse) {
+    // Create a file to be parsed.
+    writeSampleFile();
+
+    // Open the lease file.
+    boost::scoped_ptr<CSVLeaseFile6> lf(new CSVLeaseFile6(filename_));
+    ASSERT_NO_THROW(lf->open());
+
+    Lease6Ptr lease;
+    // Reading first read should be successful.
+    EXPECT_TRUE(lf->next(lease));
+    ASSERT_TRUE(lease);
+
+    // Verify that the lease attributes are correct.
+    EXPECT_EQ("2001:db8:1::1", lease->addr_.toText());
+    ASSERT_TRUE(lease->duid_);
+    EXPECT_EQ("00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f", lease->duid_->toText());
+    EXPECT_EQ(200, lease->valid_lft_);
+    EXPECT_EQ(0, lease->cltt_);
+    EXPECT_EQ(8, lease->subnet_id_);
+    EXPECT_EQ(100, lease->preferred_lft_);
+    EXPECT_EQ(Lease::TYPE_NA, lease->type_);
+    EXPECT_EQ(7, lease->iaid_);
+    EXPECT_EQ(0, lease->prefixlen_);
+    EXPECT_TRUE(lease->fqdn_fwd_);
+    EXPECT_TRUE(lease->fqdn_rev_);
+    EXPECT_EQ("host.example.com", lease->hostname_);
+
+    // Second lease is malformed - DUID is empty.
+    EXPECT_FALSE(lf->next(lease));
+
+    // Even, parsing previous lease failed, reading the next lease should be
+    // successful.
+    EXPECT_TRUE(lf->next(lease));
+    ASSERT_TRUE(lease);
+    // Verify that the third lease is correct.
+    EXPECT_EQ("2001:db8:2::10", lease->addr_.toText());
+    ASSERT_TRUE(lease->duid_);
+    EXPECT_EQ("01:01:01:01:0a:01:02:03:04:05", lease->duid_->toText());
+    EXPECT_EQ(300, lease->valid_lft_);
+    EXPECT_EQ(0, lease->cltt_);
+    EXPECT_EQ(6, lease->subnet_id_);
+    EXPECT_EQ(150, lease->preferred_lft_);
+    EXPECT_EQ(Lease::TYPE_NA, lease->type_);
+    EXPECT_EQ(8, lease->iaid_);
+    EXPECT_EQ(0, lease->prefixlen_);
+    EXPECT_FALSE(lease->fqdn_fwd_);
+    EXPECT_FALSE(lease->fqdn_rev_);
+    EXPECT_TRUE(lease->hostname_.empty());
+
+    // Reading the fourth lease should be successful.
+    EXPECT_TRUE(lf->next(lease));
+    ASSERT_TRUE(lease);
+    // Verify that the lease is correct.
+    EXPECT_EQ("3000:1::", lease->addr_.toText());
+    ASSERT_TRUE(lease->duid_);
+    EXPECT_EQ("00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f", lease->duid_->toText());
+    EXPECT_EQ(0, lease->valid_lft_);
+    EXPECT_EQ(200, lease->cltt_);
+    EXPECT_EQ(8, lease->subnet_id_);
+    EXPECT_EQ(0, lease->preferred_lft_);
+    EXPECT_EQ(Lease::TYPE_PD, lease->type_);
+    EXPECT_EQ(16, lease->iaid_);
+    EXPECT_EQ(64, lease->prefixlen_);
+    EXPECT_FALSE(lease->fqdn_fwd_);
+    EXPECT_FALSE(lease->fqdn_rev_);
+    EXPECT_TRUE(lease->hostname_.empty());
+
+    // There are no more leases. Reading should cause no error, but the returned
+    // lease pointer should be NULL.
+    EXPECT_TRUE(lf->next(lease));
+    EXPECT_FALSE(lease);
+
+    // We should be able to do it again.
+    EXPECT_TRUE(lf->next(lease));
+    EXPECT_FALSE(lease);
+
+}
+
+// This test checks creation of the lease file and writing leases.
+TEST_F(CSVLeaseFile6Test, recreate) {
+    boost::scoped_ptr<CSVLeaseFile6> lf(new CSVLeaseFile6(filename_));
+    ASSERT_NO_THROW(lf->recreate());
+    ASSERT_TRUE(io_.exists());
+
+    Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+                               makeDUID(DUID0, sizeof(DUID0)),
+                               7, 100, 200, 50, 80, 8, true, true,
+                               "host.example.com"));
+    lease->cltt_ = 0;
+    ASSERT_NO_THROW(lf->append(*lease));
+
+    lease.reset(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:2::10"),
+                           makeDUID(DUID1, sizeof(DUID1)),
+                           8, 150, 300, 40, 70, 6, false, false,
+                           "", 128));
+    lease->cltt_ = 0;
+    ASSERT_NO_THROW(lf->append(*lease));
+
+    lease.reset(new Lease6(Lease::TYPE_PD, IOAddress("3000:1:1::"),
+                           makeDUID(DUID0, sizeof(DUID0)),
+                           7, 150, 300, 40, 70, 10, false, false,
+                           "", 64));
+    lease->cltt_ = 0;
+    ASSERT_NO_THROW(lf->append(*lease));
+
+    EXPECT_EQ("address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,"
+              "lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname\n"
+              "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,"
+              "200,200,8,100,0,7,0,1,1,host.example.com\n"
+              "2001:db8:2::10,01:01:01:01:0a:01:02:03:04:05"
+              ",300,300,6,150,0,8,128,0,0,\n"
+              "3000:1:1::,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,"
+              "300,300,10,150,2,7,64,0,0,\n",
+              io_.readFile());
+}
+
+/// @todo Currently we don't check invalid lease attributes, such as invalid
+/// lease type, invalid preferred lifetime vs valid lifetime etc. The Lease6
+/// should be extended with the function that validates lease attributes. Once
+/// this is implemented we should provide more tests for malformed leases
+/// in the CSV file. See http://bind10.isc.org/ticket/2405.
+
+} // end of anonymous namespace

+ 70 - 20
src/lib/dhcpsrv/tests/dbaccess_parser_unittest.cc

@@ -15,6 +15,7 @@
 #include <config.h>
 
 #include <dhcpsrv/dbaccess_parser.h>
+#include <dhcpsrv/dhcp_parsers.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <config/ccsession.h>
 #include <gtest/gtest.h>
@@ -86,8 +87,14 @@ public:
             }
 
             // Add the keyword and value - make sure that they are quoted.
-            result += quote + keyval[i] + quote + colon + space +
-                      quote + keyval[i + 1] + quote;
+            // The only parameter which is not quoted is persist as it
+            // is a boolean value.
+            result += quote + keyval[i] + quote + colon + space;
+            if (std::string(keyval[i]) != "persist") {
+                result += quote + keyval[i + 1] + quote;
+            } else {
+                result += keyval[i + 1];
+            }
         }
 
         // Add the terminating brace
@@ -114,14 +121,18 @@ public:
     /// @param dbaccess set of database access parameters to check
     /// @param keyval Array of "const char*" strings in the order keyword,
     ///        value, keyword, value ...  A NULL entry terminates the list.
+    /// @param u Universe (V4 or V6).
     void checkAccessString(const char* trace_string,
                            const DbAccessParser::StringPairMap& parameters,
-                           const char* keyval[]) {
+                           const char* keyval[],
+                           Option::Universe u = Option::V4) {
         SCOPED_TRACE(trace_string);
 
         // Construct a map of keyword value pairs.
         std::map<string, string> expected;
-        size_t expected_count = 0;
+        expected["universe"] = (u == Option::V4 ? "4" : "6");
+        // The universe is always injected by the parser itself.
+        size_t expected_count = 1;
         for (size_t i = 0; keyval[i] != NULL; i += 2) {
             // Get the value.  This should not be NULL
             ASSERT_NE(static_cast<const char*>(NULL), keyval[i + 1]) <<
@@ -134,7 +145,7 @@ public:
         }
 
         // Check no duplicates in the test set of reference keywords.
-        ASSERT_EQ(expected_count, expected.size()) << 
+        ASSERT_EQ(expected_count, expected.size()) <<
             "Supplied reference keyword/value list contains duplicate keywords";
 
         // The passed parameter map should have the same number of entries as
@@ -169,8 +180,8 @@ public:
     /// @brief Constructor
     ///
     /// @brief Keyword/value collection of ddatabase access parameters
-    TestDbAccessParser(const std::string& param_name)
-        : DbAccessParser(param_name)
+    TestDbAccessParser(const std::string& param_name, const ParserContext& ctx)
+        : DbAccessParser(param_name, ctx)
     {}
 
     /// @brief Destructor
@@ -211,7 +222,7 @@ TEST_F(DbAccessParserTest, validTypeMemfile) {
     ConstElementPtr json_elements = Element::fromJSON(json_config);
     EXPECT_TRUE(json_elements);
 
-    TestDbAccessParser parser("lease-database");
+    TestDbAccessParser parser("lease-database", ParserContext(Option::V4));
     EXPECT_NO_THROW(parser.build(json_elements));
     checkAccessString("Valid memfile", parser.getDbAccessParameters(), config);
 }
@@ -227,11 +238,49 @@ TEST_F(DbAccessParserTest, emptyKeyword) {
     ConstElementPtr json_elements = Element::fromJSON(json_config);
     EXPECT_TRUE(json_elements);
 
-    TestDbAccessParser parser("lease-database");
+    TestDbAccessParser parser("lease-database", ParserContext(Option::V4));
     EXPECT_NO_THROW(parser.build(json_elements));
     checkAccessString("Valid memfile", parser.getDbAccessParameters(), config);
 }
 
+// Check that the parser works with more complex configuration when
+// lease file path is specified for DHCPv4.
+TEST_F(DbAccessParserTest, persistV4Memfile) {
+    const char* config[] = {"type", "memfile",
+                            "persist", "true",
+                            "name", "/opt/bind10/var/kea-leases4.csv",
+                            NULL};
+
+    string json_config = toJson(config);
+    ConstElementPtr json_elements = Element::fromJSON(json_config);
+    EXPECT_TRUE(json_elements);
+
+    TestDbAccessParser parser("lease-database", ParserContext(Option::V4));
+    EXPECT_NO_THROW(parser.build(json_elements));
+
+    checkAccessString("Valid memfile", parser.getDbAccessParameters(),
+                      config);
+}
+
+// Check that the parser works with more complex configuration when
+// lease file path is specified for DHCPv6.
+TEST_F(DbAccessParserTest, persistV6Memfile) {
+    const char* config[] = {"type", "memfile",
+                            "persist", "true",
+                            "name", "/opt/bind10/var/kea-leases6.csv",
+                            NULL};
+
+    string json_config = toJson(config);
+    ConstElementPtr json_elements = Element::fromJSON(json_config);
+    EXPECT_TRUE(json_elements);
+
+    TestDbAccessParser parser("lease-database", ParserContext(Option::V6));
+    EXPECT_NO_THROW(parser.build(json_elements));
+
+    checkAccessString("Valid memfile", parser.getDbAccessParameters(),
+                      config, Option::V6);
+}
+
 // Check that the parser works with a valid MySQL configuration
 TEST_F(DbAccessParserTest, validTypeMysql) {
     const char* config[] = {"type",     "mysql",
@@ -245,7 +294,7 @@ TEST_F(DbAccessParserTest, validTypeMysql) {
     ConstElementPtr json_elements = Element::fromJSON(json_config);
     EXPECT_TRUE(json_elements);
 
-    TestDbAccessParser parser("lease-database");
+    TestDbAccessParser parser("lease-database", ParserContext(Option::V4));
     EXPECT_NO_THROW(parser.build(json_elements));
     checkAccessString("Valid mysql", parser.getDbAccessParameters(), config);
 }
@@ -262,7 +311,7 @@ TEST_F(DbAccessParserTest, missingTypeKeyword) {
     ConstElementPtr json_elements = Element::fromJSON(json_config);
     EXPECT_TRUE(json_elements);
 
-    TestDbAccessParser parser("lease-database");
+    TestDbAccessParser parser("lease-database", ParserContext(Option::V4));
     EXPECT_THROW(parser.build(json_elements), TypeKeywordMissing);
 }
 
@@ -271,7 +320,8 @@ TEST_F(DbAccessParserTest, factory) {
 
     // Check that the parser is built through the factory.
     boost::scoped_ptr<DhcpConfigParser> parser(
-        DbAccessParser::factory("lease-database"));
+        DbAccessParser::factory("lease-database", ParserContext(Option::V4))
+    );
     EXPECT_TRUE(parser);
     DbAccessParser* dbap = dynamic_cast<DbAccessParser*>(parser.get());
     EXPECT_NE(static_cast<DbAccessParser*>(NULL), dbap);
@@ -321,7 +371,7 @@ TEST_F(DbAccessParserTest, incrementalChanges) {
                              "name",     "keatest",
                              NULL};
 
-    TestDbAccessParser parser("lease-database");
+    TestDbAccessParser parser("lease-database", ParserContext(Option::V4));
 
     // First configuration string should cause a representation of that string
     // to be held.
@@ -385,7 +435,7 @@ TEST_F(DbAccessParserTest, getDbAccessString) {
     ConstElementPtr json_elements = Element::fromJSON(json_config);
     EXPECT_TRUE(json_elements);
 
-    TestDbAccessParser parser("lease-database");
+    TestDbAccessParser parser("lease-database", ParserContext(Option::V4));
     EXPECT_NO_THROW(parser.build(json_elements));
 
     // Get the database access string
@@ -394,8 +444,7 @@ TEST_F(DbAccessParserTest, getDbAccessString) {
     // String should be either "type=mysql name=keatest" or
     // "name=keatest type=mysql". The "host" entry is null, so should not be
     // output.
-    EXPECT_TRUE((dbaccess == "type=mysql name=keatest") ||
-                (dbaccess == "name=keatest type=mysql"));
+    EXPECT_EQ(dbaccess, "name=keatest type=mysql universe=4");
 }
 
 // Check that the "commit" function actually opens the database.  We will
@@ -410,17 +459,18 @@ TEST_F(DbAccessParserTest, commit) {
             }, isc::dhcp::NoLeaseManager);
 
     // Set up the parser to open the memfile database.
-    const char* config[] = {"type", "memfile",
-                            NULL};
+    const char* config[] = {"type", "memfile", "persist", "false", NULL};
     string json_config = toJson(config);
+
     ConstElementPtr json_elements = Element::fromJSON(json_config);
     EXPECT_TRUE(json_elements);
 
-    TestDbAccessParser parser("lease-database");
+    TestDbAccessParser parser("lease-database", ParserContext(Option::V4));
     EXPECT_NO_THROW(parser.build(json_elements));
 
     // Ensure that the access string is as expected.
-    EXPECT_EQ(std::string("type=memfile"), parser.getDbAccessString());
+    EXPECT_EQ("persist=false type=memfile universe=4",
+              parser.getDbAccessString());
 
     // Committal of the parser changes should open the database.
     EXPECT_NO_THROW(parser.commit());

+ 149 - 3
src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.cc

@@ -673,7 +673,13 @@ GenericLeaseMgrTest::testAddGetDelete6(bool check_t1_t2) {
 
     // after the lease is deleted, it should really be gone
     x = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::456"));
-    EXPECT_EQ(Lease6Ptr(), x);
+    EXPECT_FALSE(x);
+
+    // Reopen the lease storage to make sure that lease is gone from the
+    // persistent storage.
+    reopen(V6);
+    x = lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::456"));
+    EXPECT_FALSE(x);
 }
 
 void
@@ -689,7 +695,7 @@ GenericLeaseMgrTest::testBasicLease4() {
     lmptr_->commit();
 
     // Reopen the database to ensure that they actually got stored.
-    reopen();
+    reopen(V4);
 
     Lease4Ptr l_returned = lmptr_->getLease4(ioaddress4_[1]);
     ASSERT_TRUE(l_returned);
@@ -717,6 +723,39 @@ GenericLeaseMgrTest::testBasicLease4() {
     l_returned = lmptr_->getLease4(ioaddress4_[2]);
     ASSERT_TRUE(l_returned);
     detailCompareLease(leases[2], l_returned);
+
+    reopen(V4);
+
+    // The deleted lease should be still gone after we re-read leases from
+    // persistent storage.
+    l_returned = lmptr_->getLease4(ioaddress4_[1]);
+    EXPECT_FALSE(l_returned);
+
+    l_returned = lmptr_->getLease4(ioaddress4_[2]);
+    ASSERT_TRUE(l_returned);
+    detailCompareLease(leases[2], l_returned);
+
+    l_returned = lmptr_->getLease4(ioaddress4_[3]);
+    ASSERT_TRUE(l_returned);
+    detailCompareLease(leases[3], l_returned);
+
+    // Update some FQDN data, so as we can check that update in
+    // persistent storage works as expected.
+    leases[2]->hostname_ = "memfile.example.com.";
+    leases[2]->fqdn_rev_ = true;
+
+    ASSERT_NO_THROW(lmptr_->updateLease4(leases[2]));
+
+    reopen(V4);
+
+    // The lease should be now updated in the storage.
+    l_returned = lmptr_->getLease4(ioaddress4_[2]);
+    ASSERT_TRUE(l_returned);
+    detailCompareLease(leases[2], l_returned);
+
+    l_returned = lmptr_->getLease4(ioaddress4_[3]);
+    ASSERT_TRUE(l_returned);
+    detailCompareLease(leases[3], l_returned);
 }
 
 
@@ -733,7 +772,7 @@ GenericLeaseMgrTest::testBasicLease6() {
     lmptr_->commit();
 
     // Reopen the database to ensure that they actually got stored.
-    reopen();
+    reopen(V6);
 
     Lease6Ptr l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
     ASSERT_TRUE(l_returned);
@@ -761,6 +800,32 @@ GenericLeaseMgrTest::testBasicLease6() {
     l_returned = lmptr_->getLease6(leasetype6_[2], ioaddress6_[2]);
     ASSERT_TRUE(l_returned);
     detailCompareLease(leases[2], l_returned);
+
+    reopen(V6);
+
+    // The deleted lease should be still gone after we re-read leases from
+    // persistent storage.
+    l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
+    EXPECT_FALSE(l_returned);
+
+    // Check that the second address is still there.
+    l_returned = lmptr_->getLease6(leasetype6_[2], ioaddress6_[2]);
+    ASSERT_TRUE(l_returned);
+    detailCompareLease(leases[2], l_returned);
+
+    // Update some FQDN data, so as we can check that update in
+    // persistent storage works as expected.
+    leases[2]->hostname_ = "memfile.example.com.";
+    leases[2]->fqdn_rev_ = true;
+
+    ASSERT_NO_THROW(lmptr_->updateLease6(leases[2]));
+
+    reopen(V6);
+
+    // The lease should be now updated in the storage.
+    l_returned = lmptr_->getLease6(leasetype6_[2], ioaddress6_[2]);
+    ASSERT_TRUE(l_returned);
+    detailCompareLease(leases[2], l_returned);
 }
 
 void
@@ -1361,6 +1426,87 @@ GenericLeaseMgrTest::testUpdateLease6() {
     EXPECT_THROW(lmptr_->updateLease6(leases[2]), isc::dhcp::NoSuchLease);
 }
 
+void
+GenericLeaseMgrTest::testRecreateLease4() {
+    // Create a lease.
+    std::vector<Lease4Ptr> leases = createLeases4();
+    // Copy the lease so as we can freely modify it.
+    Lease4Ptr lease(new Lease4(*leases[0]));
+
+    // Add a lease.
+    EXPECT_TRUE(lmptr_->addLease(lease));
+    lmptr_->commit();
+
+    // Check that the lease has been successfuly added.
+    Lease4Ptr l_returned = lmptr_->getLease4(ioaddress4_[0]);
+    ASSERT_TRUE(l_returned);
+    detailCompareLease(lease, l_returned);
+
+    // Delete a lease, check that it's gone.
+    EXPECT_TRUE(lmptr_->deleteLease(ioaddress4_[0]));
+    EXPECT_FALSE(lmptr_->getLease4(ioaddress4_[0]));
+
+    // Modify the copy of the lease. Increasing values or negating them ensures
+    // that they are really modified, because we will never get the same values.
+    ++lease->subnet_id_;
+    ++lease->valid_lft_;
+    lease->fqdn_fwd_ = !lease->fqdn_fwd_;
+    // Make sure that the lease has been really modified.
+    ASSERT_NE(*lease, *leases[1]);
+    // Add the updated lease.
+    EXPECT_TRUE(lmptr_->addLease(lease));
+    lmptr_->commit();
+
+    // Reopen the lease database, so as the lease is re-read.
+    reopen(V4);
+
+    // The lease in the database should be modified.
+    l_returned = lmptr_->getLease4(ioaddress4_[0]);
+    ASSERT_TRUE(l_returned);
+    detailCompareLease(lease, l_returned);
+}
+
+void
+GenericLeaseMgrTest::testRecreateLease6() {
+    // Create a lease.
+    std::vector<Lease6Ptr> leases = createLeases6();
+    // Copy the lease so as we can freely modify it.
+    Lease6Ptr lease(new Lease6(*leases[0]));
+
+    // Add a lease.
+    EXPECT_TRUE(lmptr_->addLease(lease));
+    lmptr_->commit();
+
+    // Check that the lease has been successfuly added.
+    Lease6Ptr l_returned = lmptr_->getLease6(Lease::TYPE_NA, ioaddress6_[0]);
+    ASSERT_TRUE(l_returned);
+    detailCompareLease(lease, l_returned);
+
+    // Delete a lease, check that it's gone.
+    EXPECT_TRUE(lmptr_->deleteLease(ioaddress6_[0]));
+    EXPECT_FALSE(lmptr_->getLease6(Lease::TYPE_NA, ioaddress6_[0]));
+
+    // Modify the copy of the lease. Increasing values or negating them ensures
+    // that they are really modified, because we will never get the same values.
+    ++lease->subnet_id_;
+    ++lease->valid_lft_;
+    lease->fqdn_fwd_ = !lease->fqdn_fwd_;
+    // Make sure that the lease has been really modified.
+    ASSERT_NE(*lease, *leases[1]);
+    // Add the updated lease.
+    EXPECT_TRUE(lmptr_->addLease(lease));
+    lmptr_->commit();
+
+    // Reopen the lease database, so as the lease is re-read.
+    reopen(V6);
+
+    // The lease in the database should be modified.
+    l_returned = lmptr_->getLease6(Lease::TYPE_NA, ioaddress6_[0]);
+    ASSERT_TRUE(l_returned);
+    detailCompareLease(lease, l_returned);
+}
+
+
 }; // namespace test
 }; // namespace dhcp
 }; // namespace isc

+ 23 - 1
src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.h

@@ -30,6 +30,12 @@ namespace test {
 class GenericLeaseMgrTest : public ::testing::Test {
 public:
 
+    /// @brief Universe (V4 or V6).
+    enum Universe {
+        V4,
+        V6
+    };
+
     /// @brief Default constructor.
     GenericLeaseMgrTest();
 
@@ -40,7 +46,9 @@ public:
     ///
     /// Closes the database and re-opens it. It must be implemented
     /// in derived classes.
-    virtual void reopen() = 0;
+    ///
+    /// @param u Universe (V4 or V6), required by some backends.
+    virtual void reopen(Universe u = V4) = 0;
 
     /// @brief Initialize Lease4 Fields
     ///
@@ -159,6 +167,13 @@ public:
     /// @todo: check if it does overlap with @ref testGetLease4NullClientId()
     void testLease4NullClientId();
 
+    /// @brief Check that the DHCPv4 lease can be added, removed and recreated.
+    ///
+    /// This test creates a lease, removes it and then recreates it with some
+    /// of the attributes changed. Next it verifies that the lease in the
+    /// persistent storage has been updated as expected.
+    void testRecreateLease4();
+
     /// @brief Basic Lease6 Checks
     ///
     /// Checks that the addLease, getLease6 (by address) and deleteLease (with an
@@ -223,6 +238,13 @@ public:
     /// Checks that the code is able to update an IPv6 lease in the database.
     void testUpdateLease6();
 
+    /// @brief Check that the DHCPv6 lease can be added, removed and recreated.
+    ///
+    /// This test creates a lease, removes it and then recreates it with some
+    /// of the attributes changed. Next it verifies that the lease in the
+    /// persistent storage has been updated as expected.
+    void testRecreateLease6();
+
     /// @brief String forms of IPv4 addresses
     std::vector<std::string>  straddress4_;
 

+ 69 - 0
src/lib/dhcpsrv/tests/lease_file_io.cc

@@ -0,0 +1,69 @@
+// 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 <dhcpsrv/tests/lease_file_io.h>
+#include <fstream>
+#include <sstream>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+LeaseFileIO::LeaseFileIO(const std::string& filename)
+    : testfile_(filename) {
+    removeFile();
+}
+
+LeaseFileIO::~LeaseFileIO() {
+    removeFile();
+}
+
+bool
+LeaseFileIO::exists() const {
+    std::ifstream fs(testfile_.c_str());
+    bool ok = fs.good();
+    fs.close();
+    return (ok);
+}
+
+std::string
+LeaseFileIO::readFile() const {
+    std::ifstream fs(testfile_.c_str());
+    if (!fs.is_open()) {
+        return ("");
+    }
+    std::string contents((std::istreambuf_iterator<char>(fs)),
+                         std::istreambuf_iterator<char>());
+    fs.close();
+    return (contents);
+}
+
+void
+LeaseFileIO::removeFile() const {
+    remove(testfile_.c_str());
+}
+
+void
+LeaseFileIO::writeFile(const std::string& contents) const {
+    std::ofstream fs(testfile_.c_str(), std::ofstream::out);
+    if (fs.is_open()) {
+        fs << contents;
+        fs.close();
+    }
+}
+
+} // end of namespace isc::dhcp::test
+} // end of namespace isc::dhcp
+} // end of namespace isc
+

+ 65 - 0
src/lib/dhcpsrv/tests/lease_file_io.h

@@ -0,0 +1,65 @@
+// 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.
+
+#ifndef LEASE_FILE_IO_H
+#define LEASE_FILE_IO_H
+
+#include <string>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief This class contains functions to perform IO operations on files.
+///
+/// This class is solely used by unit tests. Some tests often need files
+/// as an input. This class allows for easy creation of text files that can
+/// be later used by unit tests. It also provides method to read the contents
+/// of the existing file and remove existing file (cleanup after unit test).
+class LeaseFileIO {
+public:
+    /// @brief Constructor
+    ///
+    /// @param filename Abolsute path to the file.
+    LeaseFileIO(const std::string& filename);
+
+    /// @brief Destructor.
+    ~LeaseFileIO();
+
+    /// @brief Check if test file exists on disk.
+    bool exists() const;
+
+    /// @brief Reads whole lease file.
+    ///
+    /// @return Contents of the file.
+    std::string readFile() const;
+
+    /// @brief Removes existing file (if any).
+    void removeFile() const;
+
+    /// @brief Creates file with contents.
+    ///
+    /// @param contents Contents of the file.
+    void writeFile(const std::string& contents) const;
+
+    /// @brief Absolute path to the file used in the tests.
+    std::string testfile_;
+
+};
+
+}
+}
+}
+
+#endif // LEASE_FILE_IO_H

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

@@ -253,7 +253,7 @@ public:
     ///
     /// No-op implementation. We need to provide concrete implementation,
     /// as this is a pure virtual method in GenericLeaseMgrTest.
-    virtual void reopen() {
+    virtual void reopen(Universe) {
     }
 
 };

+ 190 - 14
src/lib/dhcpsrv/tests/memfile_lease_mgr_unittest.cc

@@ -16,8 +16,11 @@
 
 #include <asiolink/io_address.h>
 #include <dhcp/duid.h>
+#include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcpsrv/memfile_lease_mgr.h>
+#include <dhcpsrv/tests/lease_file_io.h>
 #include <dhcpsrv/tests/test_utils.h>
 #include <dhcpsrv/tests/generic_lease_mgr_unittest.h>
 #include <gtest/gtest.h>
@@ -32,6 +35,7 @@ using namespace isc::dhcp;
 using namespace isc::dhcp::test;
 
 namespace {
+
 // empty class for now, but may be extended once Addr6 becomes bigger
 class MemfileLeaseMgrTest : public GenericLeaseMgrTest {
 public:
@@ -39,43 +43,170 @@ public:
     /// @brief memfile lease mgr test constructor
     ///
     /// Creates memfile and stores it in lmptr_ pointer
-    MemfileLeaseMgrTest() {
-        const LeaseMgr::ParameterMap pmap;
-        lmptr_ = new Memfile_LeaseMgr(pmap);
+    MemfileLeaseMgrTest() :
+        io4_(getLeaseFilePath("leasefile4_0.csv")),
+        io6_(getLeaseFilePath("leasefile6_0.csv")) {
+
+        // Make sure there are no dangling files after previous tests.
+        io4_.removeFile();
+        io6_.removeFile();
     }
 
-    virtual void reopen() {
-        /// @todo: write lease to disk, flush, read file from disk
+    /// @brief Reopens the connection to the backend.
+    ///
+    /// This function is called by unit tests to force reconnection of the
+    /// backend to check that the leases are stored and can be retrieved
+    /// from the storage.
+    ///
+    /// @param u Universe (V4 or V6)
+    virtual void reopen(Universe u) {
+        LeaseMgrFactory::destroy();
+        startBackend(u);
     }
 
     /// @brief destructor
     ///
     /// destroys lease manager backend.
     virtual ~MemfileLeaseMgrTest() {
-        delete lmptr_;
-        lmptr_ = 0;
+        LeaseMgrFactory::destroy();
+    }
+
+    /// @brief Return path to the lease file used by unit tests.
+    ///
+    /// @param filename Name of the lease file appended to the path to the
+    /// directory where test data is held.
+    ///
+    /// @return Full path to the lease file.
+    static std::string getLeaseFilePath(const std::string& filename) {
+        std::ostringstream s;
+        s << TEST_DATA_BUILDDIR << "/" << filename;
+        return (s.str());
     }
 
+    /// @brief Returns the configuration string for the backend.
+    ///
+    /// This string configures the @c LeaseMgrFactory to create the memfile
+    /// backend and use leasefile4_0.csv and leasefile6_0.csv files as
+    /// storage for leases.
+    ///
+    /// @param no_universe Indicates whether universe parameter should be
+    /// included (false), or not (true).
+    ///
+    /// @return Configuration string for @c LeaseMgrFactory.
+    static std::string getConfigString(Universe u) {
+        std::ostringstream s;
+        s << "type=memfile " << (u == V4 ? "universe=4 " : "universe=6 ")
+          << "name="
+          << getLeaseFilePath(u == V4 ? "leasefile4_0.csv" : "leasefile6_0.csv");
+        return (s.str());
+    }
+
+    /// @brief Creates instance of the backend.
+    ///
+    /// @param u Universe (v4 or V6).
+    void startBackend(Universe u) {
+        try {
+            LeaseMgrFactory::create(getConfigString(u));
+        } catch (...) {
+            std::cerr << "*** ERROR: unable to create instance of the Memfile\n"
+                " lease database backend.\n";
+            throw;
+        }
+        lmptr_ = &(LeaseMgrFactory::instance());
+    }
+
+    /// @brief Object providing access to v4 lease IO.
+    LeaseFileIO io4_;
+
+    /// @brief Object providing access to v6 lease IO.
+    LeaseFileIO io6_;
+
 };
 
 // This test checks if the LeaseMgr can be instantiated and that it
 // parses parameters string properly.
 TEST_F(MemfileLeaseMgrTest, constructor) {
-
-    const LeaseMgr::ParameterMap pmap;  // Empty parameter map
+    LeaseMgr::ParameterMap pmap;
+    pmap["universe"] = "4";
+    pmap["persist"] = "false";
     boost::scoped_ptr<Memfile_LeaseMgr> lease_mgr;
 
-    ASSERT_NO_THROW(lease_mgr.reset(new Memfile_LeaseMgr(pmap)));
+    EXPECT_NO_THROW(lease_mgr.reset(new Memfile_LeaseMgr(pmap)));
+
+    pmap["persist"] = "true";
+    pmap["name"] = getLeaseFilePath("leasefile4_1.csv");
+    EXPECT_NO_THROW(lease_mgr.reset(new Memfile_LeaseMgr(pmap)));
+
+    // Expecting that persist parameter is yes or no. Everything other than
+    // that is wrong.
+    pmap["persist"] = "bogus";
+    pmap["name"] = getLeaseFilePath("leasefile4_1.csv");
+    EXPECT_THROW(lease_mgr.reset(new Memfile_LeaseMgr(pmap)), isc::BadValue);
 }
 
 // Checks if the getType() and getName() methods both return "memfile".
 TEST_F(MemfileLeaseMgrTest, getTypeAndName) {
+    startBackend(V4);
     EXPECT_EQ(std::string("memfile"), lmptr_->getType());
     EXPECT_EQ(std::string("memory"),  lmptr_->getName());
 }
 
+// Checks if the path to the lease files is initialized correctly.
+TEST_F(MemfileLeaseMgrTest, getLeaseFilePath) {
+    // Initialize IO objects, so as the test csv files get removed after the
+    // test (when destructors are called).
+    LeaseFileIO io4(getLeaseFilePath("leasefile4_1.csv"));
+    LeaseFileIO io6(getLeaseFilePath("leasefile6_1.csv"));
+
+    LeaseMgr::ParameterMap pmap;
+    pmap["universe"] = "4";
+    pmap["name"] = getLeaseFilePath("leasefile4_1.csv");
+    boost::scoped_ptr<Memfile_LeaseMgr> lease_mgr(new Memfile_LeaseMgr(pmap));
+
+    EXPECT_EQ(pmap["name"],
+              lease_mgr->getLeaseFilePath(Memfile_LeaseMgr::V4));
+
+    pmap["persist"] = "false";
+    lease_mgr.reset(new Memfile_LeaseMgr(pmap));
+    EXPECT_TRUE(lease_mgr->getLeaseFilePath(Memfile_LeaseMgr::V4).empty());
+    EXPECT_TRUE(lease_mgr->getLeaseFilePath(Memfile_LeaseMgr::V6).empty());
+}
+
+// Check if the persitLeases correctly checks that leases should not be written
+// to disk when disabled through configuration.
+TEST_F(MemfileLeaseMgrTest, persistLeases) {
+    // Initialize IO objects, so as the test csv files get removed after the
+    // test (when destructors are called).
+    LeaseFileIO io4(getLeaseFilePath("leasefile4_1.csv"));
+    LeaseFileIO io6(getLeaseFilePath("leasefile6_1.csv"));
+
+    LeaseMgr::ParameterMap pmap;
+    pmap["universe"] = "4";
+    // Specify the names of the lease files. Leases will be written.
+    pmap["name"] = getLeaseFilePath("leasefile4_1.csv");
+    boost::scoped_ptr<Memfile_LeaseMgr> lease_mgr(new Memfile_LeaseMgr(pmap));
+
+    lease_mgr.reset(new Memfile_LeaseMgr(pmap));
+    EXPECT_TRUE(lease_mgr->persistLeases(Memfile_LeaseMgr::V4));
+    EXPECT_FALSE(lease_mgr->persistLeases(Memfile_LeaseMgr::V6));
+
+    pmap["universe"] = "6";
+    pmap["name"] = getLeaseFilePath("leasefile6_1.csv");
+    lease_mgr.reset(new Memfile_LeaseMgr(pmap));
+    EXPECT_FALSE(lease_mgr->persistLeases(Memfile_LeaseMgr::V4));
+    EXPECT_TRUE(lease_mgr->persistLeases(Memfile_LeaseMgr::V6));
+
+    // This should disable writes of leases to disk.
+    pmap["persist"] = "false";
+    lease_mgr.reset(new Memfile_LeaseMgr(pmap));
+    EXPECT_FALSE(lease_mgr->persistLeases(Memfile_LeaseMgr::V4));
+    EXPECT_FALSE(lease_mgr->persistLeases(Memfile_LeaseMgr::V6));
+}
+
+
 // Checks that adding/getting/deleting a Lease6 object works.
 TEST_F(MemfileLeaseMgrTest, addGetDelete6) {
+    startBackend(V6);
     testAddGetDelete6(true); // true - check T1,T2 values
     // memfile is able to preserve those values, but some other
     // backends can't do that.
@@ -86,6 +217,7 @@ TEST_F(MemfileLeaseMgrTest, addGetDelete6) {
 /// Checks that the addLease, getLease4 (by address) and deleteLease (with an
 /// IPv4 address) works.
 TEST_F(MemfileLeaseMgrTest, basicLease4) {
+    startBackend(V4);
     testBasicLease4();
 }
 
@@ -93,16 +225,19 @@ TEST_F(MemfileLeaseMgrTest, basicLease4) {
 
 // Simple test about lease4 retrieval through client id method
 TEST_F(MemfileLeaseMgrTest, getLease4ClientId) {
+    startBackend(V4);
     testGetLease4ClientId();
 }
 
 // Checks that lease4 retrieval client id is null is working
 TEST_F(MemfileLeaseMgrTest, getLease4NullClientId) {
+    startBackend(V4);
     testGetLease4NullClientId();
 }
 
 // Checks lease4 retrieval through HWAddr
 TEST_F(MemfileLeaseMgrTest, getLease4HWAddr1) {
+    startBackend(V4);
     testGetLease4HWAddr1();
 }
 
@@ -111,11 +246,13 @@ TEST_F(MemfileLeaseMgrTest, getLease4HWAddr1) {
 /// Adds leases to the database and checks that they can be accessed via
 /// a combination of DUID and IAID.
 TEST_F(MemfileLeaseMgrTest, getLease4HWAddr2) {
+    startBackend(V4);
     testGetLease4HWAddr2();
 }
 
 // Checks lease4 retrieval with clientId, HWAddr and subnet_id
 TEST_F(MemfileLeaseMgrTest, getLease4ClientIdHWAddrSubnetId) {
+    startBackend(V4);
     testGetLease4ClientIdHWAddrSubnetId();
 }
 
@@ -124,10 +261,8 @@ TEST_F(MemfileLeaseMgrTest, getLease4ClientIdHWAddrSubnetId) {
 /// Checks that the addLease, getLease4(by address), getLease4(hwaddr,subnet_id),
 /// updateLease4() and deleteLease (IPv4 address) can handle NULL client-id.
 /// (client-id is optional and may not be present)
-TEST_F(MemfileLeaseMgrTest, DISABLED_lease4NullClientId) {
-
-    /// @todo Test is disabled, because memfile does not support disk storage, so
-    /// all leases are lost after reopen()
+TEST_F(MemfileLeaseMgrTest, lease4NullClientId) {
+    startBackend(V4);
     testLease4NullClientId();
 }
 
@@ -138,6 +273,7 @@ TEST_F(MemfileLeaseMgrTest, DISABLED_lease4NullClientId) {
 TEST_F(MemfileLeaseMgrTest, DISABLED_getLease4HwaddrSubnetId) {
 
     /// @todo: fails on memfile. It's probably a memfile bug.
+    startBackend(V4);
     testGetLease4HWAddrSubnetId();
 }
 
@@ -146,6 +282,7 @@ TEST_F(MemfileLeaseMgrTest, DISABLED_getLease4HwaddrSubnetId) {
 /// Adds leases to the database and checks that they can be accessed via
 /// the Client ID.
 TEST_F(MemfileLeaseMgrTest, getLease4ClientId2) {
+    startBackend(V4);
     testGetLease4ClientId2();
 }
 
@@ -153,6 +290,7 @@ TEST_F(MemfileLeaseMgrTest, getLease4ClientId2) {
 //
 // Check that the system can cope with a client ID of any size.
 TEST_F(MemfileLeaseMgrTest, getLease4ClientIdSize) {
+    startBackend(V4);
     testGetLease4ClientIdSize();
 }
 
@@ -161,9 +299,20 @@ TEST_F(MemfileLeaseMgrTest, getLease4ClientIdSize) {
 /// Adds leases to the database and checks that they can be accessed via
 /// a combination of client and subnet IDs.
 TEST_F(MemfileLeaseMgrTest, getLease4ClientIdSubnetId) {
+    startBackend(V4);
     testGetLease4ClientIdSubnetId();
 }
 
+/// @brief Basic Lease6 Checks
+///
+/// Checks that the addLease, getLease6 (by address) and deleteLease (with an
+/// IPv6 address) works.
+TEST_F(MemfileLeaseMgrTest, basicLease6) {
+    startBackend(V6);
+    testBasicLease6();
+}
+
+
 /// @brief Check GetLease6 methods - access by DUID/IAID
 ///
 /// Adds leases to the database and checks that they can be accessed via
@@ -171,6 +320,7 @@ TEST_F(MemfileLeaseMgrTest, getLease4ClientIdSubnetId) {
 /// @todo: test disabled, because Memfile_LeaseMgr::getLeases6(Lease::Type,
 /// const DUID& duid, uint32_t iaid) const is not implemented yet.
 TEST_F(MemfileLeaseMgrTest, DISABLED_getLeases6DuidIaid) {
+    startBackend(V6);
     testGetLeases6DuidIaid();
 }
 
@@ -179,6 +329,7 @@ TEST_F(MemfileLeaseMgrTest, DISABLED_getLeases6DuidIaid) {
 /// @todo: test disabled, because Memfile_LeaseMgr::getLeases6(Lease::Type,
 /// const DUID& duid, uint32_t iaid) const is not implemented yet.
 TEST_F(MemfileLeaseMgrTest, DISABLED_getLeases6DuidSize) {
+    startBackend(V6);
     testGetLeases6DuidSize();
 }
 
@@ -191,6 +342,7 @@ TEST_F(MemfileLeaseMgrTest, DISABLED_getLeases6DuidSize) {
 /// @todo: Disabled, because type parameter in Memfile_LeaseMgr::getLease6
 /// (Lease::Type, const isc::asiolink::IOAddress& addr) const is not used.
 TEST_F(MemfileLeaseMgrTest, DISABLED_lease6LeaseTypeCheck) {
+    startBackend(V6);
     testLease6LeaseTypeCheck();
 }
 
@@ -199,12 +351,14 @@ TEST_F(MemfileLeaseMgrTest, DISABLED_lease6LeaseTypeCheck) {
 /// Adds leases to the database and checks that they can be accessed via
 /// a combination of DIUID and IAID.
 TEST_F(MemfileLeaseMgrTest, getLease6DuidIaidSubnetId) {
+    startBackend(V6);
     testGetLease6DuidIaidSubnetId();
 }
 
 /// Checks that getLease6(type, duid, iaid, subnet-id) works with different
 /// DUID sizes
 TEST_F(MemfileLeaseMgrTest, getLease6DuidIaidSubnetIdSize) {
+    startBackend(V6);
     testGetLease6DuidIaidSubnetIdSize();
 }
 
@@ -215,6 +369,7 @@ TEST_F(MemfileLeaseMgrTest, getLease6DuidIaidSubnetIdSize) {
 /// We should reconsider if lease{4,6} structures should have a limit
 /// implemented in them.
 TEST_F(MemfileLeaseMgrTest, DISABLED_updateLease4) {
+    startBackend(V4);
     testUpdateLease4();
 }
 
@@ -225,9 +380,30 @@ TEST_F(MemfileLeaseMgrTest, DISABLED_updateLease4) {
 /// We should reconsider if lease{4,6} structures should have a limit
 /// implemented in them.
 TEST_F(MemfileLeaseMgrTest, DISABLED_updateLease6) {
+    startBackend(V6);
     testUpdateLease6();
 }
 
+/// @brief DHCPv4 Lease recreation tests
+///
+/// Checks that the lease can be created, deleted and recreated with
+/// different parameters. It also checks that the re-created lease is
+/// correctly stored in the lease database.
+TEST_F(MemfileLeaseMgrTest, testRecreateLease4) {
+    startBackend(V4);
+    testRecreateLease4();
+}
+
+/// @brief DHCPv6 Lease recreation tests
+///
+/// Checks that the lease can be created, deleted and recreated with
+/// different parameters. It also checks that the re-created lease is
+/// correctly stored in the lease database.
+TEST_F(MemfileLeaseMgrTest, testRecreateLease6) {
+    startBackend(V6);
+    testRecreateLease6();
+}
+
 // The following tests are not applicable for memfile. When adding
 // new tests to the list here, make sure to provide brief explanation
 // why they are not applicable:

+ 22 - 1
src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc

@@ -183,7 +183,10 @@ public:
     ///
     /// Closes the database and re-open it.  Anything committed should be
     /// visible.
-    void reopen() {
+    ///
+    /// Parameter is ignored for MySQL backend as the v4 and v6 leases share
+    /// the same database.
+    void reopen(Universe) {
         LeaseMgrFactory::destroy();
         LeaseMgrFactory::create(validConnectionString());
         lmptr_ = &(LeaseMgrFactory::instance());
@@ -485,4 +488,22 @@ TEST_F(MySqlLeaseMgrTest, updateLease6) {
     testUpdateLease6();
 }
 
+/// @brief DHCPv4 Lease recreation tests
+///
+/// Checks that the lease can be created, deleted and recreated with
+/// different parameters. It also checks that the re-created lease is
+/// correctly stored in the lease database.
+TEST_F(MySqlLeaseMgrTest, testRecreateLease4) {
+    testRecreateLease4();
+}
+
+/// @brief DHCPv6 Lease recreation tests
+///
+/// Checks that the lease can be created, deleted and recreated with
+/// different parameters. It also checks that the re-created lease is
+/// correctly stored in the lease database.
+TEST_F(MySqlLeaseMgrTest, testRecreateLease6) {
+    testRecreateLease6();
+}
+
 }; // Of anonymous namespace

+ 4 - 1
src/lib/dhcpsrv/tests/pgsql_lease_mgr_unittest.cc

@@ -190,7 +190,10 @@ public:
     ///
     /// Closes the database and re-open it.  Anything committed should be
     /// visible.
-    void reopen() {
+    ///
+    /// Parameter is ignored for Postgres abckend as the v4 and v6 leases share
+    /// the same database.
+    void reopen(Universe) {
         LeaseMgrFactory::destroy();
         LeaseMgrFactory::create(validConnectionString());
         lmptr_ = &(LeaseMgrFactory::instance());

+ 2 - 1
src/lib/util/Makefile.am

@@ -19,7 +19,8 @@ AM_CXXFLAGS += $(BOOST_MAPPED_FILE_CXXFLAG)
 endif
 
 lib_LTLIBRARIES = libb10-util.la
-libb10_util_la_SOURCES  = filename.h filename.cc
+libb10_util_la_SOURCES  = csv_file.h csv_file.cc
+libb10_util_la_SOURCES += filename.h filename.cc
 libb10_util_la_SOURCES += locks.h lru_list.h
 libb10_util_la_SOURCES += strutil.h strutil.cc
 libb10_util_la_SOURCES += buffer.h io_utilities.h

+ 374 - 0
src/lib/util/csv_file.cc

@@ -0,0 +1,374 @@
+// 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 <util/csv_file.h>
+#include <boost/algorithm/string/classification.hpp>
+#include <boost/algorithm/string/constants.hpp>
+#include <boost/algorithm/string/split.hpp>
+#include <fstream>
+#include <sstream>
+
+namespace isc {
+namespace util {
+
+CSVRow::CSVRow(const size_t cols, const char separator)
+    : separator_(1, separator), values_(cols) {
+}
+
+CSVRow::CSVRow(const std::string& text, const char separator)
+    : separator_(1, separator) {
+    // Parsing is exception safe, so this will not throw.
+    parse(text.c_str());
+}
+
+void
+CSVRow::parse(const std::string& line) {
+    // Tokenize the string using a specified separator. Disable compression,
+    // so as the two consecutive separators mark an empty value.
+    boost::split(values_, line, boost::is_any_of(separator_),
+                 boost::algorithm::token_compress_off);
+}
+
+std::string
+CSVRow::readAt(const size_t at) const {
+    checkIndex(at);
+    return (values_[at]);
+}
+
+std::string
+CSVRow::render() const {
+    std::ostringstream s;
+    for (int i = 0; i < values_.size(); ++i) {
+        // Do not put separator before the first value.
+        if (i > 0) {
+            s << separator_;
+        }
+        s << values_[i];
+    }
+    return (s.str());
+}
+
+void
+CSVRow::writeAt(const size_t at, const char* value) {
+    checkIndex(at);
+    values_[at] = value;
+}
+
+std::ostream& operator<<(std::ostream& os, const CSVRow& row) {
+    os << row.render();
+    return (os);
+}
+
+void
+CSVRow::checkIndex(const size_t at) const {
+    if (at >= values_.size()) {
+        isc_throw(CSVFileError, "value index '" << at << "' of the CSV row"
+                  " is out of bounds; maximal index is '"
+                  << (values_.size() - 1) << "'");
+    }
+}
+
+CSVFile::CSVFile(const std::string& filename)
+    : primary_separator_(','), filename_(filename), fs_(), cols_(0),
+      read_msg_() {
+}
+
+CSVFile::~CSVFile() {
+    close();
+}
+
+void
+CSVFile::close() {
+    // It is allowed to close multiple times. If file has been already closed,
+    // this is no-op.
+    if (fs_) {
+        fs_->close();
+        fs_.reset();
+    }
+}
+
+void
+CSVFile::flush() const {
+    checkStreamStatusAndReset("flush");
+    fs_->flush();
+}
+
+void
+CSVFile::addColumn(const std::string& col_name) {
+    // It is not allowed to add a new column when file is open.
+    if (fs_) {
+        isc_throw(CSVFileError, "attempt to add a column '" << col_name
+                  << "' while the file '" << getFilename()
+                  << "' is open");
+    }
+    addColumnInternal(col_name);
+}
+
+void
+CSVFile::addColumnInternal(const std::string& col_name) {
+    if (getColumnIndex(col_name) >= 0) {
+        isc_throw(CSVFileError, "attempt to add duplicate column '"
+                  << col_name << "'");
+    }
+    cols_.push_back(col_name);
+}
+
+void
+CSVFile::append(const CSVRow& row) const {
+    checkStreamStatusAndReset("append");
+
+    if (row.getValuesCount() != getColumnCount()) {
+        isc_throw(CSVFileError, "number of values in the CSV row '"
+                  << row.getValuesCount() << "' doesn't match the number of"
+                  " columns in the CSV file '" << getColumnCount() << "'");
+    }
+
+    /// @todo Apparently, seekp and seekg are interchangable. A call to seekp
+    /// results in moving the input pointer too. This is ok for now. It means
+    /// that when the append() is called, the read pointer is moved to the EOF.
+    /// For the current use cases we only read a file and then append a new
+    /// content. If we come up with the scenarios when read and write is
+    /// needed at the same time, we may revisit this: perhaps remember the
+    /// old pointer. Also, for safety, we call both functions so as we are
+    /// sure that both pointers are moved.
+    fs_->seekp(0, std::ios_base::end);
+    fs_->seekg(0, std::ios_base::end);
+    fs_->clear();
+
+    std::string text = row.render();
+    *fs_ << text << std::endl;
+    if (!fs_->good()) {
+        fs_->clear();
+        isc_throw(CSVFileError, "failed to write CSV row '"
+                  << text << "' to the file '" << filename_ << "'");
+    }
+}
+
+void
+CSVFile::checkStreamStatusAndReset(const std::string& operation) const {
+    if (!fs_) {
+        isc_throw(CSVFileError, "NULL stream pointer when performing '"
+                  << operation << "' on file '" << filename_ << "'");
+
+    } else if (!fs_->is_open()) {
+        fs_->clear();
+        isc_throw(CSVFileError, "closed stream when performing '"
+                  << operation << "' on file '" << filename_ << "'");
+
+    } else {
+        fs_->clear();
+    }
+}
+
+std::ifstream::pos_type
+CSVFile::size() const {
+    std::ifstream fs(filename_.c_str());
+    bool ok = fs.good();
+    // If something goes wrong, including that the file doesn't exist,
+    // return 0.
+    if (!ok) {
+        fs.close();
+        return (0);
+    }
+    std::ifstream::pos_type pos;
+    try {
+        // Seek to the end of file and see where we are. This is a size of
+        // the file.
+        fs.seekg(0, std::ifstream::end);
+        pos = fs.tellg();
+        fs.close();
+    } catch (const std::exception& ex) {
+        return (0);
+    }
+    return (pos);
+}
+
+int
+CSVFile::getColumnIndex(const std::string& col_name) const {
+    for (int i = 0; i < cols_.size(); ++i) {
+        if (cols_[i] == col_name) {
+            return (i);
+        }
+    }
+    return (-1);
+}
+
+std::string
+CSVFile::getColumnName(const size_t col_index) const {
+    if (col_index >= cols_.size()) {
+        isc_throw(isc::OutOfRange, "column index " << col_index << " in the "
+                  " CSV file '" << filename_ << "' is out of range; the CSV"
+                  " file has only  " << cols_.size() << " columns ");
+    }
+    return (cols_[col_index]);
+}
+
+bool
+CSVFile::next(CSVRow& row, const bool skip_validation) {
+    // Set somethings as row validation error. Although, we haven't started
+    // actual row validation we should get rid of any previously recorded
+    // errors so as the caller doesn't interpret them as the current one.
+    setReadMsg("validation not started");
+
+    try {
+        // Check that stream is "ready" for any IO operations.
+        checkStreamStatusAndReset("get next row");
+
+    } catch (isc::Exception& ex) {
+        setReadMsg(ex.what());
+        return (false);
+    }
+
+    // Get exactly one line of the file.
+    std::string line;
+    std::getline(*fs_, line);
+    // If we got empty line because we reached the end of file
+    // return an empty row.
+    if (line.empty() && fs_->eof()) {
+        row = EMPTY_ROW();
+        return (true);
+
+    } else if (!fs_->good()) {
+        // If we hit an IO error, communicate it to the caller but do NOT close
+        // the stream. Caller may try again.
+        setReadMsg("error reading a row from CSV file '"
+                   + std::string(filename_) + "'");
+        return (false);
+    }
+    // If we read anything, parse it.
+    row.parse(line);
+
+    // And check if it is correct.
+    return (skip_validation ? true : validate(row));
+}
+
+void
+CSVFile::open() {
+    // If file doesn't exist or is empty, we have to create our own file.
+    if (size() == 0) {
+        recreate();
+
+    } else {
+        // Try to open existing file, holding some data.
+        fs_.reset(new std::fstream(filename_.c_str()));
+
+        // Catch exceptions so as we can close the file if error occurs.
+        try {
+            // The file may fail to open. For example, because of insufficient
+            // persmissions. Although the file is not open we should call close
+            // to reset our internal pointer.
+            if (!fs_->is_open()) {
+                isc_throw(CSVFileError, "unable to open '" << filename_ << "'");
+            }
+            // Make sure we are on the beginning of the file, so as we can parse
+            // the header.
+            fs_->seekg(0);
+            if (!fs_->good()) {
+                isc_throw(CSVFileError, "unable to set read pointer in the file '"
+                          << filename_ << "'");
+            }
+
+            // Read the header.
+            CSVRow header;
+            if (!next(header, true)) {
+                isc_throw(CSVFileError, "failed to read and parse header of the"
+                          " CSV file '" << filename_ << "': "
+                          << getReadMsg());
+            }
+
+            // Check the header against the columns specified for the CSV file.
+            if (!validateHeader(header)) {
+                isc_throw(CSVFileError, "invalid header '" << header
+                          << "' in CSV file '" << filename_ << "'");
+            }
+
+            // Everything is good, so if we haven't added any columns yet,
+            // add them.
+            if (getColumnCount() == 0) {
+                for (size_t i = 0; i < header.getValuesCount(); ++i) {
+                    addColumnInternal(header.readAt(i));
+                }
+            }
+        } catch (const std::exception& ex) {
+            close();
+            throw;
+        }
+    }
+}
+
+void
+CSVFile::recreate() {
+    // There is no sense creating a file if we don't specify columns for it.
+    if (getColumnCount() == 0) {
+        close();
+        isc_throw(CSVFileError, "no columns defined for the newly"
+                  " created CSV file '" << filename_ << "'");
+    }
+
+    // Close any dangling files.
+    close();
+    fs_.reset(new std::fstream(filename_.c_str(), std::fstream::out));
+    if (!fs_->is_open()) {
+        close();
+        isc_throw(CSVFileError, "unable to open '" << filename_ << "'");
+    }
+    // Opened successfuly. Write a header to it.
+    try {
+        CSVRow header(getColumnCount());
+        for (int i = 0; i < getColumnCount(); ++i) {
+            header.writeAt(i, getColumnName(i));
+        }
+        *fs_ << header << std::endl;
+
+    } catch (const std::exception& ex) {
+        close();
+        isc_throw(CSVFileError, ex.what());
+    }
+
+}
+
+bool
+CSVFile::validate(const CSVRow& row) {
+    setReadMsg("success");
+    bool ok = (row.getValuesCount() == getColumnCount());
+    if (!ok) {
+        std::ostringstream s;
+        s << "the size of the row '" << row << "' doesn't match the number of"
+            " columns '" << getColumnCount() << "' of the CSV file '"
+          << filename_ << "'";
+        setReadMsg(s.str());
+    }
+    return (ok);
+}
+
+bool
+CSVFile::validateHeader(const CSVRow& header) {
+    if (getColumnCount() == 0) {
+        return (true);
+    }
+
+    if (getColumnCount() != header.getValuesCount()) {
+        return (false);
+    }
+
+    for (int i = 0; i < getColumnCount(); ++i) {
+        if (getColumnName(i) != header.readAt(i)) {
+            return (false);
+        }
+    }
+    return (true);
+}
+
+} // end of isc::util namespace
+} // end of isc namespace

+ 480 - 0
src/lib/util/csv_file.h

@@ -0,0 +1,480 @@
+// 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.
+
+#ifndef CSV_FILE_H
+#define CSV_FILE_H
+
+#include <exceptions/exceptions.h>
+#include <boost/lexical_cast.hpp>
+#include <boost/shared_ptr.hpp>
+#include <fstream>
+#include <ostream>
+#include <string>
+#include <vector>
+
+namespace isc {
+namespace util {
+
+/// @brief Exception thrown when an error occurs during CSV file processing.
+class CSVFileError : public Exception {
+public:
+    CSVFileError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief Represents a single row of the CSV file.
+///
+/// The object of this type can create the string holding a collection of the
+/// comma separated values, representing a row of the CSV file. It allows the
+/// selection of any character as a separator for the values. The default
+/// separator is the comma symbol.
+///
+/// The @c CSVRow object can be constructed in two different ways. The first
+/// option is that the caller creates an object holding empty values
+/// and then adds values one by one. Note that it is possible to either add
+/// a string or a number. The number is converted to the appropriate text
+/// representation. When all the values are added, the text representation of
+/// the row can be obtained by calling @c CSVRow::render function or output
+/// stream operator.
+///
+/// The @c CSVRow object can be also constructed by parsing a row of a CSV
+/// file. In this case, the separator has to be known in advance and passed to
+/// the class constructor. The constructor will call the @c CSVRow::parse
+/// function internally to tokenize the CSV row and create the collection of
+/// values. The class accessors can be then used to retrieve individual values.
+///
+/// This class is meant to be used by the @c CSVFile class to manipulate
+/// individual rows of the CSV file.
+class CSVRow {
+public:
+
+    /// @brief Constructor, creates the raw to be used for output.
+    ///
+    /// Creates CSV row with empty values. The values should be
+    /// later set using the @c CSVRow::writeAt functions. When the
+    /// @c CSVRow::render is called, the text representation of the
+    /// row will be created using a separator character specified
+    /// as an argument of this constructor.
+    ///
+    /// This constructor is exception-free.
+    ///
+    /// @param cols Number of values in the row.
+    /// @param separator Character used as a separator between values in the
+    /// text representation of the row.
+    CSVRow(const size_t cols = 0, const char separator = ',');
+
+    /// @brief Constructor, parses a single row of the CSV file.
+    ///
+    /// This constructor should be used to parse a single row of the CSV
+    /// file. The separator being used for the particular row needs to
+    /// be known in advance and specified as an argument of the constructor
+    /// if other than the default separator is used in the row being parsed.
+    /// An example string to be parsed by this function looks as follows:
+    /// "foo,bar,foo-bar".
+    ///
+    /// This constructor is exception-free.
+    ///
+    /// @param text Text representation of the CSV row.
+    /// @param separator Character being used as a separator in a parsed file.
+    CSVRow(const std::string& text, const char separator = ',');
+
+    /// @brief Returns number of values in a CSV row.
+    size_t getValuesCount() const {
+        return (values_.size());
+    }
+
+    /// @brief Parse the CSV file row.
+    ///
+    /// This function parses a string containing CSV values and assigns them
+    /// to the @c values_ private container. These values can be retrieved
+    /// from the container by calling @c CSVRow::readAt function.
+    ///
+    /// This function is exception-free.
+    ///
+    /// @param line String holding a row of comma separated values.
+    void parse(const std::string& line);
+
+    /// @brief Retrieves a value from the internal container.
+    ///
+    /// @param at Index of the value in the container. The values are indexed
+    /// from 0, where 0 corresponds to the left-most value in the CSV file row.
+    ///
+    /// @return Value at specified index in the text form.
+    ///
+    /// @throw CSVFileError if the index is out of range. The number of elements
+    /// being held by the container can be obtained using
+    /// @c CSVRow::getValuesCount.
+    std::string readAt(const size_t at) const;
+
+    /// @brief Retrieves a value from the internal container.
+    ///
+    /// This method is reads a value from the internal container and converts
+    /// this value to the type specified as a template parameter. Internally
+    /// it uses @c boost::lexical_cast.
+    ///
+    /// @param at Index of the value in the container. The values are indexed
+    /// from 0, where 0 corresponds to the left-most value in the CSV file row.
+    /// @tparam T type of the value to convert to.
+    ///
+    /// @return Converted value.
+    ///
+    /// @throw CSVFileError if the index is out of range or if the
+    /// @c boost::bad_lexical_cast is thrown by the @c boost::lexical_cast.
+    template<typename T>
+    T readAndConvertAt(const size_t at) const {
+        T cast_value;
+        try {
+            cast_value = boost::lexical_cast<T>(readAt(at).c_str());
+
+        } catch (const boost::bad_lexical_cast& ex) {
+            isc_throw(CSVFileError, ex.what());
+        }
+        return (cast_value);
+    }
+
+    /// @brief Creates a text representation of the CSV file row.
+    ///
+    /// This function iterates over all values currently held in the internal
+    /// @c values_ container and appends them to a string. The values are
+    /// separated using the separator character specified in the constructor.
+    ///
+    /// This function is exception free.
+    ///
+    /// @return Text representation of the CSV file row.
+    std::string render() const;
+
+    /// @brief Replaces the value at specified index.
+    ///
+    /// This function is used to set values to be rendered using
+    /// @c CSVRow::render function.
+    ///
+    /// @param at Index of the value to be replaced.
+    /// @param value Value to be written given as string.
+    ///
+    /// @throw CSVFileError if index is out of range.
+    void writeAt(const size_t at, const char* value);
+
+    /// @brief Replaces the value at specified index.
+    ///
+    /// This function is used to set values to be rendered using
+    /// @c CSVRow::render function.
+    ///
+    /// @param at Index of the value to be replaced.
+    /// @param value Value to be written given as string.
+    ///
+    /// @throw CSVFileError if index is out of range.
+    void writeAt(const size_t at, const std::string& value) {
+        writeAt(at, value.c_str());
+    }
+
+    /// @brief Replaces the value at specified index.
+    ///
+    /// This function is used to set values to be rendered using
+    /// @c CSVRow::render function.
+    ///
+    /// @param at Index of the value to be replaced.
+    /// @param value Value to be written - typically a number.
+    /// @tparam T Type of the value being written.
+    ///
+    /// @throw CSVFileError if index is out of range.
+    template<typename T>
+    void writeAt(const size_t at, const T value) {
+        checkIndex(at);
+        try {
+            values_[at] = boost::lexical_cast<std::string>(value);
+        } catch (const boost::bad_lexical_cast& ex) {
+            isc_throw(CSVFileError, "unable to stringify the value to be"
+                      " written in the CSV file row at position '"
+                      << at << "'");
+        }
+    }
+
+    /// @brief Equality operator.
+    ///
+    /// Two CSV rows are equal when their string representation is equal. This
+    /// includes the order of fields, separator etc.
+    ///
+    /// @param other Object to compare to.
+    bool operator==(const CSVRow& other) const {
+        return (render() == other.render());
+    }
+
+    /// @brief Unequality operator.
+    ///
+    /// Two CSV rows are unequal when their string representation is unequal.
+    /// This includes the order of fields, separator etc.
+    ///
+    /// @param other Object to compare to.
+    bool operator!=(const CSVRow& other) const {
+        return (render() != other.render());
+    }
+
+private:
+
+    /// @brief Check if the specified index of the value is in range.
+    ///
+    /// This function is used interally by other functions.
+    ///
+    /// @param at Value index.
+    /// @throw CSVFileError if specified index is not in range.
+    void checkIndex(const size_t at) const;
+
+    /// @brief Separator character specifed in the constructor.
+    ///
+    /// @note Separator is held as a string object (one character long),
+    /// because the boost::is_any_of algorithm requires a string, not a
+    /// char value. If we held the separator as a char, we would need to
+    /// convert it to string on every call to @c CSVRow::parse.
+    std::string separator_;
+
+    /// @brief Internal container holding values that belong to the row.
+    std::vector<std::string> values_;
+};
+
+/// @brief Overrides standard output stream operator for @c CSVRow object.
+///
+/// The resulting string of characters is the same as the one returned by
+/// @c CSVRow::render function.
+///
+/// @param os Output stream.
+/// @param row Object representing a CSV file row.
+std::ostream& operator<<(std::ostream& os, const CSVRow& row);
+
+/// @brief Provides input/output access to CSV files.
+///
+/// This class provides basic methods to access (parse) and create CSV files.
+/// The file is identified by its name qualified with the absolute path.
+/// The name of the file is passed to the constructor. Constructor doesn't
+/// open/create a file, but simply records a file name specified by a caller.
+///
+/// There are two functions that can be used to open a file:
+/// - @c open - opens an existing file; if the file doesn't exist it creates it,
+/// - @c recreate - removes existing file and creates a new one.
+///
+/// When the file is opened its header file is parsed and column names are
+/// idenetified. At this point it is already possible to get the list of the
+/// column names using appropriate accessors. The data rows are not parsed
+/// at this time. The row parsing is triggered by calling @c next function.
+/// The result of parsing a row is stored in the @c CSVRow object passed as
+/// a parameter.
+///
+/// When the new file is created (when @c recreate is called), the CSV header is
+/// immediately written into it. The header consists of the column names
+/// specified with the @c addColumn function. The subsequent rows are written
+/// into this file by calling @c append.
+class CSVFile {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param filename CSV file name.
+    CSVFile(const std::string& filename);
+
+    /// @brief Destructor
+    virtual ~CSVFile();
+
+    /// @brief Adds new column name.
+    ///
+    /// This column adds a new column but doesn't write it to the file yet.
+    /// The name of the column will be placed in the CSV header when new file
+    /// is created by calling @c recreate or @c open function.
+    ///
+    /// @param col_name Name of the column.
+    ///
+    /// @throw CSVFileError if a column with the specified name exists.
+    void addColumn(const std::string& col_name);
+
+    /// @brief Writes the CSV row into the file.
+    ///
+    /// @param Object representing a CSV file row.
+    ///
+    /// @throw CSVFileError When error occured during IO operation or if the
+    /// size of the row doesn't match the number of columns.
+    void append(const CSVRow& row) const;
+
+    /// @brief Closes the CSV file.
+    void close();
+
+    /// @brief Flushes a file.
+    void flush() const;
+
+    /// @brief Returns the number of columns in the file.
+    size_t getColumnCount() const {
+        return (cols_.size());
+    }
+
+    /// @brief Returns the path to the CSV file.
+    std::string getFilename() const {
+        return (filename_);
+    }
+
+    /// @brief Returns the description of the last error returned by the
+    /// @c CSVFile::next function.
+    ///
+    /// @return Description of the last error during row validation.
+    std::string getReadMsg() const {
+        return (read_msg_);
+    }
+
+    /// @brief Returns the index of the column having specified name.
+    ///
+    /// This function is exception safe.
+    ///
+    /// @param col_name Name of the column.
+    /// @return Index of the column or negative value if the column doesn't
+    /// exist.
+    int getColumnIndex(const std::string& col_name) const;
+
+    /// @brief Returns the name of the column.
+    ///
+    /// @param col_index Index of the column.
+    ///
+    /// @return Name of the column.
+    /// @throw CSVFileError if the specified index is out of range.
+    std::string getColumnName(const size_t col_index) const;
+
+    /// @brief Reads next row from CSV file.
+    ///
+    /// This function will return the @c CSVRow object representing a
+    /// parsed row if parsing is successful. If the end of file has been
+    /// reached, the empty row is returned (a row containing no values).
+    ///
+    /// @param [out] row Object receiving the parsed CSV file.
+    /// @param skip_validation Do not perform validation.
+    ///
+    /// @return true if row has been read and validated; false if validation
+    /// failed.
+    bool next(CSVRow& row, const bool skip_validation = false);
+
+    /// @brief Opens existing file or creates a new one.
+    ///
+    /// This function will try to open existing file if this file has size
+    /// greater than 0. If the file doesn't exist or has size of 0, the
+    /// file is recreated. If the existing file has been opened, the header
+    /// is parsed and column names are initialized in the @c CSVFile object.
+    /// The data pointer in the file is set to the beginning of the first
+    /// row. In order to retrieve the row contents the @c next function should
+    /// be called.
+    ///
+    /// @throw CSVFileError when IO operation fails.
+    void open();
+
+    /// @brief Creates a new CSV file.
+    ///
+    /// The file creation will fail if there are no columns specified.
+    /// Otherwise, this function will write the header to the file.
+    /// In order to write rows to opened file, the @c append function
+    /// should be called.
+    void recreate();
+
+    /// @brief Sets error message after row validation.
+    ///
+    /// The @c CSVFile::validate function is responsible for setting the
+    /// error message after validation of the row read from the CSV file.
+    /// It will use this function to set this message. Note, that the
+    /// @c validate function can set a message after successful validation
+    /// too. Such message could say "success", or something similar.
+    ///
+    /// @param val_msg Error message to be set.
+    void setReadMsg(const std::string& read_msg) {
+        read_msg_ = read_msg;
+    }
+
+    /// @brief Represents empty row.
+    static CSVRow EMPTY_ROW() {
+        static CSVRow row(0);
+        return (row);
+    }
+
+protected:
+
+    /// @brief Adds a column regardless if the file is open or not.
+    ///
+    /// This function adds as new column to the collection. It is meant to be
+    /// called internally by the methods of the base class and derived classes.
+    /// It must not be used in the public scope. The @c CSVFile::addColumn
+    /// must be used in the public scope instead, because it prevents addition
+    /// of the new column when the file is open.
+    ///
+    /// @param col_name Name of the column.
+    ///
+    /// @throw CSVFileError if a column with the specified name exists.
+    void addColumnInternal(const std::string& col_name);
+
+    /// @brief Validate the row read from a file.
+    ///
+    /// This function implements a basic validation for the row read from the
+    /// CSV file. It is virtual so as it may be customized in derived classes.
+    ///
+    /// This default implementation checks that the number of values in the
+    /// row corresponds to the number of columns specified for this file.
+    ///
+    /// If row validation fails, the error message is noted and can be retrieved
+    /// using @c CSVFile::getReadMsg. The function which overrides this
+    /// base implementation is responsible for setting the error message using
+    /// @c CSVFile::setReadMsg.
+    ///
+    /// @param row A row to be validated.
+    ///
+    /// @return true if the column is valid; false otherwise.
+    virtual bool validate(const CSVRow& row);
+
+private:
+
+    /// @brief This function validates the header of the CSV file.
+    ///
+    /// If there are any columns added to the @c CSVFile object, it will
+    /// compare that they exactly match (including order) the header read
+    /// from the file.
+    ///
+    /// This function is called internally by @CSVFile::open.
+    ///
+    /// @param header A row holding a header.
+    /// @return true if header matches the columns; false otherwise.
+    bool validateHeader(const CSVRow& header);
+
+    /// @brief Sanity check if stream is open.
+    ///
+    /// Checks if the file stream is open so as IO operations can be performed
+    /// on it. This is internally called by the public class members to prevent
+    /// them from performing IO operations on invalid stream and using NULL
+    /// pointer to a stream. The @c clear() method is called on the stream
+    /// after the status has been checked.
+    ///
+    /// @throw CSVFileError if stream is closed or pointer to it is NULL.
+    void checkStreamStatusAndReset(const std::string& operation) const;
+
+    /// @brief Returns size of the CSV file.
+    std::ifstream::pos_type size() const;
+
+    /// @brief Separator used by CSV file.
+    char primary_separator_;
+
+    /// @brief CSV file name.
+    std::string filename_;
+
+    /// @brief Holds a pointer to the file stream.
+    boost::shared_ptr<std::fstream> fs_;
+
+    /// @brief Holds CSV file columns.
+    std::vector<std::string> cols_;
+
+    /// @brief Holds last error during row reading or validation.
+    std::string read_msg_;
+};
+
+} // namespace isc::util
+} // namespace isc
+
+#endif // CSV_FILE_H

+ 3 - 0
src/lib/util/tests/Makefile.am

@@ -14,6 +14,8 @@ AM_LDFLAGS = -static
 endif
 
 CLEANFILES = *.gcno *.gcda
+# CSV files are created by unit tests for CSVFile class.
+CLEANFILES += *.csv
 
 TESTS_ENVIRONMENT = \
         $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
@@ -25,6 +27,7 @@ run_unittests_SOURCES  = run_unittests.cc
 run_unittests_SOURCES += base32hex_unittest.cc
 run_unittests_SOURCES += base64_unittest.cc
 run_unittests_SOURCES += buffer_unittest.cc
+run_unittests_SOURCES += csv_file_unittest.cc
 run_unittests_SOURCES += fd_share_tests.cc
 run_unittests_SOURCES += fd_tests.cc
 run_unittests_SOURCES += filename_unittest.cc

+ 439 - 0
src/lib/util/tests/csv_file_unittest.cc

@@ -0,0 +1,439 @@
+// 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 <util/csv_file.h>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+#include <fstream>
+#include <sstream>
+#include <string>
+
+namespace {
+
+using namespace isc::util;
+
+// This test checks that the single data row is parsed.
+TEST(CSVRow, parse) {
+    CSVRow row0("foo,bar,foo-bar");
+    ASSERT_EQ(3, row0.getValuesCount());
+    EXPECT_EQ("foo", row0.readAt(0));
+    EXPECT_EQ("bar", row0.readAt(1));
+    EXPECT_EQ("foo-bar", row0.readAt(2));
+
+    row0.parse("bar,,foo-bar");
+    ASSERT_EQ(3, row0.getValuesCount());
+    EXPECT_EQ("bar", row0.readAt(0));
+    EXPECT_TRUE(row0.readAt(1).empty());
+    EXPECT_EQ("foo-bar", row0.readAt(2));
+
+    CSVRow row1("foo-bar|foo|bar|", '|');
+    ASSERT_EQ(4, row1.getValuesCount());
+    EXPECT_EQ("foo-bar", row1.readAt(0));
+    EXPECT_EQ("foo", row1.readAt(1));
+    EXPECT_EQ("bar", row1.readAt(2));
+    EXPECT_TRUE(row1.readAt(3).empty());
+
+    row1.parse("");
+    ASSERT_EQ(1, row1.getValuesCount());
+    EXPECT_TRUE(row1.readAt(0).empty());
+}
+
+// This test checks that the text representation of the CSV row
+// is created correctly.
+TEST(CSVRow, render) {
+    CSVRow row0(3);
+    row0.writeAt(0, "foo");
+    row0.writeAt(1, "foo-bar");
+    row0.writeAt(2, "bar");
+
+    std::string text;
+    ASSERT_NO_THROW(text = row0.render());
+    EXPECT_EQ(text, "foo,foo-bar,bar");
+
+    CSVRow row1(4, ';');
+    row1.writeAt(0, "foo");
+    row1.writeAt(2, "bar");
+    row1.writeAt(3, 10);
+
+    ASSERT_NO_THROW(text = row1.render());
+    EXPECT_EQ(text, "foo;;bar;10");
+
+    CSVRow row2(0);
+    ASSERT_NO_THROW(text = row2.render());
+    EXPECT_TRUE(text.empty());
+}
+
+// This test checks that the data values can be set for the CSV row.
+TEST(CSVRow, writeAt) {
+    CSVRow row(3);
+    row.writeAt(0, 10);
+    row.writeAt(1, "foo");
+    row.writeAt(2, "bar");
+
+    EXPECT_EQ("10", row.readAt(0));
+    EXPECT_EQ("foo", row.readAt(1));
+    EXPECT_EQ("bar", row.readAt(2));
+
+    EXPECT_THROW(row.writeAt(3, 20), CSVFileError);
+    EXPECT_THROW(row.writeAt(3, "foo"), CSVFileError);
+}
+
+/// @brief Test fixture class for testing operations on CSV file.
+///
+/// It implements basic operations on files, such as reading writing
+/// file removal and checking presence of the file. This is used by
+/// unit tests to verify correctness of the file created by the
+/// CSVFile class.
+class CSVFileTest : public ::testing::Test {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Sets the path to the CSV file used throughout the tests.
+    /// The name of the file is test.csv and it is located in the
+    /// current build folder.
+    ///
+    /// It also deletes any dangling files after previous tests.
+    CSVFileTest();
+
+    /// @brief Destructor.
+    ///
+    /// Deletes the test CSV file if any.
+    virtual ~CSVFileTest();
+
+    /// @brief Prepends the absolute path to the file specified
+    /// as an argument.
+    ///
+    /// @param filename Name of the file.
+    /// @return Absolute path to the test file.
+    static std::string absolutePath(const std::string& filename);
+
+    /// @brief Check if test file exists on disk.
+    bool exists() const;
+
+    /// @brief Reads whole CSV file.
+    ///
+    /// @return Contents of the file.
+    std::string readFile() const;
+
+    /// @brief Removes existing file (if any).
+    void removeFile() const;
+
+    /// @brief Creates file with contents.
+    ///
+    /// @param contents Contents of the file.
+    void writeFile(const std::string& contents) const;
+
+    /// @brief Absolute path to the file used in the tests.
+    std::string testfile_;
+
+};
+
+CSVFileTest::CSVFileTest()
+    : testfile_(absolutePath("test.csv")) {
+    removeFile();
+}
+
+CSVFileTest::~CSVFileTest() {
+    removeFile();
+}
+
+std::string
+CSVFileTest::absolutePath(const std::string& filename) {
+    std::ostringstream s;
+    s << TEST_DATA_BUILDDIR << "/" << filename;
+    return (s.str());
+}
+
+bool
+CSVFileTest::exists() const {
+    std::ifstream fs(testfile_.c_str());
+    bool ok = fs.good();
+    fs.close();
+    return (ok);
+}
+
+std::string
+CSVFileTest::readFile() const {
+    std::ifstream fs(testfile_.c_str());
+    if (!fs.is_open()) {
+        return ("");
+    }
+    std::string contents((std::istreambuf_iterator<char>(fs)),
+                         std::istreambuf_iterator<char>());
+    fs.close();
+    return (contents);
+}
+
+void
+CSVFileTest::removeFile() const {
+    remove(testfile_.c_str());
+}
+
+void
+CSVFileTest::writeFile(const std::string& contents) const {
+    std::ofstream fs(testfile_.c_str(), std::ofstream::out);
+    if (fs.is_open()) {
+        fs << contents;
+        fs.close();
+    }
+}
+
+// This test checks that the function which is used to add columns of the
+// CSV file works as expected.
+TEST_F(CSVFileTest, addColumn) {
+    boost::scoped_ptr<CSVFile> csv(new CSVFile(testfile_));
+    // Add two columns.
+    ASSERT_NO_THROW(csv->addColumn("animal"));
+    ASSERT_NO_THROW(csv->addColumn("color"));
+    // Make sure we can't add duplicates.
+    EXPECT_THROW(csv->addColumn("animal"), CSVFileError);
+    EXPECT_THROW(csv->addColumn("color"), CSVFileError);
+    // But we should still be able to add unique columns.
+    EXPECT_NO_THROW(csv->addColumn("age"));
+    EXPECT_NO_THROW(csv->addColumn("comments"));
+    // Assert that the file is opened, because the rest of the test relies
+    // on this.
+    ASSERT_NO_THROW(csv->recreate());
+    ASSERT_TRUE(exists());
+
+    // Make sure we can't add columns (even unique) when the file is open.
+    ASSERT_THROW(csv->addColumn("zoo"), CSVFileError);
+    // Close the file.
+    ASSERT_NO_THROW(csv->close());
+    // And check that now it is possible to add the column.
+    EXPECT_NO_THROW(csv->addColumn("zoo"));
+}
+
+// This test checks that the appropriate file name is initialized.
+TEST_F(CSVFileTest, getFilename) {
+    CSVFile csv(testfile_);
+    EXPECT_EQ(testfile_, csv.getFilename());
+}
+
+// This test checks that the file can be opened,  its whole content is
+// parsed correctly and data may be appended. It also checks that empty
+// row is returned when EOF is reached.
+TEST_F(CSVFileTest, openReadAllWrite) {
+    // Create a new CSV file that contains a header and two data rows.
+    writeFile("animal,age,color\n"
+              "cat,10,white\n"
+              "lion,15,yellow\n");
+
+    // Open this file and check that the header is parsed.
+    boost::scoped_ptr<CSVFile> csv(new CSVFile(testfile_));
+    ASSERT_NO_THROW(csv->open());
+    ASSERT_EQ(3, csv->getColumnCount());
+    EXPECT_EQ("animal", csv->getColumnName(0));
+    EXPECT_EQ("age", csv->getColumnName(1));
+    EXPECT_EQ("color", csv->getColumnName(2));
+
+    // Read first row.
+    CSVRow row;
+    ASSERT_TRUE(csv->next(row));
+    ASSERT_EQ(3, row.getValuesCount());
+    EXPECT_EQ("cat", row.readAt(0));
+    EXPECT_EQ("10", row.readAt(1));
+    EXPECT_EQ("white", row.readAt(2));
+
+    // Read second row.
+    ASSERT_TRUE(csv->next(row));
+    ASSERT_EQ(3, row.getValuesCount());
+    EXPECT_EQ("lion", row.readAt(0));
+    EXPECT_EQ("15", row.readAt(1));
+    EXPECT_EQ("yellow", row.readAt(2));
+
+    // There is no 3rd row, so the empty one should be returned.
+    ASSERT_TRUE(csv->next(row));
+    EXPECT_EQ(CSVFile::EMPTY_ROW(), row);
+
+    // It should be fine to read again, but again empty row should be returned.
+    ASSERT_TRUE(csv->next(row));
+    EXPECT_EQ(CSVFile::EMPTY_ROW(), row);
+
+    // Now, let's try to append something to this file.
+    CSVRow row_write(3);
+    row_write.writeAt(0, "dog");
+    row_write.writeAt(1, 2);
+    row_write.writeAt(2, "blue");
+    ASSERT_NO_THROW(csv->append(row_write));
+
+    // Close the file.
+    ASSERT_NO_THROW(csv->flush());
+    csv->close();
+
+    // Check the the file contents are correct.
+    EXPECT_EQ("animal,age,color\n"
+              "cat,10,white\n"
+              "lion,15,yellow\n"
+              "dog,2,blue\n",
+              readFile());
+
+    // Any attempt to read from the file or write to it should now fail.
+    EXPECT_FALSE(csv->next(row));
+    EXPECT_THROW(csv->append(row_write), CSVFileError);
+}
+
+// This test checks that contents may be appended to a file which hasn't
+// been fully parsed/read.
+TEST_F(CSVFileTest, openReadPartialWrite) {
+    // Create a CSV file with two rows in it.
+    writeFile("animal,age,color\n"
+              "cat,10,white\n"
+              "lion,15,yellow\n");
+
+    // Open this file.
+    boost::scoped_ptr<CSVFile> csv(new CSVFile(testfile_));
+    ASSERT_NO_THROW(csv->open());
+
+    // Read the first row.
+    CSVRow row0(0);
+    ASSERT_NO_THROW(csv->next(row0));
+    ASSERT_EQ(3, row0.getValuesCount());
+    EXPECT_EQ("cat", row0.readAt(0));
+    EXPECT_EQ("10", row0.readAt(1));
+    EXPECT_EQ("white", row0.readAt(2));
+
+    // There is still second row to be read. But, it should be possible to
+    // skip reading it and append new row to the end of file.
+    CSVRow row_write(3);
+    row_write.writeAt(0, "dog");
+    row_write.writeAt(1, 2);
+    row_write.writeAt(2, "blue");
+    ASSERT_NO_THROW(csv->append(row_write));
+
+    // At this point, the file pointer is at the end of file, so reading
+    // should return empty row.
+    CSVRow row1(0);
+    ASSERT_NO_THROW(csv->next(row1));
+    EXPECT_EQ(CSVFile::EMPTY_ROW(), row1);
+
+    // Close the file.
+    ASSERT_NO_THROW(csv->flush());
+    csv->close();
+
+    // Check that there are two initial lines and one new there.
+    EXPECT_EQ("animal,age,color\n"
+              "cat,10,white\n"
+              "lion,15,yellow\n"
+              "dog,2,blue\n",
+              readFile());
+
+}
+
+// This test checks that the new CSV file is created and header
+// is written to it. It also checks that data rows can be
+// appended to it.
+TEST_F(CSVFileTest, recreate) {
+    boost::scoped_ptr<CSVFile> csv(new CSVFile(testfile_));
+    csv->addColumn("animal");
+    csv->addColumn("color");
+    csv->addColumn("age");
+    csv->addColumn("comments");
+    ASSERT_NO_THROW(csv->recreate());
+    ASSERT_TRUE(exists());
+
+    CSVRow row0(4);
+    row0.writeAt(0, "dog");
+    row0.writeAt(1, "grey");
+    row0.writeAt(2, 3);
+    row0.writeAt(3, "nice one");
+    ASSERT_NO_THROW(csv->append(row0));
+
+    CSVRow row1(4);
+    row1.writeAt(0, "cat");
+    row1.writeAt(1, "black");
+    row1.writeAt(2, 2);
+    ASSERT_NO_THROW(csv->append(row1));
+
+    ASSERT_NO_THROW(csv->flush());
+    csv->close();
+
+    EXPECT_EQ("animal,color,age,comments\n"
+              "dog,grey,3,nice one\n"
+              "cat,black,2,\n",
+              readFile());
+}
+
+// This test checks that the error is reported when the size of the row being
+// read doesn't match the number of columns of the CSV file.
+TEST_F(CSVFileTest, validate) {
+    // Create CSV file with 2 invalid rows in it: one too long, one too short.
+    // Apart from that, there are two valid columns that should be read
+    // successfuly.
+    writeFile("animal,age,color\n"
+              "cat,10,white\n"
+              "lion,15,yellow,black\n"
+              "dog,3,green\n"
+              "elephant,11\n");
+
+    boost::scoped_ptr<CSVFile> csv(new CSVFile(testfile_));
+    ASSERT_NO_THROW(csv->open());
+    // First row is correct.
+    CSVRow row0;
+    ASSERT_TRUE(csv->next(row0));
+    EXPECT_EQ("cat", row0.readAt(0));
+    EXPECT_EQ("10", row0.readAt(1));
+    EXPECT_EQ("white", row0.readAt(2));
+    EXPECT_EQ("success", csv->getReadMsg());
+    // This row is too long.
+    CSVRow row1;
+    EXPECT_FALSE(csv->next(row1));
+    EXPECT_NE("success", csv->getReadMsg());
+    // This row is correct.
+    CSVRow row2;
+    ASSERT_TRUE(csv->next(row2));
+    EXPECT_EQ("dog", row2.readAt(0));
+    EXPECT_EQ("3", row2.readAt(1));
+    EXPECT_EQ("green", row2.readAt(2));
+    EXPECT_EQ("success", csv->getReadMsg());
+    // This row is too short.
+    CSVRow row3;
+    EXPECT_FALSE(csv->next(row3));
+    EXPECT_NE("success", csv->getReadMsg());
+}
+
+// Test test checks that exception is thrown when the header of the CSV file
+// parsed, doesn't match the columns specified.
+TEST_F(CSVFileTest, validateHeader) {
+    // Create CSV file with 3 columns.
+    writeFile("animal,age,color\n"
+              "cat,10,white\n"
+              "lion,15,yellow,black\n");
+
+    // Invalid order of columns.
+    boost::scoped_ptr<CSVFile> csv(new CSVFile(testfile_));
+    csv->addColumn("color");
+    csv->addColumn("animal");
+    csv->addColumn("age");
+    EXPECT_THROW(csv->open(), CSVFileError);
+
+    // Too many columns.
+    csv.reset(new CSVFile(testfile_));
+    csv->addColumn("animal");
+    csv->addColumn("age");
+    csv->addColumn("color");
+    csv->addColumn("notes");
+    EXPECT_THROW(csv->open(), CSVFileError);
+
+    // Too few columns.
+    csv.reset(new CSVFile(testfile_));
+    csv->addColumn("animal");
+    csv->addColumn("age");
+    EXPECT_THROW(csv->open(), CSVFileError);
+}
+
+
+} // end of anonymous namespace