Browse Source

[master] Merge branch 'trac3669'

Marcin Siodelski 10 years ago
parent
commit
c92665ce05

+ 2 - 0
configure.ac

@@ -1469,6 +1469,7 @@ AC_CONFIG_FILES([compatcheck/Makefile
                  src/lib/util/python/Makefile
                  src/lib/util/python/gen_wiredata.py
                  src/lib/util/tests/Makefile
+                 src/lib/util/tests/process_spawn_app.sh
                  src/lib/util/threads/Makefile
                  src/lib/util/threads/tests/Makefile
                  src/lib/util/unittests/Makefile
@@ -1491,6 +1492,7 @@ AC_CONFIG_FILES([compatcheck/Makefile
            chmod +x src/lib/log/tests/logger_lock_test.sh
            chmod +x src/lib/log/tests/severity_test.sh
            chmod +x src/lib/util/python/gen_wiredata.py
+           chmod +x src/lib/util/tests/process_spawn_app.sh
            chmod +x tools/path_replacer.sh
 ])
 

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

@@ -3,9 +3,12 @@ AUTOMAKE_OPTIONS = subdir-objects
 SUBDIRS = . testutils tests
 
 dhcp_data_dir = @localstatedir@/@PACKAGE@
+kea_lfc_location = @prefix@/sbin/kea-lfc
 
 AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib -DDHCP_DATA_DIR="\"$(dhcp_data_dir)\""
 AM_CPPFLAGS += -DTOP_BUILDDIR="\"$(top_builddir)\""
+# Set location of the kea-lfc binary.
+AM_CPPFLAGS += -DKEA_LFC_EXECUTABLE="\"$(kea_lfc_location)\""
 AM_CPPFLAGS += $(BOOST_INCLUDES)
 if HAVE_MYSQL
 AM_CPPFLAGS += $(MYSQL_CPPFLAGS)

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

@@ -292,15 +292,39 @@ replaced by those read from the file.
 % DHCPSRV_MEMFILE_LEASE_LOAD loading lease %1
 A debug message issued when DHCP lease is being loaded from the file to memory.
 
+% DHCPSRV_MEMFILE_LFC_LEASE_FILE_RENAME_FAIL failed to rename the current lease file %1 to %2, reason: %3
+An error message logged when the Memfile lease database backend fails to
+move the current lease file to a new file on which the cleanup should
+be performed. This effectively means that the lease file cleanup
+will not take place.
+
+% DHCPSRV_MEMFILE_LFC_LEASE_FILE_REOPEN_FAIL failed to reopen lease file %1 after preparing input file for lease file cleanup, reason: %2, new leases will not be persisted!
+An error message logged when the Memfile lease database backend
+failed to re-open or re-create the lease file after renaming the
+lease file for lease file cleanup. The server will continue to
+operate but leases will not be persisted to disk.
+
 % DHCPSRV_MEMFILE_LFC_SETUP setting up the Lease File Cleanup interval to %1 sec
 An informational message logged when the Memfile lease database backend
 configures the LFC to be executed periodically. The argument holds the
 interval in seconds in which the LFC will be executed.
 
+% DHCPSRV_MEMFILE_LFC_SPAWN_FAIL lease file cleanup failed to run because kea-lfc process couldn't be spawned
+This error message is logged when the the Kea server fails to run kea-lfc,
+the program that cleans up the lease file. The server will try again the
+next time a lease file cleanup is scheduled. Although this message should
+not appear and the reason why it did investigated, the occasional failure
+to start the lease file cleanup will not impact operations. Should the
+failure persist however, the size of the lease file will increase without bound.
+
 % DHCPSRV_MEMFILE_LFC_START starting Lease File Cleanup
 An informational message issued when the Memfile lease database backend
 starts the periodic Lease File Cleanup.
 
+% DHCPSRV_MEMFILE_LFC_EXECUTE executing Lease File Cleanup using: %1
+An informational message issued when the Memfile lease database backend
+starts a new process to perform Lease File Cleanup.
+
 % 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

+ 354 - 42
src/lib/dhcpsrv/memfile_lease_mgr.cc

@@ -17,6 +17,12 @@
 #include <dhcpsrv/lease_file_loader.h>
 #include <dhcpsrv/memfile_lease_mgr.h>
 #include <exceptions/exceptions.h>
+#include <util/pid_file.h>
+#include <util/process_spawn.h>
+#include <util/signal_set.h>
+#include <cstdio>
+#include <cstring>
+#include <errno.h>
 #include <iostream>
 #include <sstream>
 
@@ -25,12 +31,201 @@ namespace {
 /// @brief Maximum number of errors to read the leases from the lease file.
 const uint32_t MAX_LEASE_ERRORS = 100;
 
+/// @brief A name of the environmental variable specifying the kea-lfc
+/// program location.
+///
+/// This variable can be set by tests to point to the location of the
+/// kea-lfc program within a build directory. If this variable is not
+/// set, the backend will use the location of the kea-lfc in the
+/// Kea installation directory.
+const char* KEA_LFC_EXECUTABLE_ENV_NAME = "KEA_LFC_EXECUTABLE";
+
 } // end of anonymous namespace
 
-using namespace isc::dhcp;
+using namespace isc::util;
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Represents a configuration for Lease File Cleanup.
+///
+/// This class is solely used by the @c Memfile_LeaseMgr as a configuration
+/// information storage for %Lease File Cleanup. Internally, it creates
+/// the interval timer and assigns a callback function (pointer to which is
+/// passed in the constructor), which will be called at the specified
+/// intervals to perform the cleanup. It is also responsible for creating
+/// and maintaing the object which is used to spawn the new process which
+/// executes the @c kea-lfc program.
+///
+/// This functionality is enclosed in a separate class so as the implementation
+/// details are not exposed in the @c Memfile_LeaseMgr header file and
+/// to maintain a single place with the LFC configuration, instead of multiple
+/// members and functions scattered in the @c Memfile_LeaseMgr class.
+class LFCSetup {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Assigns a pointer to the function triggered to perform the cleanup.
+    /// This pointer should point to the appropriate method of the
+    /// @c Memfile_LeaseMgr class.
+    ///
+    /// @param callback A pointer to the callback function.
+    /// @param io_service An io service used to create the interval timer.
+    LFCSetup(asiolink::IntervalTimer::Callback callback,
+             asiolink::IOService& io_service);
+
+    /// @brief Sets the new configuration for the %Lease File Cleanup.
+    ///
+    /// @param lfc_interval An interval in seconds at which the cleanup should
+    /// be performed.
+    /// @param lease_file4 A pointer to the DHCPv4 lease file to be cleaned up
+    /// or NULL. If this is NULL, the @c lease_file6 must be non-null.
+    /// @param lease_file6 A pointer to the DHCPv6 lease file to be cleaned up
+    /// or NULL. If this is NULL, the @c lease_file4 must be non-null.
+    void setup(const uint32_t lfc_interval,
+               const boost::shared_ptr<CSVLeaseFile4>& lease_file4,
+               const boost::shared_ptr<CSVLeaseFile6>& lease_file6);
+
+    /// @brief Spawns a new process.
+    void execute();
+
+    /// @brief Returns interval at which the cleanup is performed.
+    ///
+    /// @return Interval in milliseconds.
+    long getInterval() const;
+
+    /// @brief Checks if the lease file cleanup is in progress.
+    ///
+    /// @return true if the lease file cleanup is being executed.
+    bool isRunning() const;
+
+    /// @brief Returns exit code of the last completed cleanup.
+    int getExitStatus() const;
+
+private:
+
+    /// @brief Interval timer for LFC.
+    asiolink::IntervalTimer timer_;
+
+    /// @brief A pointer to the @c ProcessSpawn object used to execute
+    /// the LFC.
+    boost::scoped_ptr<util::ProcessSpawn> process_;
+
+    /// @brief A pointer to the callback function executed by the timer.
+    asiolink::IntervalTimer::Callback callback_;
+
+    /// @brief A PID of the last executed LFC process.
+    pid_t pid_;
+};
+
+LFCSetup::LFCSetup(asiolink::IntervalTimer::Callback callback,
+                   asiolink::IOService& io_service)
+    : timer_(io_service), process_(), callback_(callback), pid_(0) {
+}
+
+void
+LFCSetup::setup(const uint32_t lfc_interval,
+                const boost::shared_ptr<CSVLeaseFile4>& lease_file4,
+                const boost::shared_ptr<CSVLeaseFile6>& lease_file6) {
+
+    // If LFC is enabled, we have to setup the interval timer and prepare for
+    // executing the kea-lfc process.
+    if (lfc_interval > 0) {
+        std::string executable;
+        char* c_executable = getenv(KEA_LFC_EXECUTABLE_ENV_NAME);
+        if (c_executable == NULL) {
+            executable = KEA_LFC_EXECUTABLE;
+
+        } else {
+            executable = c_executable;
+        }
+
+        // Set the timer to call callback function periodically.
+        LOG_INFO(dhcpsrv_logger, DHCPSRV_MEMFILE_LFC_SETUP).arg(lfc_interval);
+        // Multiple the lfc_interval value by 1000 as this value specifies
+        // a timeout in seconds, whereas the setup() method expects the
+        // timeout in milliseconds.
+        timer_.setup(callback_, lfc_interval * 1000);
+
+        // Start preparing the command line for kea-lfc.
+
+        // Gather the base file name.
+        std::string lease_file = lease_file4 ? lease_file4->getFilename() :
+            lease_file6->getFilename();
+
+        // Create the other names by appending suffixes to the base name.
+        util::ProcessArgs args;
+        // Universe: v4 or v6.
+        args.push_back(lease_file4 ? "-4" : "-6");
+        // Previous file.
+        args.push_back("-x");
+        args.push_back(Memfile_LeaseMgr::appendSuffix(lease_file,
+                                                      Memfile_LeaseMgr::FILE_PREVIOUS));
+        // Input file.
+        args.push_back("-i");
+        args.push_back(Memfile_LeaseMgr::appendSuffix(lease_file,
+                                                      Memfile_LeaseMgr::FILE_INPUT));
+        // Output file.
+        args.push_back("-o");
+        args.push_back(Memfile_LeaseMgr::appendSuffix(lease_file,
+                                                      Memfile_LeaseMgr::FILE_OUTPUT));
+        // Finish file.
+        args.push_back("-f");
+        args.push_back(Memfile_LeaseMgr::appendSuffix(lease_file,
+                                                      Memfile_LeaseMgr::FILE_FINISH));
+        // PID file.
+        args.push_back("-p");
+        args.push_back(Memfile_LeaseMgr::appendSuffix(lease_file,
+                                                      Memfile_LeaseMgr::FILE_PID));
+
+        // The configuration file is currently unused.
+        args.push_back("-c");
+        args.push_back("ignored-path");
+
+        // Create the process (do not start it yet).
+        process_.reset(new util::ProcessSpawn(executable, args));
+    }
+}
+
+long
+LFCSetup::getInterval() const {
+    return (timer_.getInterval());
+}
+
+void
+LFCSetup::execute() {
+    try {
+        LOG_INFO(dhcpsrv_logger, DHCPSRV_MEMFILE_LFC_EXECUTE)
+            .arg(process_->getCommandLine());
+        pid_ = process_->spawn();
+        std::cout << process_->getCommandLine() << std::endl;
+
+    } catch (const ProcessSpawnError& ex) {
+        LOG_ERROR(dhcpsrv_logger, DHCPSRV_MEMFILE_LFC_SPAWN_FAIL);
+    }
+}
+
+bool
+LFCSetup::isRunning() const {
+    return (process_ && process_->isRunning(pid_));
+}
+
+int
+LFCSetup::getExitStatus() const {
+    if (!process_) {
+        isc_throw(InvalidOperation, "unable to obtain LFC process exit code: "
+                  " the process is NULL");
+    }
+    return (process_->getExitStatus(pid_));
+}
+
 
 Memfile_LeaseMgr::Memfile_LeaseMgr(const ParameterMap& parameters)
-    : LeaseMgr(parameters), lfc_timer_(*getIOService()) {
+    : LeaseMgr(parameters),
+      lfc_setup_(new LFCSetup(boost::bind(&Memfile_LeaseMgr::lfcCallback, this),
+                              *getIOService()))
+    {
     // Check the universe and use v4 file or v6 file.
     std::string universe = getParameter("universe");
     if (universe == "4") {
@@ -51,11 +246,11 @@ Memfile_LeaseMgr::Memfile_LeaseMgr(const ParameterMap& parameters)
     // 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)) {
+   if (!persistLeases(V4) && !persistLeases(V6)) {
         LOG_WARN(dhcpsrv_logger, DHCPSRV_MEMFILE_NO_STORAGE);
 
     } else  {
-        initTimers();
+        lfcSetup();
     }
 }
 
@@ -418,9 +613,38 @@ Memfile_LeaseMgr::rollback() {
               DHCPSRV_MEMFILE_ROLLBACK);
 }
 
+std::string
+Memfile_LeaseMgr::appendSuffix(const std::string& file_name,
+                               const LFCFileType& file_type) {
+    std::string name(file_name);
+    switch (file_type) {
+    case FILE_INPUT:
+        name += ".1";
+        break;
+    case FILE_PREVIOUS:
+        name += ".2";
+        break;
+    case FILE_OUTPUT:
+        name += ".output";
+        break;
+    case FILE_FINISH:
+        name += ".completed";
+        break;
+    case FILE_PID:
+        name += ".pid";
+        break;
+    default:
+        // Do not append any suffix for the FILE_CURRENT.
+        ;
+    }
+
+    return (name);
+}
+
+
 uint32_t
 Memfile_LeaseMgr::getIOServiceExecInterval() const {
-    return (static_cast<uint32_t>(lfc_timer_.getInterval() / 1000));
+    return (static_cast<uint32_t>(lfc_setup_->getInterval() / 1000));
 }
 
 std::string
@@ -482,43 +706,22 @@ Memfile_LeaseMgr::initLeaseFilePath(Universe u) {
     return (lease_file);
 }
 
-void
-Memfile_LeaseMgr::initTimers() {
-    std::string lfc_interval_str = "0";
-    try {
-        lfc_interval_str = getParameter("lfc-interval");
-    } catch (const std::exception& ex) {
-        // Ignore and default to 0.
-    }
-
-    uint32_t lfc_interval = 0;
-    try {
-        lfc_interval = boost::lexical_cast<uint32_t>(lfc_interval_str);
-    } catch (boost::bad_lexical_cast& ex) {
-        isc_throw(isc::BadValue, "invalid value of the lfc-interval "
-                  << lfc_interval_str << " specified");
-    }
-
-    if (lfc_interval > 0) {
-        asiolink::IntervalTimer::Callback cb =
-            boost::bind(&Memfile_LeaseMgr::lfcCallback, this);
-        LOG_INFO(dhcpsrv_logger, DHCPSRV_MEMFILE_LFC_SETUP).arg(lfc_interval);
-        lfc_timer_.setup(cb, lfc_interval * 1000);
+template<typename LeaseObjectType, typename LeaseFileType, typename StorageType>
+void Memfile_LeaseMgr::loadLeasesFromFiles(const std::string& filename,
+                                           boost::shared_ptr<LeaseFileType>& lease_file,
+                                           StorageType& storage) {
+    // Check if the instance of the LFC is running right now. If it is
+    // running, we refuse to load leases as the LFC may be writing to the
+    // lease files right now. When the user retries server configuration
+    // it should go through.
+    /// @todo Consider applying a timeout for an LFC and retry when this
+    /// timeout elapses.
+    PIDFile pid_file(appendSuffix(filename, FILE_PID));
+    if (pid_file.check()) {
+        isc_throw(DbOpenError, "unable to load leases from files while the "
+                  "lease file cleanup is in progress");
     }
-}
-
-void
-Memfile_LeaseMgr::lfcCallback() {
-    /// @todo Extend this method to spawn the new process which will
-    /// perform the Lease File Cleanup in background.
-    LOG_INFO(dhcpsrv_logger, DHCPSRV_MEMFILE_LFC_START);
-}
 
-template<typename LeaseObjectType, typename LeaseFileType, typename StorageType>
-void Memfile_LeaseMgr::
-loadLeasesFromFiles(const std::string& filename,
-                    boost::shared_ptr<LeaseFileType>& lease_file,
-                    StorageType& storage) {
     storage.clear();
 
     // Load the leasefile.completed, if exists.
@@ -530,13 +733,13 @@ loadLeasesFromFiles(const std::string& filename,
     } else {
         // If the leasefile.completed doesn't exist, let's load the leases
         // from leasefile.2 and leasefile.1, if they exist.
-        lease_file.reset(new LeaseFileType(std::string(filename + ".2")));
+        lease_file.reset(new LeaseFileType(appendSuffix(filename, FILE_PREVIOUS)));
         if (lease_file->exists()) {
             LeaseFileLoader::load<LeaseObjectType>(*lease_file, storage,
                                                    MAX_LEASE_ERRORS);
         }
 
-        lease_file.reset(new LeaseFileType(std::string(filename + ".1")));
+        lease_file.reset(new LeaseFileType(appendSuffix(filename, FILE_INPUT)));
         if (lease_file->exists()) {
             LeaseFileLoader::load<LeaseObjectType>(*lease_file, storage,
                                                    MAX_LEASE_ERRORS);
@@ -553,3 +756,112 @@ loadLeasesFromFiles(const std::string& filename,
     LeaseFileLoader::load<LeaseObjectType>(*lease_file, storage,
                                            MAX_LEASE_ERRORS, false);;
 }
+
+
+bool
+Memfile_LeaseMgr::isLFCRunning() const {
+    return (lfc_setup_->isRunning());
+}
+
+int
+Memfile_LeaseMgr::getLFCExitStatus() const {
+    return (lfc_setup_->getExitStatus());
+}
+
+void
+Memfile_LeaseMgr::lfcCallback() {
+    LOG_INFO(dhcpsrv_logger, DHCPSRV_MEMFILE_LFC_START);
+
+    // Check if we're in the v4 or v6 space and use the appropriate file.
+    if (lease_file4_) {
+        lfcExecute(lease_file4_);
+
+    } else if (lease_file6_) {
+        lfcExecute(lease_file6_);
+    }
+}
+
+void
+Memfile_LeaseMgr::lfcSetup() {
+    std::string lfc_interval_str = "0";
+    try {
+        lfc_interval_str = getParameter("lfc-interval");
+    } catch (const std::exception& ex) {
+        // Ignore and default to 0.
+    }
+
+    uint32_t lfc_interval = 0;
+    try {
+        lfc_interval = boost::lexical_cast<uint32_t>(lfc_interval_str);
+    } catch (boost::bad_lexical_cast& ex) {
+        isc_throw(isc::BadValue, "invalid value of the lfc-interval "
+                  << lfc_interval_str << " specified");
+    }
+
+    if (lfc_interval > 0) {
+        lfc_setup_->setup(lfc_interval, lease_file4_, lease_file6_);
+    }
+}
+
+template<typename LeaseFileType>
+void Memfile_LeaseMgr::lfcExecute(boost::shared_ptr<LeaseFileType>& lease_file) {
+    bool do_lfc = true;
+    // This string will hold a reason for the failure to rote the lease files.
+    std::string error_string = "(no details)";
+    // Check if the copy of the lease file exists already. If it does, it
+    // is an indication that another LFC instance may be in progress or
+    // may be stalled. In that case we don't want to rotate the current
+    // lease file to avoid overriding the contents of the existing file.
+    CSVFile lease_file_copy(appendSuffix(lease_file->getFilename(), FILE_INPUT));
+    if (!lease_file_copy.exists()) {
+        // Close the current file so as we can move it to the copy file.
+        lease_file->close();
+        // Move the current file to the copy file. Remember the result
+        // because we don't want to run LFC if the rename failed.
+        do_lfc = (rename(lease_file->getFilename().c_str(),
+                         lease_file_copy.getFilename().c_str()) == 0);
+
+        if (!do_lfc) {
+            LOG_ERROR(dhcpsrv_logger, DHCPSRV_MEMFILE_LFC_LEASE_FILE_RENAME_FAIL)
+                .arg(lease_file->getFilename())
+                .arg(lease_file_copy.getFilename())
+                .arg(strerror(errno));
+        }
+
+        // Regardless if we successfully moved the current file or not,
+        // we need to re-open the current file for the server to write
+        // new lease updates. If the file has been successfully moved,
+        // this will result in creation of the new file. Otherwise,
+        // an existing file will be opened.
+        try {
+            lease_file->open(true);
+
+        } catch (const CSVFileError& ex) {
+            // If we're unable to open the lease file this is a serious
+            // error because the server will not be able to persist
+            // leases.
+            /// @todo We need to better address this error. It should
+            /// trigger an alarm (once we have a monitoring system in
+            /// place) so as an administrator can correct it. In
+            /// practice it should be very rare that this happens and
+            /// is most likely related to a human error, e.g. changing
+            /// file permissions.
+            LOG_ERROR(dhcpsrv_logger, DHCPSRV_MEMFILE_LFC_LEASE_FILE_REOPEN_FAIL)
+                .arg(lease_file->getFilename())
+                .arg(ex.what());
+            // Reset the pointer to the file so as the backend doesn't
+            // try to write leases to disk.
+            lease_file.reset();
+            do_lfc = false;
+        }
+    }
+    // Once the files have been rotated, or untouched if another LFC had
+    // not finished, a new process is started.
+    if (do_lfc) {
+        lfc_setup_->execute();
+    }
+}
+
+
+} // end of namespace isc::dhcp
+} // end of namespace isc

+ 160 - 22
src/lib/dhcpsrv/memfile_lease_mgr.h

@@ -21,12 +21,16 @@
 #include <dhcpsrv/csv_lease_file6.h>
 #include <dhcpsrv/memfile_lease_storage.h>
 #include <dhcpsrv/lease_mgr.h>
+#include <util/process_spawn.h>
 
+#include <boost/scoped_ptr.hpp>
 #include <boost/shared_ptr.hpp>
 
 namespace isc {
 namespace dhcp {
 
+class LFCSetup;
+
 /// @brief Concrete implementation of a lease database backend using flat file.
 ///
 /// This class implements a lease database backend using CSV files to store
@@ -103,7 +107,7 @@ public:
 
     /// @brief Specifies universe (V4, V6)
     ///
-    /// This enumeration is used by various functions in Memfile Lease Manager,
+    /// 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.
@@ -112,6 +116,11 @@ public:
         V6
     };
 
+    /// @name Methods implementing the API of the lease database backend.
+    ///       The following methods are implementing the API of the
+    ///       @c LeaseMgr to manage leases.
+    //@{
+
     /// @brief The sole lease manager constructor
     ///
     /// dbconfig is a generic way of passing parameters. Parameters
@@ -320,10 +329,54 @@ public:
     /// support transactions, this is a no-op.
     virtual void rollback();
 
+    //@}
+
+    /// @name Public type and method used to determine file names for LFC.
+    //@{
+
+    /// @brief Types of the lease files used by the %Lease File Cleanup.
+    ///
+    /// This enumeration is used by a method which appends the appropriate
+    /// suffix to the lease file name.
+    enum LFCFileType {
+        FILE_CURRENT,  ///< %Lease File
+        FILE_INPUT,    ///< %Lease File Copy
+        FILE_PREVIOUS, ///< Previous %Lease File
+        FILE_OUTPUT,   ///< LFC Output File
+        FILE_FINISH,   ///< LFC Finish File
+        FILE_PID       ///< PID File
+    };
+
+    /// @brief Appends appropriate suffix to the file name.
+    ///
+    /// The suffix is selected using the LFC file type specified as a
+    /// parameter. Each file type uses a unique suffix or no suffix:
+    /// - Current File: no suffix
+    /// - %Lease File Copy or Input File: ".1"
+    /// - Previous File: ".2"
+    /// - LFC Output File: ".output"
+    /// - LFC Finish File: ".completed"
+    /// - LFC PID File: ".pid"
+    ///
+    /// See http://kea.isc.org/wiki/LFCDesign for details.
+    ///
+    /// @param file_name A base file name to which suffix is appended.
+    /// @param file_type An LFC file type.
+    /// @return A lease file name with a suffix appended.
+    static std::string appendSuffix(const std::string& file_name,
+                                    const LFCFileType& file_type);
+    //@}
+
+
+    /// @name Miscellaneous public convenience methods.
+    ///       The following methods allow for retrieving useful information
+    ///       about the state of the backend.
+    //@{
+
     /// @brief Returns the interval at which the @c IOService events should
     /// be released.
     ///
-    /// The Memfile backend may install a timer to execute the Lease File
+    /// The Memfile backend may install a timer to execute the %Lease File
     /// Cleanup periodically. If this timer is installed, the method returns
     /// the LFC interval in milliseconds.
     ///
@@ -359,27 +412,11 @@ public:
     /// server shut down.
     bool persistLeases(Universe u) const;
 
-private:
-
-    /// @brief A callback function triggering Lease File Cleanup.
-    ///
-    /// This method is virtual so as it can be overriden and customized in
-    /// the unit tests. In particular, the unit test which checks that the
-    /// callback function has been executed would override this function
-    /// to increase the execution counter each time it is executed.
-    ///
-    /// @todo Once the callback is implemented, there is a need to
-    /// extend the documentation of this method. Currently, it simply
-    /// logs that it has been called.
-    virtual void lfcCallback();
+    //@}
 
 private:
 
-    /// @brief Initialize the timers used to perform repeating tasks.
-    ///
-    /// Currently only one timer is supported. This timer executes the
-    /// Lease File Cleanup periodically.
-    void initTimers();
+
     /// @brief Initialize the location of the lease file.
     ///
     /// This method uses the parameters passed as a map to the constructor to
@@ -422,6 +459,21 @@ private:
     /// products of the lease file cleanups (LFC).
     /// See: http://kea.isc.org/wiki/LFCDesign for details.
     ///
+    /// @note: When the server starts up or is reconfigured it will try to
+    /// read leases from the lease files using this method. It is possible
+    /// that the %Lease File Cleanup is performed upon the lease files to
+    /// be read by this method. This may result in conflicts between the
+    /// server process and the LFC. To prevent it, the method checks if the
+    /// instance of the @c kea-lfc is running (using the PID file) before it
+    /// tries to load leases from the lease files. If it finds that there
+    /// is an LFC in progress, it throws an exception which will result
+    /// in the server refuse to start or reconfigure. When the administrator
+    /// retries starting up or reconfiguring the server it will most likely
+    /// be successful as the LFC should be complete by that time.
+    ///
+    /// @todo Consider implementing delaying the lease files loading when
+    /// the LFC is in progress by the specified amount of time.
+    ///
     /// @param filename Name of the lease file.
     /// @param lease_file An object representing a lease file to which
     /// the server will store lease updates.
@@ -431,6 +483,7 @@ private:
     /// @tparam StorageType @c Lease4Storage or @c Lease6Storage.
     ///
     /// @throw CSVFileError when parsing any of the lease files fails.
+    /// @throw DbOpenError when it is found that the LFC is in progress.
     template<typename LeaseObjectType, typename LeaseFileType,
              typename StorageType>
     void loadLeasesFromFiles(const std::string& filename,
@@ -449,8 +502,93 @@ private:
     /// @brief Holds the pointer to the DHCPv6 lease file IO.
     boost::shared_ptr<CSVLeaseFile6> lease_file6_;
 
-    /// @brief A timer scheduled to perform Lease File Cleanup.
-    asiolink::IntervalTimer lfc_timer_;
+public:
+
+    /// @name Public methods to retrieve information about the LFC process state.
+    ///       These methods are meant to be used by unit tests to retrieve the
+    ///       state of the spawned LFC process before validating the result of
+    ///       the lease file cleanup.
+    //@{
+
+    /// @brief Checks if the process performing lease file cleanup is running.
+    ///
+    /// @return true if the process performing lease file cleanup is running.
+    bool isLFCRunning() const;
+
+    /// @brief Returns the status code returned by the last executed
+    /// LFC process.
+    int getLFCExitStatus() const;
+    //@}
+
+
+    /// @name Protected methods used for %Lease File Cleanup.
+    /// The following methods are protected so as they can be accessed and
+    /// tested by unit tests.
+    //@{
+
+protected:
+
+    /// @brief A callback function triggering %Lease File Cleanup (LFC).
+    ///
+    /// This method is executed periodically to start the lease file cleanup.
+    /// It checks whether the file is a DHCPv4 lease file or DHCPv6 lease file
+    /// and executes the @c Memfile_LeaseMgr::lfcExecute private method
+    /// with the appropriate parameters.
+    ///
+    /// This method is virtual so as it can be overridden and customized in
+    /// the unit tests. In particular, the unit test which checks that the
+    /// callback function has been executed would override this function
+    /// to increase the execution counter each time it is executed.
+    virtual void lfcCallback();
+    //@}
+
+    /// @name Private methods and members used for %Lease File Cleanup.
+    //@{
+
+private:
+
+    /// @brief Setup the periodic %Lease File Cleanup.
+    ///
+    /// This method checks if the @c lfc-interval configuration parameter
+    /// is set to a non-zero value and sets up the interval timer to
+    /// perform the %Lease File Cleanup periodically. It also prepares the
+    /// path and arguments for the @c kea-lfc application which will be
+    /// executed to perform the cleanup. By default the backend will use
+    /// the path to the kea-lfc in the Kea installation directory. If
+    /// the unit tests need to override this path (with the path in the
+    /// Kea build directory, the @c KEA_LFC_EXECUTABLE environmental
+    /// variable should be set to hold an absolute path to the kea-lfc
+    /// excutable.
+    void lfcSetup();
+
+    /// @brief Performs a lease file cleanup for DHCPv4 or DHCPv6.
+    ///
+    /// This method performs all the actions necessary to prepare for the
+    /// execution of the LFC and if these actions are successful, it executes
+    /// the @c kea-lfc application as a background process to process (cleanup)
+    /// the lease files.
+    ///
+    /// For the design and the terminology used in this description refer to
+    /// the http://kea.isc.org/wiki/LFCDesign.
+    ///
+    /// If the method finds that the %Lease File Copy exists it simply runs
+    /// the @c kea-lfc application.
+    ///
+    /// If the %Lease File Copy doesn't exist it moves the Current %Lease File
+    /// to Lease File Copy, and then recreates the Current Lease File without
+    /// any lease entries. If the file has been successfully moved, it runs
+    /// the @c kea-lfc application.
+    ///
+    /// @param lease_file A pointer to the object representing the Current
+    /// %Lease File (DHCPv4 or DHCPv6 lease file).
+    ///
+    /// @tparam LeaseFileType One of @c CSVLeaseFile4 or @c CSVLeaseFile6.
+    template<typename LeaseFileType>
+    void lfcExecute(boost::shared_ptr<LeaseFileType>& lease_file);
+
+    /// @brief A pointer to the Lease File Cleanup configuration.
+    boost::scoped_ptr<LFCSetup> lfc_setup_;
+    //@}
 
 };
 

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

@@ -4,6 +4,7 @@ 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/dhcpsrv/tests\"
 AM_CPPFLAGS += -DDHCP_DATA_DIR=\"$(abs_top_builddir)/src/lib/dhcpsrv/tests\"
+AM_CPPFLAGS += -DKEA_LFC_BUILD_DIR=\"$(abs_top_builddir)/src/bin/lfc\"
 AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
 
 AM_CXXFLAGS = $(KEA_CXXFLAGS)

+ 9 - 5
src/lib/dhcpsrv/tests/lease_file_io.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -20,13 +20,17 @@ namespace isc {
 namespace dhcp {
 namespace test {
 
-LeaseFileIO::LeaseFileIO(const std::string& filename)
-    : testfile_(filename) {
-    removeFile();
+LeaseFileIO::LeaseFileIO(const std::string& filename, const bool recreate)
+    : testfile_(filename), recreate_(recreate)  {
+    if (recreate_) {
+        removeFile();
+    }
 }
 
 LeaseFileIO::~LeaseFileIO() {
-    removeFile();
+    if (recreate_) {
+        removeFile();
+    }
 }
 
 bool

+ 8 - 2
src/lib/dhcpsrv/tests/lease_file_io.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2015 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
@@ -32,7 +32,9 @@ public:
     /// @brief Constructor
     ///
     /// @param filename Abolsute path to the file.
-    LeaseFileIO(const std::string& filename);
+    /// @param recreate A boolean flag indicating if the new file should
+    /// be created, even if one exists.
+    LeaseFileIO(const std::string& filename, const bool recreate = true);
 
     /// @brief Destructor.
     ~LeaseFileIO();
@@ -56,6 +58,10 @@ public:
     /// @brief Absolute path to the file used in the tests.
     std::string testfile_;
 
+    /// @brief Indicates if the file should be recreated during object
+    /// construction and removed during destruction.
+    bool recreate_;
+
 };
 
 }

+ 318 - 3
src/lib/dhcpsrv/tests/memfile_lease_mgr_unittest.cc

@@ -23,19 +23,24 @@
 #include <dhcpsrv/tests/lease_file_io.h>
 #include <dhcpsrv/tests/test_utils.h>
 #include <dhcpsrv/tests/generic_lease_mgr_unittest.h>
+#include <util/pid_file.h>
 #include <gtest/gtest.h>
 
 #include <boost/bind.hpp>
 
+#include <cstdlib>
 #include <iostream>
 #include <fstream>
+#include <queue>
 #include <sstream>
+#include <unistd.h>
 
 using namespace std;
 using namespace isc;
 using namespace isc::asiolink;
 using namespace isc::dhcp;
 using namespace isc::dhcp::test;
+using namespace isc::util;
 
 namespace {
 
@@ -77,6 +82,20 @@ private:
 
 };
 
+/// @brief A derivation of the lease manager exposing protected methods.
+class NakedMemfileLeaseMgr : public Memfile_LeaseMgr {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Creates instance of the lease manager.
+    NakedMemfileLeaseMgr(const ParameterMap& parameters)
+        : Memfile_LeaseMgr(parameters) {
+    }
+
+    using Memfile_LeaseMgr::lfcCallback;
+};
+
 /// @brief Test fixture class for @c Memfile_LeaseMgr
 class MemfileLeaseMgrTest : public GenericLeaseMgrTest {
 public:
@@ -90,9 +109,13 @@ public:
         io_service_(),
         fail_on_callback_(false) {
 
-        // Make sure there are no dangling files after previous tests.
-        io4_.removeFile();
-        io6_.removeFile();
+        std::ostringstream s;
+        s << KEA_LFC_BUILD_DIR << "/kea-lfc";
+        setenv("KEA_LFC_EXECUTABLE", s.str().c_str(), 1);
+
+        // Remove lease files and products of Lease File Cleanup.
+        removeFiles(getLeaseFilePath("leasefile4_0.csv"));
+        removeFiles(getLeaseFilePath("leasefile6_0.csv"));
     }
 
     /// @brief Reopens the connection to the backend.
@@ -112,6 +135,30 @@ public:
     /// destroys lease manager backend.
     virtual ~MemfileLeaseMgrTest() {
         LeaseMgrFactory::destroy();
+        // Remove lease files and products of Lease File Cleanup.
+        removeFiles(getLeaseFilePath("leasefile4_0.csv"));
+        removeFiles(getLeaseFilePath("leasefile6_0.csv"));
+    }
+
+
+    /// @brief Remove files being products of Lease File Cleanup.
+    ///
+    /// @param base_name Path to the lease file name. This file is removed
+    /// and all files which names are crated from this name (having specific
+    /// suffixes used by Lease File Cleanup mechanism).
+    void removeFiles(const std::string& base_name) const {
+        // Generate suffixes and append them to the base name. The
+        // resulting file names are the ones that may exist as a
+        // result of LFC.
+        for (int i = static_cast<int>(Memfile_LeaseMgr::FILE_CURRENT);
+             i <= static_cast<int>(Memfile_LeaseMgr::FILE_FINISH);
+             ++i) {
+            Memfile_LeaseMgr::LFCFileType type = static_cast<
+                Memfile_LeaseMgr::LFCFileType>(i);
+            std::string suffix = Memfile_LeaseMgr::appendSuffix(base_name, type);
+            LeaseFileIO io(Memfile_LeaseMgr::appendSuffix(base_name, type));
+            io.removeFile();
+        }
     }
 
     /// @brief Return path to the lease file used by unit tests.
@@ -173,6 +220,24 @@ public:
         }
     }
 
+    /// @brief Waits for the specified process to finish.
+    ///
+    /// @param process An object which started the process.
+    /// @param timeout Timeout in seconds.
+    ///
+    /// @return true if the process ended, false otherwise
+    bool waitForProcess(const Memfile_LeaseMgr& lease_mgr,
+                        const uint8_t timeout) {
+        uint32_t iterations = 0;
+        const uint32_t iterations_max = timeout * 1000;
+        while (lease_mgr.isLFCRunning() && (iterations < iterations_max)) {
+            usleep(1000);
+            ++iterations;
+        }
+        return (iterations < iterations_max);
+    }
+
+
     /// @brief Object providing access to v4 lease IO.
     LeaseFileIO io4_;
 
@@ -331,6 +396,200 @@ TEST_F(MemfileLeaseMgrTest, lfcTimerDisabled) {
     EXPECT_EQ(0, lease_mgr->getLFCCount());
 }
 
+// This test that the callback function executing the cleanup of the
+// DHCPv4 lease file works as expected.
+TEST_F(MemfileLeaseMgrTest, leaseFileCleanup4) {
+    // This string contains the lease file header, which matches
+    // the contents of the new file in which no leases have been
+    // stored.
+    std::string new_file_contents =
+        "address,hwaddr,client_id,valid_lifetime,expire,"
+        "subnet_id,fqdn_fwd,fqdn_rev,hostname\n";
+
+    // This string contains the contents of the lease file with exactly
+    // one lease, but two entries. One of the entries should be removed
+    // as a result of lease file cleanup.
+    std::string current_file_contents = new_file_contents +
+        "192.0.2.2,02:02:02:02:02:02,,200,200,8,1,1,,\n"
+        "192.0.2.2,02:02:02:02:02:02,,200,800,8,1,1,,\n";
+    LeaseFileIO current_file(getLeaseFilePath("leasefile4_0.csv"));
+    current_file.writeFile(current_file_contents);
+
+    std::string previous_file_contents = new_file_contents +
+        "192.0.2.3,03:03:03:03:03:03,,200,200,8,1,1,,\n"
+        "192.0.2.3,03:03:03:03:03:03,,200,800,8,1,1,,\n";
+    LeaseFileIO previous_file(getLeaseFilePath("leasefile4_0.csv.2"));
+    previous_file.writeFile(previous_file_contents);
+
+    // Create the backend.
+    LeaseMgr::ParameterMap pmap;
+    pmap["type"] = "memfile";
+    pmap["universe"] = "4";
+    pmap["name"] = getLeaseFilePath("leasefile4_0.csv");
+    pmap["lfc-interval"] = "1";
+    boost::scoped_ptr<NakedMemfileLeaseMgr> lease_mgr(new NakedMemfileLeaseMgr(pmap));
+
+    // Try to run the lease file cleanup.
+    ASSERT_NO_THROW(lease_mgr->lfcCallback());
+
+    // The new lease file should have been created and it should contain
+    // no leases.
+    ASSERT_TRUE(current_file.exists());
+    EXPECT_EQ(new_file_contents, current_file.readFile());
+
+    // Wait for the LFC process to complete.
+    ASSERT_TRUE(waitForProcess(*lease_mgr, 2));
+
+    // And make sure it has returned an exit status of 0.
+    EXPECT_EQ(0, lease_mgr->getLFCExitStatus())
+        << "Executing the LFC process failed: make sure that"
+        " the kea-lfc program has been compiled.";
+
+    // Check if we can still write to the lease file.
+    std::vector<uint8_t> hwaddr_vec(6);
+    HWAddrPtr hwaddr(new HWAddr(hwaddr_vec, HTYPE_ETHER));
+    Lease4Ptr new_lease(new Lease4(IOAddress("192.0.2.45"), hwaddr, 0, 0,
+                                   100, 50, 60, 0, 1));
+    ASSERT_NO_THROW(lease_mgr->addLease(new_lease));
+
+    std::string updated_file_contents = new_file_contents +
+        "192.0.2.45,00:00:00:00:00:00,,100,100,1,0,0,\n";
+    EXPECT_EQ(updated_file_contents, current_file.readFile());
+
+    /// @todo Replace the following with the checks that the LFC has
+    /// completed successfully, i.e. the leasefile4_0.csv.2 exists
+    /// and it holds the cleaned up lease information.
+
+    // Until the kea-lfc is implemented and performs the cleanup, we can
+    // only check that the backend has moved the lease file to a lease
+    // file with suffix ".1".
+    LeaseFileIO input_file(getLeaseFilePath("leasefile4_0.csv.1"), false);
+    ASSERT_TRUE(input_file.exists());
+    // And this file should contain the contents of the original file.
+    EXPECT_EQ(current_file_contents, input_file.readFile());
+}
+
+// This test that the callback function executing the cleanup of the
+// DHCPv6 lease file works as expected.
+TEST_F(MemfileLeaseMgrTest, leaseFileCleanup6) {
+    // This string contains the lease file header, which matches
+    // the contents of the new file in which no leases have been
+    // stored.
+    std::string new_file_contents =
+        "address,duid,valid_lifetime,expire,subnet_id,"
+        "pref_lifetime,lease_type,iaid,prefix_len,fqdn_fwd,"
+        "fqdn_rev,hostname,hwaddr\n";
+
+    // This string contains the contents of the lease file with exactly
+    // one lease, but two entries. One of the entries should be removed
+    // as a result of lease file cleanup.
+    std::string current_file_contents = new_file_contents +
+        "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,,\n"
+        "2001:db8:1::1,00:01:02:03:04:05:06:0a:0b:0c:0d:0e:0f,200,800,"
+        "8,100,0,7,0,1,1,,\n";
+    LeaseFileIO current_file(getLeaseFilePath("leasefile6_0.csv"));
+    current_file.writeFile(current_file_contents);
+
+    std::string previous_file_contents = new_file_contents +
+        "2001:db8:1::2,01:01:01:01:01:01:01:01:01:01:01:01:01,200,200,"
+        "8,100,0,7,0,1,1,,\n"
+        "2001:db8:1::2,01:01:01:01:01:01:01:01:01:01:01:01:01,200,800,"
+        "8,100,0,7,0,1,1,,\n";
+    LeaseFileIO previous_file(getLeaseFilePath("leasefile6_0.csv.2"));
+    previous_file.writeFile(previous_file_contents);
+
+    // Create the backend.
+    LeaseMgr::ParameterMap pmap;
+    pmap["type"] = "memfile";
+    pmap["universe"] = "6";
+    pmap["name"] = getLeaseFilePath("leasefile6_0.csv");
+    pmap["lfc-interval"] = "1";
+    boost::scoped_ptr<NakedMemfileLeaseMgr> lease_mgr(new NakedMemfileLeaseMgr(pmap));
+
+    // Try to run the lease file cleanup.
+    ASSERT_NO_THROW(lease_mgr->lfcCallback());
+
+    // The new lease file should have been created and it should contain
+    // no leases.
+    ASSERT_TRUE(current_file.exists());
+    EXPECT_EQ(new_file_contents, current_file.readFile());
+
+    // Wait for the LFC process to complete.
+    ASSERT_TRUE(waitForProcess(*lease_mgr, 2));
+
+    // And make sure it has returned an exit status of 0.
+    EXPECT_EQ(0, lease_mgr->getLFCExitStatus())
+        << "Executing the LFC process failed: make sure that"
+        " the kea-lfc program has been compiled.";
+
+
+    // Check if we can still write to the lease file.
+    std::vector<uint8_t> duid_vec(13);
+    DuidPtr duid(new DUID(duid_vec));
+    Lease6Ptr new_lease(new Lease6(Lease::TYPE_NA, IOAddress("3000::1"),duid,
+                                   123, 300, 400, 100, 300, 2));
+    new_lease->cltt_ = 0;
+    ASSERT_NO_THROW(lease_mgr->addLease(new_lease));
+
+    std::string update_file_contents = new_file_contents +
+        "3000::1,00:00:00:00:00:00:00:00:00:00:00:00:00,400,"
+        "400,2,300,0,123,128,0,0,,\n";
+    EXPECT_EQ(update_file_contents, current_file.readFile());
+
+    /// @todo Replace the following with the checks that the LFC has
+    /// completed successfully, i.e. the leasefile6_0.csv.2 exists
+    /// and it holds the cleaned up lease information.
+
+    // Until the kea-lfc is implemented and performs the cleanup, we can
+    // only check that the backend has moved the lease file to a lease
+    // file with suffix ".1".
+    LeaseFileIO input_file(getLeaseFilePath("leasefile6_0.csv.1"), false);
+    ASSERT_TRUE(input_file.exists());
+    // And this file should contain the contents of the original file.
+    EXPECT_EQ(current_file_contents, input_file.readFile());
+}
+
+// This test verifies that EXIT_FAILURE status code is returned when
+// the LFC process fails to start.
+TEST_F(MemfileLeaseMgrTest, leaseFileCleanupStartFail) {
+    // This string contains the lease file header, which matches
+    // the contents of the new file in which no leases have been
+    // stored.
+    std::string new_file_contents =
+        "address,hwaddr,client_id,valid_lifetime,expire,"
+        "subnet_id,fqdn_fwd,fqdn_rev,hostname\n";
+
+    // Create the lease file to be used by the backend.
+    std::string current_file_contents = new_file_contents +
+        "192.0.2.2,02:02:02:02:02:02,,200,200,8,1,1,,\n";
+    LeaseFileIO current_file(getLeaseFilePath("leasefile4_0.csv"));
+    current_file.writeFile(current_file_contents);
+
+    // Specify invalid path to the kea-lfc executable. This should result
+    // in failure status code returned by the child process.
+    setenv("KEA_LFC_EXECUTABLE", "foobar", 1);
+
+    // Create the backend.
+    LeaseMgr::ParameterMap pmap;
+    pmap["type"] = "memfile";
+    pmap["universe"] = "4";
+    pmap["name"] = getLeaseFilePath("leasefile4_0.csv");
+    pmap["lfc-interval"] = "1";
+    boost::scoped_ptr<NakedMemfileLeaseMgr> lease_mgr(new NakedMemfileLeaseMgr(pmap));
+
+    // Try to run the lease file cleanup.
+    ASSERT_NO_THROW(lease_mgr->lfcCallback());
+
+    // Wait for the LFC process to complete.
+    ASSERT_TRUE(waitForProcess(*lease_mgr, 2));
+
+    // And make sure it has returned an error.
+    EXPECT_EQ(EXIT_FAILURE, lease_mgr->getLFCExitStatus())
+        << "Executing the LFC process failed: make sure that"
+        " the kea-lfc program has been compiled.";
+}
+
 // Test that the backend returns a correct value of the interval
 // at which the IOService must be executed to run the handlers
 // for the installed timers.
@@ -346,11 +605,13 @@ TEST_F(MemfileLeaseMgrTest, getIOServiceExecInterval) {
 
     // lfc-interval = 10
     pmap["lfc-interval"] = "10";
+    lease_mgr.reset();
     lease_mgr.reset(new LFCMemfileLeaseMgr(pmap));
     EXPECT_EQ(10, lease_mgr->getIOServiceExecInterval());
 
     // lfc-interval = 20
     pmap["lfc-interval"] = "20";
+    lease_mgr.reset();
     lease_mgr.reset(new LFCMemfileLeaseMgr(pmap));
     EXPECT_EQ(20, lease_mgr->getIOServiceExecInterval());
 
@@ -739,6 +1000,33 @@ TEST_F(MemfileLeaseMgrTest, load4CompletedFile) {
     EXPECT_FALSE(lmptr_->getLease4(IOAddress("192.0.2.1")));
 }
 
+// This test checks that backend constructor refuses to load leases from the
+// lease files if the LFC is in progress.
+TEST_F(MemfileLeaseMgrTest, load4LFCInProgress) {
+    // Create the backend configuration.
+    LeaseMgr::ParameterMap pmap;
+    pmap["type"] = "memfile";
+    pmap["universe"] = "4";
+    pmap["name"] = getLeaseFilePath("leasefile4_0.csv");
+    pmap["lfc-interval"] = "1";
+
+    // Create a pid file holding the PID of the current process. Choosing the
+    // pid of the current process guarantees that when the backend starts up
+    // the process is alive.
+    PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(pmap["name"], Memfile_LeaseMgr::FILE_PID));
+    pid_file.write();
+
+    // There is a pid file and the process which pid is in the file is
+    // running, so the backend should refuse to start.
+    boost::scoped_ptr<NakedMemfileLeaseMgr> lease_mgr;
+    ASSERT_THROW(lease_mgr.reset(new NakedMemfileLeaseMgr(pmap)),
+                 DbOpenError);
+
+    // Remove the pid file, and retry. The bakckend should be created.
+    pid_file.deleteFile();
+    ASSERT_NO_THROW(lease_mgr.reset(new NakedMemfileLeaseMgr(pmap)));
+}
+
 // This test checks that the backend reads DHCPv6 lease data from multiple
 // files.
 TEST_F(MemfileLeaseMgrTest, load6MultipleLeaseFiles) {
@@ -951,4 +1239,31 @@ TEST_F(MemfileLeaseMgrTest, load6CompletedFile) {
     EXPECT_FALSE(lmptr_->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::3")));
 }
 
+// This test checks that backend constructor refuses to load leases from the
+// lease files if the LFC is in progress.
+TEST_F(MemfileLeaseMgrTest, load6LFCInProgress) {
+    // Create the backend configuration.
+    LeaseMgr::ParameterMap pmap;
+    pmap["type"] = "memfile";
+    pmap["universe"] = "6";
+    pmap["name"] = getLeaseFilePath("leasefile6_0.csv");
+    pmap["lfc-interval"] = "1";
+
+    // Create a pid file holding the PID of the current process. Choosing the
+    // pid of the current process guarantees that when the backend starts up
+    // the process is alive.
+    PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(pmap["name"], Memfile_LeaseMgr::FILE_PID));
+    pid_file.write();
+
+    // There is a pid file and the process which pid is in the file is
+    // running, so the backend should refuse to start.
+    boost::scoped_ptr<NakedMemfileLeaseMgr> lease_mgr;
+    ASSERT_THROW(lease_mgr.reset(new NakedMemfileLeaseMgr(pmap)),
+                 DbOpenError);
+
+    // Remove the pid file, and retry. The bakckend should be created.
+    pid_file.deleteFile();
+    ASSERT_NO_THROW(lease_mgr.reset(new NakedMemfileLeaseMgr(pmap)));
+}
+
 }; // end of anonymous namespace

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

@@ -19,6 +19,7 @@ libkea_util_la_SOURCES += memory_segment.h
 libkea_util_la_SOURCES += memory_segment_local.h memory_segment_local.cc
 libkea_util_la_SOURCES += optional_value.h
 libkea_util_la_SOURCES += pid_file.h pid_file.cc
+libkea_util_la_SOURCES += process_spawn.h process_spawn.cc
 libkea_util_la_SOURCES += range_utilities.h
 libkea_util_la_SOURCES += signal_set.cc signal_set.h
 libkea_util_la_SOURCES += encode/base16_from_binary.h

+ 14 - 1
src/lib/util/csv_file.cc

@@ -261,7 +261,7 @@ CSVFile::next(CSVRow& row, const bool skip_validation) {
 }
 
 void
-CSVFile::open() {
+CSVFile::open(const bool seek_to_end) {
     // If file doesn't exist or is empty, we have to create our own file.
     if (size() == static_cast<std::streampos>(0)) {
         recreate();
@@ -308,6 +308,19 @@ CSVFile::open() {
                     addColumnInternal(header.readAt(i));
                 }
             }
+
+            // If caller requested that the pointer is set at the end of file,
+            // move both read and write pointer.
+            if (seek_to_end) {
+                fs_->seekp(0, std::ios_base::end);
+                fs_->seekg(0, std::ios_base::end);
+                if (!fs_->good()) {
+                    isc_throw(CSVFileError, "unable to move to the end of"
+                              " CSV file '" << filename_ << "'");
+                }
+                fs_->clear();
+            }
+
         } catch (const std::exception& ex) {
             close();
             throw;

+ 9 - 4
src/lib/util/csv_file.h

@@ -385,12 +385,17 @@ public:
     /// 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.
+    /// By default, 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. If a @c seek_to_end parameter is set to
+    /// true, the file will be opened and the interal pointer will be set
+    /// to the end of file.
+    ///
+    /// @param seek_to_end A boolean value which indicates if the intput and
+    /// output file pointer should be set at the end of file.
     ///
     /// @throw CSVFileError when IO operation fails.
-    void open();
+    void open(const bool seek_to_end = false);
 
     /// @brief Creates a new CSV file.
     ///

+ 308 - 0
src/lib/util/process_spawn.cc

@@ -0,0 +1,308 @@
+// Copyright (C) 2015 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 <exceptions/exceptions.h>
+#include <util/process_spawn.h>
+#include <util/signal_set.h>
+#include <boost/bind.hpp>
+#include <map>
+#include <signal.h>
+#include <stdlib.h>
+#include <sys/wait.h>
+
+namespace isc {
+namespace util {
+
+/// @brief Implementation of the @c ProcessSpawn class.
+///
+/// This pimpl idiom is used by the @c ProcessSpawn in this case to
+/// avoid exposing the internals of the implementation, such as
+/// custom handling of a SIGCHLD signal, and the conversion of the
+/// arguments of the executable from the STL container to the array.
+class ProcessSpawnImpl {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param executable A path to the program to be executed.
+    /// @param args Arguments for the program to be executed.
+    ProcessSpawnImpl(const std::string& executable,
+                     const ProcessArgs& args);
+
+    /// @brief Destructor.
+    ~ProcessSpawnImpl();
+
+    /// @brief Returns full command line, including arguments, for the process.
+    std::string getCommandLine() const;
+
+    /// @brief Spawn the new process.
+    ///
+    /// This method forks the current process and executes the specified
+    /// binary with arguments within the child process.
+    ///
+    /// The child process will return EXIT_FAILURE if the method was unable
+    /// to start the executable, e.g. as a result of insufficient permissions
+    /// or when the executable does not exist. If the process ends successfully
+    /// the EXIT_SUCCESS is returned.
+    ///
+    /// @return PID of the spawned process.
+    /// @throw ProcessSpawnError if forking a current process failed.
+    pid_t spawn();
+
+    /// @brief Checks if the process is still running.
+    ///
+    /// @param pid ID of the child processes for which state should be checked.
+    /// @return true if the child process is running, false otherwise.
+    bool isRunning(const pid_t pid) const;
+
+    /// @brief Checks if any of the spawned processes is still running.
+    ///
+    /// @return true if at least one child process is still running.
+    bool isAnyRunning() const;
+
+    /// @brief Returns exit status of the process.
+    ///
+    /// If the process is still running, the previous status is returned
+    /// or 0, if the process is being ran for the first time.
+    ///
+    /// @param pid ID of the child process for which exit status should be
+    /// returned.
+    /// @return Exit code of the process.
+    int getExitStatus(const pid_t pid) const;
+
+    /// @brief Removes the status of the process with a specified PID.
+    ///
+    /// This method removes the status of the process with a specified PID.
+    /// If the process is still running, the status is not removed and the
+    /// exception is thrown.
+    ///
+    /// @param pid A process pid.
+    void clearStatus(const pid_t pid);
+
+private:
+
+    /// @brief Copies the argument specified as a C++ string to the new
+    /// C string.
+    ///
+    /// This method is used to convert arguments specified as an STL container
+    /// holding @c std::string objects to an array of C strings, used by the
+    /// @c execvp function in the @c ProcessSpawnImpl::spawn. It allocates a
+    /// new C string and copies the contents of the @c src to it.
+    ///
+    /// @param src A source string.
+    ///
+    /// @return Allocated C string holding the data from @c src.
+    char* allocateArg(const std::string& src) const;
+
+    /// @brief Signal handler for SIGCHLD.
+    ///
+    /// This handler waits for the child process to finish and retrieves
+    /// its exit code into the @c status_ member.
+    ///
+    /// @return true if the processed signal was SIGCHLD or false if it
+    /// was a different signal.
+    bool waitForProcess(int signum);
+
+    /// @brief A signal set installing a handler for SIGCHLD.
+    SignalSetPtr signals_;
+
+    /// @brief A map holding the status codes of executed processes.
+    std::map<pid_t, int> process_status_;
+
+    /// @brief Path to an executable.
+    std::string executable_;
+
+    /// @brief An array holding arguments for the executable.
+    char** args_;
+};
+
+ProcessSpawnImpl::ProcessSpawnImpl(const std::string& executable,
+                                   const ProcessArgs& args)
+    : signals_(new SignalSet(SIGCHLD)), process_status_(),
+      executable_(executable), args_(new char*[args.size() + 2]) {
+    // Set the handler which is invoked immediately when the signal
+    // is received.
+    signals_->setOnReceiptHandler(boost::bind(&ProcessSpawnImpl::waitForProcess,
+                                              this, _1));
+    // Convertion of the arguments to the C-style array we start by setting
+    // all pointers within an array to NULL to indicate that they haven't
+    // been allocated yet.
+    memset(args_, 0, (args.size() + 2) * sizeof(char*));
+    // By convention, the first argument points to an executable name.
+    args_[0] = allocateArg(executable_);
+    // Copy arguments to the array.
+    for (int i = 1; i <= args.size(); ++i) {
+        args_[i] = allocateArg(args[i-1]);
+    }
+}
+
+
+ProcessSpawnImpl::~ProcessSpawnImpl() {
+    int i = 0;
+    // Deallocate strings in the array of arguments.
+    while (args_[i] != NULL) {
+        delete[] args_[i];
+        ++i;
+    }
+    // Deallocate the array.
+    delete[] args_;
+}
+
+std::string
+ProcessSpawnImpl::getCommandLine() const {
+    std::ostringstream s;
+    s << executable_;
+    // Start with index 1, because the first argument duplicates the
+    // path to the executable. Note, that even if there are no parameters
+    // the minimum size of the table is 2.
+    int i = 1;
+    while (args_[i] != NULL) {
+        s << " " << args_[i];
+        ++i;
+    }
+    return (s.str());
+}
+
+pid_t
+ProcessSpawnImpl::spawn() {
+    pid_t pid = fork();
+    if (pid < 0) {
+        isc_throw(ProcessSpawnError, "unable to fork current process");
+
+    } else if (pid == 0) {
+        // We're in the child process. Run the executable.
+        if (execvp(executable_.c_str(), args_) != 0) {
+            // We may end up here if the execvp failed, e.g. as a result
+            // of issue with permissions or invalid executable name.
+            exit(EXIT_FAILURE);
+        }
+        // Process finished, exit the child process.
+        exit(EXIT_SUCCESS);
+    }
+
+    process_status_[pid] = 0;
+    return (pid);
+}
+
+bool
+ProcessSpawnImpl::isRunning(const pid_t pid) const {
+    if (process_status_.find(pid) == process_status_.end()) {
+        isc_throw(BadValue, "the process with the pid '" << pid
+                  << "' hasn't been spawned and it status cannot be"
+                  " returned");
+    }
+    return ((pid != 0) && (kill(pid, 0) == 0));
+}
+
+bool
+ProcessSpawnImpl::isAnyRunning() const {
+    for (std::map<pid_t, int>::const_iterator proc = process_status_.begin();
+         proc != process_status_.end(); ++proc) {
+        if (isRunning(proc->first)) {
+            return (true);
+        }
+    }
+    return (false);
+}
+
+int
+ProcessSpawnImpl::getExitStatus(const pid_t pid) const {
+    std::map<pid_t, int>::const_iterator status = process_status_.find(pid);
+    if (status == process_status_.end()) {
+        isc_throw(InvalidOperation, "the process with the pid '" << pid
+                  << "' hasn't been spawned and it status cannot be"
+                  " returned");
+    }
+    return (WEXITSTATUS(status->second));
+}
+
+char*
+ProcessSpawnImpl::allocateArg(const std::string& src) const {
+    const size_t src_len = src.length();
+    // Allocate the C-string with one byte more for the null termination.
+    char* dest = new char[src_len + 1];
+    // copy doesn't append the null at the end.
+    src.copy(dest, src_len);
+    // Append null on our own.
+    dest[src_len] = '\0';
+    return (dest);
+}
+
+bool
+ProcessSpawnImpl::waitForProcess(int signum) {
+    // We're only interested in SIGCHLD.
+    if (signum == SIGCHLD) {
+        int status = 0;
+        pid_t pid = waitpid(-1, &status, 0);
+        if (pid > 0) {
+            /// @todo Check that the terminatin process was started
+            /// by our instance of ProcessSpawn and only handle it
+            /// if it was.
+            process_status_[pid] = status;
+        }
+        return (true);
+    }
+    return (false);
+}
+
+void
+ProcessSpawnImpl::clearStatus(const pid_t pid) {
+    if (isRunning(pid)) {
+        isc_throw(InvalidOperation, "unable to remove the status for the"
+                  "process (pid: " << pid << ") which is still running");
+    }
+    process_status_.erase(pid);
+}
+
+ProcessSpawn::ProcessSpawn(const std::string& executable,
+                           const ProcessArgs& args)
+    : impl_(new ProcessSpawnImpl(executable, args)) {
+}
+
+ProcessSpawn::~ProcessSpawn() {
+    delete impl_;
+}
+
+std::string
+ProcessSpawn::getCommandLine() const {
+    return (impl_->getCommandLine());
+}
+
+pid_t
+ProcessSpawn::spawn() {
+    return (impl_->spawn());
+}
+
+bool
+ProcessSpawn::isRunning(const pid_t pid) const {
+    return (impl_->isRunning(pid));
+}
+
+bool
+ProcessSpawn::isAnyRunning() const {
+    return (impl_->isAnyRunning());
+}
+
+int
+ProcessSpawn::getExitStatus(const pid_t pid) const {
+    return (impl_->getExitStatus(pid));
+}
+
+void
+ProcessSpawn::clearStatus(const pid_t pid) {
+    return (impl_->clearStatus(pid));
+}
+
+}
+}

+ 130 - 0
src/lib/util/process_spawn.h

@@ -0,0 +1,130 @@
+// Copyright (C) 2015 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 PROCESS_SPAWN_H
+#define PROCESS_SPAWN_H
+
+#include <exceptions/exceptions.h>
+#include <string>
+#include <sys/types.h>
+#include <vector>
+
+namespace isc {
+namespace util {
+
+/// @brief Exception thrown when error occurs during spawning a process.
+class ProcessSpawnError : public Exception {
+public:
+    ProcessSpawnError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief Forward declaration to the implementation of the @c ProcessSpawn
+/// class.
+class ProcessSpawnImpl;
+
+/// @brief Type of the container holding arguments of the executable
+/// being run as a background process.
+typedef std::vector<std::string> ProcessArgs;
+
+/// @brief Utility class for spawning new processes.
+///
+/// This class is used to spawn new process by Kea. It forks the current
+/// process and then uses the @c execvp function to execute the specified
+/// binary with parameters. The @c ProcessSpawn installs the handler for
+/// the SIGCHLD signal, which is executed when the child process ends.
+/// The handler checks the exit code returned by the process and records
+/// it. The exit code can be retrieved by the caller using the
+/// @c ProcessSpawn::getExitStatus method.
+///
+/// @warning Only one instance of the @c ProcessSpawn class may exist
+/// at the given time. Creating additional instance would cause an
+/// attempt to register a new SIGCHLD signal handler and, as a
+/// consequence, the new @c ProcessSpawn object will fail to create.
+///
+/// @todo The SIGCHLD handling logic should be moved to the @c SignalSet
+/// class so as multiple instances of the @c ProcessSpawn use the same
+/// SIGCHLD signal handler.
+class ProcessSpawn {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param executable A path to the program to be executed.
+    /// @param args Arguments for the program to be executed.
+    ProcessSpawn(const std::string& executable,
+                 const ProcessArgs& args = ProcessArgs());
+
+    /// @brief Destructor.
+    ~ProcessSpawn();
+
+    /// @brief Returns full command line, including arguments, for the process.
+    std::string getCommandLine() const;
+
+    /// @brief Spawn the new process.
+    ///
+    /// This method forks the current process and execues the specified
+    /// binary with arguments within the child process.
+    ///
+    /// The child process will return EXIT_FAILURE if the method was unable
+    /// to start the exuctable, e.g. as a result of insufficient permissions
+    /// or when the executable does not exist. If the process ends successfully
+    /// the EXIT_SUCCESS is returned.
+    ///
+    /// @throw ProcessSpawnError if forking a current process failed.
+    pid_t spawn();
+
+    /// @brief Checks if the process is still running.
+    ///
+    /// @param pid ID of the child processes for which state should be checked.
+    ///
+    /// @return true if the child process is running, false otherwise.
+    bool isRunning(const pid_t pid) const;
+
+    /// @brief Checks if any of the spawned processes is still running.
+    ///
+    /// @return true if at least one child process is still running.
+    bool isAnyRunning() const;
+
+    /// @brief Returns exit status of the process.
+    ///
+    /// If the process is still running, the previous status is returned
+    /// or 0, if the process is being ran for the first time.
+    ///
+    /// @param pid ID of the child process for which exit status should be
+    /// returned.
+    ///
+    /// @return Exit code of the process.
+    int getExitStatus(const pid_t pid) const;
+
+    /// @brief Removes the status of the process with a specified PID.
+    ///
+    /// This method removes the status of the process with a specified PID.
+    /// If the process is still running, the status is not removed and the
+    /// exception is thrown.
+    ///
+    /// @param pid A process pid.
+    void clearStatus(const pid_t pid);
+
+private:
+
+    /// @brief A pointer to the implementation of this class.
+    ProcessSpawnImpl* impl_;
+
+};
+
+}
+}
+
+#endif // PROCESS_SPAWN_H

+ 1 - 0
src/lib/util/tests/.gitignore

@@ -1 +1,2 @@
 /run_unittests
+/process_spawn_app.sh

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

@@ -17,6 +17,8 @@ CLEANFILES = *.gcno *.gcda
 # CSV files are created by unit tests for CSVFile class.
 CLEANFILES += *.csv
 
+noinst_SCRIPTS = process_spawn_app.sh
+
 TESTS_ENVIRONMENT = \
         $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
 
@@ -39,6 +41,7 @@ run_unittests_SOURCES += memory_segment_common_unittest.h
 run_unittests_SOURCES += memory_segment_common_unittest.cc
 run_unittests_SOURCES += optional_value_unittest.cc
 run_unittests_SOURCES += pid_file_unittest.cc
+run_unittests_SOURCES += process_spawn_unittest.cc
 run_unittests_SOURCES += qid_gen_unittest.cc
 run_unittests_SOURCES += random_number_generator_unittest.cc
 run_unittests_SOURCES += socketsession_unittest.cc

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

@@ -303,6 +303,30 @@ TEST_F(CSVFileTest, openReadAllWrite) {
     // 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);
+
+    CSVRow row_write2(3);
+    row_write2.writeAt(0, "bird");
+    row_write2.writeAt(1, 3);
+    row_write2.writeAt(2, "purple");
+
+    // Reopen the file, seek to the end of file so as we can append
+    // some more data.
+    ASSERT_NO_THROW(csv->open(true));
+    // The file pointer should be at the end of file, so an attempt
+    //  to read should result in an empty row.
+    ASSERT_TRUE(csv->next(row));
+    EXPECT_EQ(CSVFile::EMPTY_ROW(), row);
+    // We should be able to append new data.
+    ASSERT_NO_THROW(csv->append(row_write2));
+    ASSERT_NO_THROW(csv->flush());
+    csv->close();
+    // Check that new data has been appended.
+    EXPECT_EQ("animal,age,color\n"
+              "cat,10,white\n"
+              "lion,15,yellow\n"
+              "dog,2,blue\n"
+              "bird,3,purple\n",
+              readFile());
 }
 
 // This test checks that contents may be appended to a file which hasn't

+ 65 - 0
src/lib/util/tests/process_spawn_app.sh.in

@@ -0,0 +1,65 @@
+#!/bin/sh
+#
+# Copyright (C) 2015 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.
+
+
+# This script is used for testing the ProcessSpawn utility class. This
+# class is used to fork and execute a new process. It also allows for
+# checking the exit code returned when the process terminates.
+# The unit tests execute this script via ProcessSpawn class with
+# different command line parameters to test the class functionality.
+#
+# In particular, they check if the class correctly records the exit code
+# returned. The exit code returned is controlled by the caller. It is
+# possible to explictily specify the exit code to be returned using
+# the command line options. It is also possible to specify that the
+# exit code is "unique" for the process, so as the test can check
+# that two distinct processes spawned by the same ProcessSpawn
+# object may return different status code. The command line of this
+# script also allows for forcing the process to sleep so as the
+# test has much enough time to verify that the convenience methods
+# checking the state of the process, i.e. process running or not.
+
+exit_code=
+
+while [ ! -z "${1}" ]
+do
+    option=${1}
+    case ${option} in
+        -p)
+            exit_code=$$
+            ;;
+        -e)
+            shift
+            exit_code=${1}
+            ;;
+        -s)
+            shift
+            sleep ${1}
+            ;;
+        *)
+            exit 123
+            ;;
+    esac
+    shift
+done
+
+# The exit code of 32 is returned when no args specified or
+# when only the -s arg has been specified.
+if [ -z "${exit_code}" ]; then
+    exit 32;
+fi
+
+exit ${exit_code}

+ 172 - 0
src/lib/util/tests/process_spawn_unittest.cc

@@ -0,0 +1,172 @@
+// Copyright (C) 2015 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/process_spawn.h>
+#include <gtest/gtest.h>
+#include <signal.h>
+#include <stdint.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+namespace {
+
+using namespace isc;
+using namespace isc::util;
+
+/// @brief Returns a location of the test script.
+///
+/// The test script is no-op and it returns the exit code equal to
+/// the argument passed to it.
+///
+/// @return Absolute location of the test script.
+std::string getApp() {
+    std::ostringstream s;
+    s << TEST_DATA_TOPBUILDDIR << "/src/lib/util/tests/process_spawn_app.sh";
+    return (s.str());
+}
+
+/// @brief Waits for the specified process to finish.
+///
+/// @param process An object which started the process.
+/// @param pid ID of the spawned process.
+/// @param timeout Timeout in seconds.
+///
+/// @return true if the process ended, false otherwise
+bool waitForProcess(const ProcessSpawn& process, const pid_t pid,
+                    const uint8_t timeout) {
+    uint32_t iterations = 0;
+    const uint32_t iterations_max = timeout * 1000;
+    while (process.isRunning(pid) && (iterations < iterations_max)) {
+        usleep(1000);
+        ++iterations;
+    }
+    return (iterations < iterations_max);
+}
+
+// This test verifies that the external application can be ran with
+// arguments and that the exit code is gathered.
+TEST(ProcessSpawn, spawnWithArgs) {
+    std::vector<std::string> args;
+    args.push_back("-e");
+    args.push_back("64");
+    ProcessSpawn process(getApp(), args);
+    pid_t pid = 0;
+    ASSERT_NO_THROW(pid = process.spawn());
+
+    ASSERT_TRUE(waitForProcess(process, pid, 2));
+
+    EXPECT_EQ(64, process.getExitStatus(pid));
+}
+
+// This test verifies that the single ProcessSpawn object can be used
+// to start two processes and that their status codes can be gathered.
+// It also checks that it is possible to clear the status of the
+// process.
+TEST(ProcessSpawn, spawnTwoProcesses) {
+    std::vector<std::string> args;
+    args.push_back("-p");
+    ProcessSpawn process(getApp(), args);
+    pid_t pid1 = 0;
+    ASSERT_NO_THROW(pid1 = process.spawn());
+    ASSERT_TRUE(waitForProcess(process, pid1, 2));
+
+    pid_t pid2 = 0;
+    ASSERT_NO_THROW(pid2 = process.spawn());
+    ASSERT_TRUE(waitForProcess(process, pid2, 2));
+
+    EXPECT_NE(process.getExitStatus(pid1), process.getExitStatus(pid2));
+
+    // Clear the status of the first process. An atttempt to get the status
+    // for the cleared process should result in exception. But, there should
+    // be no exception for the second process.
+    process.clearStatus(pid1);
+    EXPECT_THROW(process.getExitStatus(pid1), InvalidOperation);
+    EXPECT_NO_THROW(process.getExitStatus(pid2));
+
+    process.clearStatus(pid2);
+    EXPECT_THROW(process.getExitStatus(pid2), InvalidOperation);
+}
+
+// This test verifies that the external application can be ran without
+// arguments and that the exit code is gathered.
+TEST(ProcessSpawn, spawnNoArgs) {
+    std::vector<std::string> args;
+    ProcessSpawn process(getApp());
+    pid_t pid = 0;
+    ASSERT_NO_THROW(pid = process.spawn());
+
+    ASSERT_TRUE(waitForProcess(process, pid, 2));
+
+    EXPECT_EQ(32, process.getExitStatus(pid));
+}
+
+
+// This test verifies that the EXIT_FAILURE code is returned when
+// application can't be executed.
+TEST(ProcessSpawn, invalidExecutable) {
+    ProcessSpawn process("foo");
+    pid_t pid = 0;
+    ASSERT_NO_THROW(pid = process.spawn());
+
+    ASSERT_TRUE(waitForProcess(process, pid, 2));
+
+    EXPECT_EQ(EXIT_FAILURE, process.getExitStatus(pid));
+}
+
+// This test verifies that the full command line for the process is
+// returned.
+TEST(ProcessSpawn, getCommandLine) {
+    // Note that cases below are enclosed in separate scopes to make
+    // sure that the ProcessSpawn object is destructed before a new
+    // object is created. Current implementation doesn't allow for
+    // having two ProcessSpawn objects simulatneously as they will
+    // both try to allocate a signal handler for SIGCHLD.
+    {
+        // Case 1: arguments present.
+        ProcessArgs args;
+        args.push_back("-x");
+        args.push_back("-y");
+        args.push_back("foo");
+        args.push_back("bar");
+        ProcessSpawn process("myapp", args);
+        EXPECT_EQ("myapp -x -y foo bar", process.getCommandLine());
+    }
+
+    {
+        // Case 2: no arguments.
+        ProcessSpawn process("myapp");
+        EXPECT_EQ("myapp", process.getCommandLine());
+    }
+}
+
+// This test verifies that it is possible to check if the process is
+// running.
+TEST(ProcessSpawn, isRunning) {
+    // Run the process which sleeps for 10 seconds, so as we have
+    // enough time to check if it is running.
+    std::vector<std::string> args;
+    args.push_back("-s");
+    args.push_back("10");
+    ProcessSpawn process(getApp(), args);
+    pid_t pid = 0;
+    ASSERT_NO_THROW(pid = process.spawn());
+    EXPECT_TRUE(process.isRunning(pid));
+
+    // Kill the process.
+    ASSERT_EQ(0, kill(pid, SIGKILL));
+    // And make sure if died.
+    ASSERT_TRUE(waitForProcess(process, pid, 2));
+}
+
+} // end of anonymous namespace