Browse Source

[3086] Initial implementation of NameChangeTransaction class

Interim commit for 3086 which includes the preliminary implementation
of the base class, NameChangeTransaction.  b10-dhcp-ddns module will use
derivations of this class to carry out NameChangeRequests.
Thomas Markwalder 11 years ago
parent
commit
f3c1bcd7be

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

@@ -59,6 +59,7 @@ b10_dhcp_ddns_SOURCES += d2_update_message.cc d2_update_message.h
 b10_dhcp_ddns_SOURCES += d2_update_mgr.cc d2_update_mgr.h
 b10_dhcp_ddns_SOURCES += d2_zone.cc d2_zone.h
 b10_dhcp_ddns_SOURCES += dns_client.cc dns_client.h
+b10_dhcp_ddns_SOURCES += nc_trans.cc nc_trans.h
 
 nodist_b10_dhcp_ddns_SOURCES = d2_messages.h d2_messages.cc
 EXTRA_DIST += d2_messages.mes

+ 6 - 0
src/bin/d2/d2_messages.mes

@@ -252,3 +252,9 @@ in event loop.
 % DHCP_DDNS_SHUTDOWN application received shutdown command with args: %1
 This is informational message issued when the application has been instructed
 to shut down by the controller.
+
+% DHCP_DDNS_TRANS_PROCESS_EROR application encountered an unexpected error while carrying out a NameChangeRequest: %1
+This is error message issued when the application fails to process a 
+NameChangeRequest correctly. Some or all of the DNS updates requested as part
+of this update did not succeed. This is a programmatic error and should be
+reported. 

+ 6 - 0
src/bin/d2/d2_update_mgr.cc

@@ -108,6 +108,12 @@ void D2UpdateMgr::pickNextJob() {
         if (!hasTransaction(found_ncr->getDhcid())) {
             queue_mgr_->dequeueAt(index);
             makeTransaction(found_ncr);
+
+#if 0
+            // this will run it up to its first IO
+            trans->startTransaction();
+#endif
+
             return;
         }
     }

+ 1 - 49
src/bin/d2/d2_update_mgr.h

@@ -22,6 +22,7 @@
 #include <d2/d2_log.h>
 #include <d2/d2_queue_mgr.h>
 #include <d2/d2_cfg_mgr.h>
+#include <d2/nc_trans.h>
 
 #include <boost/noncopyable.hpp>
 #include <boost/shared_ptr.hpp>
@@ -37,58 +38,9 @@ public:
         isc::Exception(file, line, what) { };
 };
 
-//@{
-/// @todo  This is a stub implementation of NameChangeTransaction that is here
-/// strictly to facilitate development of D2UpdateMgr. It will move to its own
-/// source file(s) once NameChangeTransaction class development begins.
-
-/// @brief Defines the key for transactions.
-typedef isc::dhcp_ddns::D2Dhcid TransactionKey;
-
-class NameChangeTransaction {
-public:
-    NameChangeTransaction(isc::asiolink::IOService& io_service,
-                          dhcp_ddns::NameChangeRequestPtr& ncr,
-                          DdnsDomainPtr forward_domain,
-                          DdnsDomainPtr reverse_domain)
-    : io_service_(io_service), ncr_(ncr), forward_domain_(forward_domain),
-      reverse_domain_(reverse_domain) {
-    }
-
-    ~NameChangeTransaction(){
-    }
-
-    const dhcp_ddns::NameChangeRequestPtr& getNcr() const {
-        return (ncr_);
-    }
-
-    const TransactionKey& getTransactionKey() const {
-        return (ncr_->getDhcid());
-    }
-
-    dhcp_ddns::NameChangeStatus getNcrStatus() const {
-        return (ncr_->getStatus());
-    }
-
-private:
-    isc::asiolink::IOService& io_service_;
-
-    dhcp_ddns::NameChangeRequestPtr ncr_;
-
-    DdnsDomainPtr forward_domain_;
-
-    DdnsDomainPtr reverse_domain_;
-};
-
-/// @brief Defines a pointer to a NameChangeTransaction.
-typedef boost::shared_ptr<NameChangeTransaction> NameChangeTransactionPtr;
-
-//@}
-
 /// @brief Defines a list of transactions.
 typedef std::map<TransactionKey, NameChangeTransactionPtr> TransactionList;
 
-
 /// @brief D2UpdateMgr creates and manages update transactions.
 ///
 /// D2UpdateMgr is the DHCP_DDNS task master, instantiating and then supervising

+ 244 - 0
src/bin/d2/nc_trans.cc

@@ -0,0 +1,244 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_log.h>
+#include <d2/nc_trans.h>
+
+namespace isc {
+namespace d2 {
+
+// Common transaction states
+const int NameChangeTransaction::NEW_ST;
+const int NameChangeTransaction::READY_ST;
+const int NameChangeTransaction::SELECTING_FWD_SERVER_ST;
+const int NameChangeTransaction::SELECTING_REV_SERVER_ST;
+const int NameChangeTransaction::DONE_ST;
+
+const int NameChangeTransaction::DERIVED_STATES;
+
+// Common transaction events
+const int NameChangeTransaction::NOP_EVT;
+const int NameChangeTransaction::START_TRANSACTION_EVT;
+const int NameChangeTransaction::SELECT_SERVER_EVT;
+const int NameChangeTransaction::SERVER_SELECTED_EVT;
+const int NameChangeTransaction::SERVER_IO_ERROR_EVT;
+const int NameChangeTransaction::NO_MORE_SERVERS_EVT;
+const int NameChangeTransaction::IO_COMPLETED_EVT;
+const int NameChangeTransaction::UPDATE_OK_EVT;
+const int NameChangeTransaction::UPDATE_FAILED_EVT;
+const int NameChangeTransaction::CANCEL_TRANSACTION_EVT;
+const int NameChangeTransaction::ALL_DONE_EVT;
+
+const int NameChangeTransaction::DERIVED_EVENTS;
+
+NameChangeTransaction::
+NameChangeTransaction(isc::asiolink::IOService& io_service,
+                      dhcp_ddns::NameChangeRequestPtr& ncr,
+                      DdnsDomainPtr forward_domain,
+                      DdnsDomainPtr reverse_domain)
+    : state_handlers_(), io_service_(io_service), ncr_(ncr),
+     forward_domain_(forward_domain), reverse_domain_(reverse_domain),
+     dns_client_(), state_(NEW_ST), next_event_(NOP_EVT),
+     dns_update_status_(DNSClient::OTHER), dns_update_response_(),
+     forward_change_completed_(false), reverse_change_completed_(false) {
+    if (!ncr_) {
+        isc_throw(NameChangeTransactionError, "NameChangeRequest cannot null");
+    }
+
+    if (ncr_->isForwardChange() && !(forward_domain_)) {
+        isc_throw(NameChangeTransactionError,
+                 "Forward change must have a forward domain");
+    }
+
+    if (ncr_->isReverseChange() && !(reverse_domain_)) {
+        isc_throw(NameChangeTransactionError,
+                 "Reverse change must have a reverse domain");
+    }
+    
+    // Use setters here so we get proper values for previous state, last event.
+    setState(state_);
+    setNextEvent(NOP_EVT);
+}
+
+NameChangeTransaction::~NameChangeTransaction(){
+}
+
+void
+NameChangeTransaction::startTransaction() {
+    // Initialize the state handler map first.
+    initStateHandlerMap();
+
+    // Set the current state to READY and enter the run loop.
+    setState(READY_ST);
+    runStateModel(START_TRANSACTION_EVT);
+}
+
+void
+NameChangeTransaction::cancelTransaction() {
+    //@todo It is up to the deriving state model to handle this event.
+    runStateModel(CANCEL_TRANSACTION_EVT);
+}
+
+void
+NameChangeTransaction::operator()(DNSClient::Status status) {
+    // Stow the completion status and re-enter the run loop with the event
+    // set to indicate IO completed.
+    // runStateModel is exception safe so we are good to call it here.  
+    // It won't exit until we hit the next IO wait or the state model ends.
+    setDnsUpdateStatus(status);
+    runStateModel(IO_COMPLETED_EVT);
+}
+
+void
+NameChangeTransaction::runStateModel(unsigned int run_event) {
+    try {
+        // Seed the loop with the given event as the next to process.
+        setNextEvent(run_event);
+        do {
+            // Invoke the current state's handler.  It should consume the 
+            // next event, then determine what happens next by setting
+            // current state and/or the next event.  
+            (getStateHandler(state_))();
+
+            // Keep going until a handler sets next event to a NOP_EVT. 
+        } while (getNextEvent() != NOP_EVT);
+    }
+    catch (const std::exception& ex) {
+        // Transaction has suffered an unexpected exception.  This indicates
+        // a programmatic shortcoming.  Log it and set status to ST_FAILED.
+        // In theory, the model should account for all error scenarios and
+        // deal with them accordingly.
+        LOG_ERROR(dctl_logger, DHCP_DDNS_TRANS_PROCESS_EROR).arg(ex.what());
+        setNcrStatus(dhcp_ddns::ST_FAILED);
+    }
+}
+
+
+StateHandler
+NameChangeTransaction::getStateHandler(unsigned int state) {
+    StateHandlerMap::iterator it = state_handlers_.find(state);
+    if (it == state_handlers_.end()) {
+        isc_throw(NameChangeTransactionError, "Invalid state: " << state);
+    }
+
+    return ((*it).second);
+}
+
+void
+NameChangeTransaction::addToMap(unsigned int state, StateHandler handler) {
+    StateHandlerMap::iterator it = state_handlers_.find(state);
+    if (it != state_handlers_.end()) {
+        isc_throw(NameChangeTransactionError,
+                  "Attempted duplicate entry in state handler mape, state: "
+                   << state);
+    }
+
+    state_handlers_[state] = handler;
+}
+
+void
+NameChangeTransaction::setState(unsigned int state) {
+    prev_state_ = state_;
+    state_ = state;
+}
+
+void
+NameChangeTransaction::setNextEvent(unsigned int event) {
+    last_event_ = next_event_;
+    next_event_ = event;
+}
+
+void
+NameChangeTransaction::setDnsUpdateStatus(const DNSClient::Status& status) {
+    dns_update_status_ = status;
+}
+
+void
+NameChangeTransaction::setDnsUpdateResponse(D2UpdateMessagePtr& response) {
+    dns_update_response_ = response;
+}
+
+void
+NameChangeTransaction::setForwardChangeCompleted(const bool value) {
+    forward_change_completed_ = value;
+}
+
+void
+NameChangeTransaction::setReverseChangeCompleted(const bool value) {
+    reverse_change_completed_ = value;
+}
+
+const dhcp_ddns::NameChangeRequestPtr& 
+NameChangeTransaction::getNcr() const {
+    return (ncr_);
+}
+
+const TransactionKey& 
+NameChangeTransaction::getTransactionKey() const {
+    return (ncr_->getDhcid());
+}
+
+dhcp_ddns::NameChangeStatus
+NameChangeTransaction::getNcrStatus() const {
+    return (ncr_->getStatus());
+}
+
+void
+NameChangeTransaction::setNcrStatus(const dhcp_ddns::NameChangeStatus& status) {
+    return (ncr_->setStatus(status));
+}
+
+unsigned int
+NameChangeTransaction::getState() const {
+    return (state_);
+}
+
+unsigned int
+NameChangeTransaction::getPrevState() const {
+    return (prev_state_);
+}
+
+unsigned int
+NameChangeTransaction::getLastEvent() const {
+    return (last_event_);
+}
+
+unsigned int
+NameChangeTransaction::getNextEvent() const {
+    return (next_event_);
+}
+
+DNSClient::Status
+NameChangeTransaction::getDnsUpdateStatus() const {
+    return (dns_update_status_);
+}
+
+const D2UpdateMessagePtr&
+NameChangeTransaction::getDnsUpdateResponse() const {
+    return (dns_update_response_);
+}
+
+bool
+NameChangeTransaction::getForwardChangeCompleted() const {
+    return (forward_change_completed_);
+}
+
+bool
+NameChangeTransaction::getReverseChangeCompleted() const {
+    return (reverse_change_completed_);
+}
+
+
+} // namespace isc::d2
+} // namespace isc

+ 478 - 0
src/bin/d2/nc_trans.h

@@ -0,0 +1,478 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef NC_TRANS_H
+#define NC_TRANS_H
+
+/// @file nc_trans.h This file defines the class NameChangeTransaction.
+
+#include <asiolink/io_service.h>
+#include <exceptions/exceptions.h>
+#include <d2/d2_config.h>
+#include <d2/dns_client.h>
+#include <dhcp_ddns/ncr_msg.h>
+
+#include <boost/shared_ptr.hpp>
+#include <map>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Thrown if the update manager encounters a general error.
+class NameChangeTransactionError : public isc::Exception {
+public:
+    NameChangeTransactionError(const char* file, size_t line,
+                               const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief Defines the type used as the unique key for transactions.
+typedef isc::dhcp_ddns::D2Dhcid TransactionKey;
+
+/// @brief Defines a function pointer for the handler method for a state.
+typedef boost::function<void()> StateHandler;
+
+/// @brief Defines a map of states to their handler methods.
+typedef std::map<unsigned int, StateHandler> StateHandlerMap;
+
+/// @brief Embodies the "life-cycle" required to carry out a DDNS update.
+///
+/// NameChangeTransaction is the base class that provides the common state
+/// model mechanics and services performing the DNS updates needed to carry out
+/// a DHCP_DDNS request as described by a NameChangeRequest.
+///
+/// Upon construction, each transaction has all of the information and
+/// resources required to carry out its assigned request, including the list(s)
+/// of DNS server(s) needed. It is responsible for knowing what conversations
+/// it must have with which servers and in the order necessary to fulfill the
+/// request. Upon fulfillment of the request, the transaction's work is complete
+/// it is destroyed.
+///
+/// Fulfillment of the request is carried out through the performance of the
+/// transaction's state model.  Using a state driven implementation accounts
+/// for the conditional processing flow necessary to meet the DDNS RFCs as well
+/// as the asynchronous nature of IO with DNS servers.
+///
+/// Derivations of the class are responsible for defining the state model and
+/// conversations necessary to carry out the specific of request.
+///
+/// Conversations with DNS servers are done through the use of the DNSClient
+/// class.  The DNSClient provides a IOService-based means a service which
+/// performs a single, packet exchange with a given DNS server.  It sends a
+/// single update to the server and returns the response, asynchronously,
+/// through a callback.  At each point in a transaction's state model, where
+/// an update is to be sent, the model "suspends" until notified by the
+/// DNSClient via the callback.
+///
+/// The state model implementation used is a very basic approach. States
+/// and events are simple integer constants. Each state must have a state
+/// handler. State handlers are void methods which accept an event as their
+/// only parameter.  Each transaction instance contains a map of states to
+/// to bound method pointers to their respective state handlers.
+///
+/// When invoked, the handler determines what it should do based upon the event,
+/// including what the next state and event should be. In other words the state
+/// transition knowledge is distributed among the state handlers rather than
+/// encapsulated in some form of state transition table.  Events set from within
+/// the state handlers are "internally" triggered events.  Events set from
+/// outside the state model, such as through the DNSClient completion callback
+/// are "externally" triggered.
+///
+/// Executing the model consists of iteratively invoking the state handler
+/// indicated by the current state and passing it the current event.  As the
+/// handlers update the state and event, the machine is traversed. The loop
+/// "stops" whenever the model cannot continue without an externally triggered
+/// event or when it has reached its final state.  In the case of the former,
+/// the loop is re-entered upon arrival of the external event.
+///
+/// This loop is implemented in the runStateModel method.  This method accepts
+/// an event as argument.  This event is treated as the "next event" to process
+/// and is fed to the current state's handler.  The runStateModel does not exit
+/// until a handler sets the next event to a special value, NOP_EVT,
+/// indicating that either it is now waiting for IO to complete of the state
+/// model has reached its conclusion.
+///
+/// Re-entering the "loop" when a DNS update completes is done by a call to
+/// runStateModel() from within the DNSClient callback, with an event value
+/// of IO_COMPLETED_EVT.  As above, runStateModel() will loop until either the
+/// next IO is issued or the state model has reached its conclusion.
+///
+/// This class defines a set of events and states that are a common to all
+/// transactions. Each derivation may add define additional states and events
+/// as needed, but it must support the common set.  NameChangeTransaction
+/// does not supply any state handlers.  These are the sole responsibility of
+/// derivations.
+class NameChangeTransaction : public DNSClient::Callback {
+public:
+
+    //@{ States common to all transactions.
+    /// @brief State a transaction is in immediately after construction.
+    static const int NEW_ST = 0;
+    /// @brief State from which a transaction is started.
+    static const int READY_ST = 1;
+    /// @brief State in which forward DNS server  selection is done.
+    static const int SELECTING_FWD_SERVER_ST = 2;
+    /// @brief State in which reverse DNS server  selection is done.
+    static const int SELECTING_REV_SERVER_ST = 3;
+    /// @brief Final state, all work has been performed.
+    static const int DONE_ST = 4;
+
+    /// @define Value at which custom states in a derived class should begin.
+    static const int DERIVED_STATES = 100;
+    //@}
+
+
+    //@{ Events common to all transactions.
+    /// @brief Signifies that no event has occurred.
+    /// This is event used to interrupt the event loop to allow waiting for
+    /// an IO event or when there is no more work to be done.
+    static const int NOP_EVT = 0;
+    /// @brief Event used to start the transaction.
+    static const int START_TRANSACTION_EVT = 1;
+    /// @brief Issued when a server needs to be selected.
+    static const int SELECT_SERVER_EVT = 2;
+    /// @brief Issued when a server  has been selected.
+    static const int SERVER_SELECTED_EVT = 3;
+    /// @brief Issued when an update fails due to an IO error.
+    static const int SERVER_IO_ERROR_EVT = 4;
+    /// @brief Issued when there are no more servers from which to select.
+    /// This occurs when none of the servers in the list can be reached to
+    /// perform the update.
+    static const int NO_MORE_SERVERS_EVT = 5;
+    /// @brief Issued when a DNS update packet exchange has completed.
+    /// This occurs whenever the DNSClient callback is invoked whether the
+    /// exchange was successful or not.
+    static const int IO_COMPLETED_EVT = 6;
+    /// @brief Issued when the attempted update successfully completed.
+    /// This occurs when an DNS update packet was successfully processed
+    /// by the server.
+    static const int UPDATE_OK_EVT = 7;
+    /// @brief Issued when the attempted update fails to complete.
+    /// This occurs when an DNS update packet fails to process. The nature of
+    /// the failure is given by the DNSClient return status and the response
+    /// packet (if one was received).
+    static const int UPDATE_FAILED_EVT = 8;
+    /// @brief Issued when the transaction should be cancelled.
+    /// @todo - still on the fence about this one.
+    static const int CANCEL_TRANSACTION_EVT = 9;
+    /// @brief Issued when the state model has no more work left to do.
+    static const int ALL_DONE_EVT = 10;
+
+    /// @define Value at which custom events in a derived class should begin.
+    static const int DERIVED_EVENTS = 100;
+    //@}
+
+    /// @brief Constructor
+    ///
+    /// Instantiates a transaction that is ready to be started.
+    ///
+    /// @param io_service IO service to be used for IO processing
+    /// @param ncr is the NameChangeRequest to fulfill
+    /// @param forward_domain is the domain to use for forward DNS updates
+    /// @param reverse_domain is the domain to use for reverse DNS updates
+    ///
+    /// @throw NameChangeTransaction error if given an null request,
+    /// if forward change is enabled but forward domain is null, if
+    /// reverse change is enabled but reverse domain is null.
+    NameChangeTransaction(isc::asiolink::IOService& io_service,
+                          dhcp_ddns::NameChangeRequestPtr& ncr,
+                          DdnsDomainPtr forward_domain,
+                          DdnsDomainPtr reverse_domain);
+
+    virtual ~NameChangeTransaction();
+
+    /// @brief Begins execution of the transaction.
+    ///
+    /// This method invokes initHandlersMap() to initialize the map of state
+    /// handlers. It then starts the transaction's state model by setting the
+    /// current state to READY_ST and invoking runStateModel() with an event
+    /// parameter of START_TRANSACTION_EVT.
+    void startTransaction();
+
+    /// @todo - Not sure about this yet.
+    void cancelTransaction();
+
+    /// @brief Serves as the DNSClient IO completion event handler.
+    ///
+    /// This is the implementation of the method inherited by our derivation
+    /// from DNSClient::Callback.  When the DNSClient completes an update it
+    /// invokes this method as the completion handler.  This method stores
+    /// the given status and invokes runStateModel() with an event value of
+    /// IO_COMPLETED_EVT.
+    ///
+    /// @param status is the outcome of the DNS update packet exchange.
+    /// This method is exception safe.
+    virtual void operator()(DNSClient::Status status);
+
+protected:
+    /// @brief Populates the map of state handlers.
+    ///
+    /// This method is used by derivations to construct a map of states to
+    /// their appropriate state handlers (bound method pointers).  It is
+    /// invoked at the beginning of startTransaction().
+    ///
+    /// Implementations should use the addToMap() method add entries to
+    /// the map.
+    /// @todo This method should be pure virtual but until there are
+    /// derivations for the update manager to use we will provide an
+    /// temporary empty, implementation.  If we make it pure virtual now
+    /// D2UpdateManager will not compile.
+    virtual void initStateHandlerMap() {};
+
+    /// @brief Adds an entry to the state handler map.
+    ///
+    /// This method attempts to add an entry to the handler map which maps
+    /// the given handler to the given state.  The state handler must be
+    /// a bound member pointer to a handler method of the transaction instance.
+    /// The following code snippet shows an example derivation and call to
+    /// addToMap() within its initStateHandlerMap() method.
+    ///
+    /// @code
+    /// class ExampleTrans : public NameChangeTransaction {
+    /// public:
+    /// :
+    /// void readyHandler() {
+    /// }
+    ///
+    /// void initStateHandlerMap() {
+    ///     addToMap(READY_ST,
+    ///        boost::bind(&ExampleTrans::readyHandler, this));
+    ///     :
+    ///
+    /// @endcode
+    ///
+    /// @param state the value of the state to which to map
+    /// @param handler the bound method pointer to the handler for the state
+    ///
+    /// @throw NameChangeTransactionError if the map already contains an entry
+    /// for the given state.
+    void addToMap(unsigned int idx, StateHandler handler);
+
+    /// @brief Processes events through the state model
+    ///
+    /// This method implements the state model "execution loop".  It uses
+    /// the given event as the next event to process and begins looping by
+    /// passing it the state handler for the current state.   As described
+    /// above, the invoked state handler determines the current state and the
+    /// next event required to implement the business logic. The method
+    /// continues to loop until next event is set to NOP_EVT, at which point
+    /// the method exits.
+    ///
+    /// Any exception thrown during the loop is caught, logged, and the
+    /// transaction is immediately set to failed status.  The state model is
+    /// expected to account for any possible errors so any that escape are
+    /// treated as unrecoverable in terms of the current transaction.
+    ///
+    /// @param event is the next event to process
+    ///
+    /// This is guaranteed not to throw.
+    void runStateModel(unsigned int event);
+
+    /// @brief Return the state handler for a given state.
+    ///
+    /// This method looks up the state handler for the given state from within
+    /// the state handler map.
+    ///
+    /// @param state is the state constant of the desired handler.
+    ///
+    /// @return A StateHandler (bound method pointer) for the method that
+    /// handles the given state for this transaction.
+    ///
+    /// @throw NameChangeTransactionError
+    StateHandler getStateHandler(unsigned int state);
+
+    /// @brief Sets the current state to the given state value.
+    ///
+    /// This updates the transaction's notion of the current state and is the
+    /// state whose handler will be executed on the next iteration of the run
+    /// loop.
+    ///
+    /// @param state the new value to assign to the current state.
+    void setState(unsigned int state);
+
+    /// @brief Sets the next event to the given event value.
+    ///
+    /// This updates the transaction's notion of the next event and is the
+    /// event that will be passed into the current state's handler on the next
+    /// iteration of the run loop.
+    ///
+    /// @param state the new value to assign to the current state.
+    void setNextEvent(unsigned int event);
+
+    /// @brief Sets the update status to the given status value.
+    ///
+    /// @param status is the new value for the update status.
+    void setDnsUpdateStatus(const DNSClient::Status& status);
+
+    /// @brief Sets the update response packet to the given packet.
+    ///
+    /// @param response is the new response packet to assign.
+    void setDnsUpdateResponse(D2UpdateMessagePtr& response);
+
+    /// @brief Sets the forward change completion flag to the given value.
+    ///
+    /// @param value is the new value to assign to the flag.
+    void setForwardChangeCompleted(const bool value);
+
+    /// @brief Sets the reverse change completion flag to the given value.
+    ///
+    /// @param value is the new value to assign to the flag.
+    void setReverseChangeCompleted(const bool value);
+
+public:
+    /// @brief Fetches the NameChangeRequest for this transaction.
+    ///
+    /// @return A const pointer reference to the NameChangeRequest.
+    const dhcp_ddns::NameChangeRequestPtr& getNcr() const;
+
+    /// @brief Fetches the unique key that identifies this transaction.
+    ///
+    /// Transactions are uniquely identified by a TransactionKey. Currently
+    /// this is wrapper around a D2Dhcid.
+    ///
+    /// @return A const reference to the TransactionKey.
+    const TransactionKey& getTransactionKey() const;
+
+    /// @brief Fetches the NameChangeRequest status of the transaction.
+    ///
+    /// This is the current status of the NameChangeRequest, not to
+    /// be confused with the state of the transaction.  Once the transaction
+    /// is reached it's conclusion, the request will end up with a final
+    /// status.
+    ///
+    /// @return A dhcp_ddns::NameChangeStatus representing the current
+    /// status of the transaction.
+    dhcp_ddns::NameChangeStatus getNcrStatus() const;
+
+    /// @brief Sets the status of the transaction's NameChangeRequest
+    ///
+    /// @param status is the new value to assign to the NCR status.
+    void setNcrStatus(const dhcp_ddns::NameChangeStatus& status);
+
+    /// @brief Fetches the transaction's current state.
+    ///
+    /// This returns the transaction's notion of the current state. It is the
+    /// state whose handler will be executed on the next iteration of the run
+    /// loop.
+    ///
+    /// @return An unsigned int representing the current state.
+    unsigned int getState() const;
+
+    /// @brief Fetches the transaction's previous state.
+    ///
+    /// @return An unsigned int representing the previous state.
+    unsigned int getPrevState() const;
+
+    /// @brief Fetches the transaction's last event.
+    ///
+    /// @return An unsigned int representing the last event.
+    unsigned int getLastEvent() const;
+
+    /// @brief Fetches the transaction's next event.
+    ///
+    /// This returns the transaction's notion of the next event. It is the
+    /// event that will be passed into the current state's handler on the next
+    /// iteration of the run loop.
+    ///
+    /// @return An unsigned int representing the next event.
+    unsigned int getNextEvent() const;
+
+    /// @brief Fetches the most recent DNS update status.
+    ///
+    /// @return A DNSClient::Status indicating the result of the most recent
+    /// DNS update to complete.
+    DNSClient::Status getDnsUpdateStatus() const;
+
+    /// @brief Fetches the most recent DNS update response packet.
+    ///
+    /// @return A const pointer reference to the D2UpdateMessage most recently
+    /// received.
+    const D2UpdateMessagePtr& getDnsUpdateResponse() const;
+
+    /// @brief Returns whether the forward change has completed or not.
+    ///
+    /// The value returned is only meaningful if the NameChangeRequest calls
+    /// for a forward change to be done. The value returned indicates if
+    /// forward change has been completed successfully.
+    ///
+    /// @return True if the forward change has been completed, false otherwise.
+    bool getForwardChangeCompleted() const;
+
+    /// @brief Returns whether the reverse change has completed or not.
+    ///
+    /// The value returned is only meaningful if the NameChangeRequest calls
+    /// for a reverse change to be done. The value returned indicates if
+    /// reverse change has been completed successfully.
+    ///
+    /// @return True if the reverse change has been completed, false otherwise.
+    bool getReverseChangeCompleted() const;
+
+private:
+    /// @brief Contains a map of states to their state handlers.
+    StateHandlerMap state_handlers_;
+
+    /// @brief The IOService which should be used to for IO processing.
+    isc::asiolink::IOService& io_service_;
+
+    /// @brief The NameChangeRequest that the transaction is to fulfill.
+    dhcp_ddns::NameChangeRequestPtr ncr_;
+
+    /// @brief The forward domain that matches the request.
+    ///
+    /// The forward "domain" is DdnsDomain which contains all of the information
+    /// necessary, including the list of DNS servers to be used for a forward
+    /// change.
+    DdnsDomainPtr forward_domain_;
+
+    /// @brief The reverse domain that matches the request.
+    ///
+    /// The reverse "domain" is DdnsDomain which contains all of the information
+    /// necessary, including the list of DNS servers to be used for a reverse
+    /// change.
+    DdnsDomainPtr reverse_domain_;
+
+    /// @brief The DNSClient instance that will carry out DNS packet exchanges.
+    DNSClientPtr dns_client_;
+
+    /// @brief The current state within the transaction's state model.
+    unsigned int state_;
+
+    /// @brief The previous state within the transaction's state model.
+    unsigned int prev_state_;
+
+    /// @brief The event last processed by the transaction.
+    unsigned int last_event_;
+
+    /// @brief The event the transaction should process next.
+    unsigned int next_event_;
+
+    /// @brief The outcome of the most recently completed DNS packet exchange.
+    DNSClient::Status dns_update_status_;
+
+    /// @brief The DNS update response packet most recently received.
+    D2UpdateMessagePtr dns_update_response_;
+
+    /// @brief Indicator for whether or not the forward change completed ok.
+    bool forward_change_completed_;
+
+    /// @brief Indicator for whether or not the reverse change completed ok.
+    bool reverse_change_completed_;
+};
+
+/// @brief Defines a pointer to a NameChangeTransaction.
+typedef boost::shared_ptr<NameChangeTransaction> NameChangeTransactionPtr;
+
+} // namespace isc::d2
+} // namespace isc
+#endif

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

@@ -64,6 +64,7 @@ d2_unittests_SOURCES += ../d2_update_message.cc ../d2_update_message.h
 d2_unittests_SOURCES += ../d2_update_mgr.cc ../d2_update_mgr.h
 d2_unittests_SOURCES += ../d2_zone.cc ../d2_zone.h
 d2_unittests_SOURCES += ../dns_client.cc ../dns_client.h
+d2_unittests_SOURCES += ../nc_trans.cc ../nc_trans.h
 d2_unittests_SOURCES += d_test_stubs.cc d_test_stubs.h
 d2_unittests_SOURCES += d2_unittests.cc
 d2_unittests_SOURCES += d2_process_unittests.cc
@@ -76,6 +77,7 @@ d2_unittests_SOURCES += d2_update_message_unittests.cc
 d2_unittests_SOURCES += d2_update_mgr_unittests.cc
 d2_unittests_SOURCES += d2_zone_unittests.cc
 d2_unittests_SOURCES += dns_client_unittests.cc
+d2_unittests_SOURCES += nc_trans_unittests.cc
 nodist_d2_unittests_SOURCES = ../d2_messages.h ../d2_messages.cc
 
 d2_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)

+ 336 - 0
src/bin/d2/tests/nc_trans_unittests.cc

@@ -0,0 +1,336 @@
+// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/nc_trans.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::d2;
+
+namespace {
+
+/// @brief Test derivation of NameChangeTransaction for exercising state
+/// model mechanics.
+///
+/// This class faciliates testing by making non-public methods accessible so
+/// they can be invoked directly in test routines.  It implements a very
+/// rudimentary state model, sufficient to test the state model mechanics
+/// supplied by the base class.
+class NameChangeStub : public NameChangeTransaction {
+public:
+
+    // NameChangeStub states
+    static const int DO_WORK_ST = DERIVED_STATES + 1;
+
+    // NameChangeStub events
+    static const int START_WORK_EVT = DERIVED_EVENTS + 1;
+
+    /// @brief Constructor
+    ///
+    /// Parameters match those needed by NameChangeTransaction.
+    NameChangeStub(isc::asiolink::IOService& io_service,
+                   dhcp_ddns::NameChangeRequestPtr& ncr,
+                   DdnsDomainPtr forward_domain,
+                   DdnsDomainPtr reverse_domain)
+        : NameChangeTransaction(io_service, ncr, forward_domain,
+                                reverse_domain) {
+    }
+
+    /// @brief Destructor
+    virtual ~NameChangeStub() {
+    }
+
+    /// @brief State handler for the READY_ST.
+    /// 
+    /// Serves as the starting state handler, it consumes the
+    /// START_TRANSACTION_EVT "transitioing" to the state, DO_WORK_ST and
+    /// sets the next event to START_WORK_EVT.
+    void readyHandler() {
+        switch(getNextEvent()) {
+        case START_TRANSACTION_EVT:
+            setState(DO_WORK_ST);
+            setNextEvent(START_WORK_EVT);
+            break;
+        default:
+            // its bogus
+            isc_throw(NameChangeTransactionError, "invalid event: "
+                      << getNextEvent() << " for state: " << getState());
+        }
+    }
+
+    /// @brief State handler for the DO_WORK_ST.
+    /// 
+    /// Simulates a state that starts some form of asynchronous work.
+    /// When next event is START_WROK_EVT it sets the status to pending
+    /// and signals the state model must "wait" for an event by setting
+    /// next event to NOP_EVT. 
+    ///
+    /// When next event is IO_COMPLETED_EVT, it transitions to the state,
+    /// DONE_ST, and sets the next event to ALL_DONE_EVT.
+    void doWorkHandler() {
+        switch(getNextEvent()) {
+        case START_WORK_EVT:
+            setNcrStatus(dhcp_ddns::ST_PENDING);
+            setNextEvent(NOP_EVT);
+            break;
+        //case WORK_DONE_EVT:
+        case IO_COMPLETED_EVT:
+            setState(DONE_ST);
+            setNextEvent(ALL_DONE_EVT);
+            break;
+        default:
+            // its bogus
+            isc_throw(NameChangeTransactionError, "invalid event: "
+                      << getNextEvent() << " for state: " << getState());
+        }
+    }
+
+    /// @brief State handler for the DONE_ST.
+    ///
+    /// This is the last state in the model.  Note that it sets the
+    /// status to completed and next event to NOP_EVT. 
+    void doneHandler() {
+        switch(getNextEvent()) {
+        case ALL_DONE_EVT:
+            setNcrStatus(dhcp_ddns::ST_COMPLETED);
+            setNextEvent(NOP_EVT);
+            break;
+        default:
+            // its bogus
+            isc_throw(NameChangeTransactionError, "invalid event: "
+                      << getNextEvent() << " for state: " << getState());
+        }
+    }
+
+    /// @brief Initializes the state handler map.
+    void initStateHandlerMap() {
+        addToMap(READY_ST,
+            boost::bind(&NameChangeStub::readyHandler, this));
+
+         addToMap(DO_WORK_ST,
+            boost::bind(&NameChangeStub::doWorkHandler, this));
+
+         addToMap(DONE_ST,
+            boost::bind(&NameChangeStub::doneHandler, this));
+    }
+
+    // Expose the protected methods to be tested.
+    using NameChangeTransaction::addToMap;
+    using NameChangeTransaction::getStateHandler;
+    using NameChangeTransaction::initStateHandlerMap;
+    using NameChangeTransaction::runStateModel;
+    using NameChangeTransaction::setState;
+    using NameChangeTransaction::setNextEvent;
+};
+
+const int NameChangeStub::DO_WORK_ST;
+const int NameChangeStub::START_WORK_EVT;
+
+/// @brief Defines a pointer to a D2UpdateMgr instance.
+typedef boost::shared_ptr<NameChangeStub> NameChangeStubPtr;
+
+/// @brief Test fixture for testing NameChangeTransaction
+///
+/// Note this class uses NameChangeStub class to exercise non-public
+/// aspects of NameChangeTransaction.
+class NameChangeTransactionTest : public ::testing::Test {
+public:
+    isc::asiolink::IOService io_service_;
+
+    virtual ~NameChangeTransactionTest() {
+    }
+
+    /// @brief Instantiates a NameChangeStub built around a canned 
+    /// NameChangeRequest.
+    NameChangeStubPtr makeCannedTransaction() {
+        const char* msg_str =
+            "{"
+            " \"change_type\" : 0 , "
+            " \"forward_change\" : true , "
+            " \"reverse_change\" : true , "
+            " \"fqdn\" : \"walah.walah.org.\" , "
+            " \"ip_address\" : \"192.168.2.1\" , "
+            " \"dhcid\" : \"0102030405060708\" , "
+            " \"lease_expires_on\" : \"20130121132405\" , "
+            " \"lease_length\" : 1300 "
+            "}";
+
+        dhcp_ddns::NameChangeRequestPtr ncr;
+        DnsServerInfoStoragePtr servers;
+        DdnsDomainPtr forward_domain;
+        DdnsDomainPtr reverse_domain;
+
+        ncr = dhcp_ddns::NameChangeRequest::fromJSON(msg_str);
+        forward_domain.reset(new DdnsDomain("*", "", servers));
+        reverse_domain.reset(new DdnsDomain("*", "", servers));
+        return (NameChangeStubPtr(new NameChangeStub(io_service_, ncr,
+                                  forward_domain, reverse_domain)));
+
+    }
+
+};
+
+/// @brief Tests NameChangeTransaction construction.
+/// This test verifies that:
+/// 1. Construction with null NameChangeRequest
+/// 2. Construction with null forward domain is not allowed when the request
+/// requires forward change.
+/// 3. Construction with null reverse domain is not allowed when the request
+/// requires reverse change.
+/// 4. Valid construction functions properly
+TEST(NameChangeTransaction, construction) {
+    isc::asiolink::IOService io_service;
+
+    const char* msg_str =
+        "{"
+        " \"change_type\" : 0 , "
+        " \"forward_change\" : true , "
+        " \"reverse_change\" : true , "
+        " \"fqdn\" : \"walah.walah.org.\" , "
+        " \"ip_address\" : \"192.168.2.1\" , "
+        " \"dhcid\" : \"0102030405060708\" , "
+        " \"lease_expires_on\" : \"20130121132405\" , "
+        " \"lease_length\" : 1300 "
+        "}";
+
+    dhcp_ddns::NameChangeRequestPtr ncr;
+
+    dhcp_ddns::NameChangeRequestPtr empty_ncr;
+    DnsServerInfoStoragePtr servers;
+    DdnsDomainPtr forward_domain;
+    DdnsDomainPtr reverse_domain;
+    DdnsDomainPtr empty_domain;
+
+    ASSERT_NO_THROW(ncr = dhcp_ddns::NameChangeRequest::fromJSON(msg_str));
+    ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", "", servers)));
+    ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", "", servers)));
+
+    // Verify that construction with an empty NameChangeRequest throws. 
+    EXPECT_THROW(NameChangeTransaction(io_service, empty_ncr,
+                                       forward_domain, reverse_domain),
+                                        NameChangeTransactionError);
+
+    // Verify that construction with an empty forward domain when the
+    // NameChangeRequest calls for a forward change throws. 
+    EXPECT_THROW(NameChangeTransaction(io_service, ncr,
+                                       empty_domain, reverse_domain),
+                                       NameChangeTransactionError);
+
+    // Verify that construction with an empty reverse domain when the
+    // NameChangeRequest calls for a reverse change throws. 
+    EXPECT_THROW(NameChangeTransaction(io_service, ncr,
+                                       forward_domain, empty_domain),
+                                       NameChangeTransactionError);
+
+    // Verify that a valid construction attempt works.
+    EXPECT_NO_THROW(NameChangeTransaction(io_service, ncr,
+                                          forward_domain, reverse_domain));
+
+    // Verify that an empty forward domain is allowed when the requests does
+    // include a forward change. 
+    ncr->setForwardChange(false);
+    EXPECT_NO_THROW(NameChangeTransaction(io_service, ncr,
+                                          empty_domain, reverse_domain));
+
+    // Verify that an empty reverse domain is allowed when the requests does
+    // include a reverse change. 
+    ncr->setReverseChange(false);
+    EXPECT_NO_THROW(NameChangeTransaction(io_service, ncr,
+                                          empty_domain, empty_domain));
+}
+
+/// @brief Test the basic mechanics of state model execution.
+/// It first verifies basic state handle map fucntionality, and then 
+/// runs the NameChangeStub state model through from start to finish.
+TEST_F(NameChangeTransactionTest, stateModelTest) {
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+    // Verify that getStateHandler will throw when, handler map is empty.
+    EXPECT_THROW(name_change->getStateHandler(NameChangeTransaction::READY_ST),
+                 NameChangeTransactionError);
+
+    // Verify that we can add a handler to the map.
+    ASSERT_NO_THROW(name_change->addToMap(NameChangeTransaction::READY_ST,
+                           boost::bind(&NameChangeStub::readyHandler,
+                                       name_change.get())));
+
+    // Verify that we can find the handler by its state.
+    EXPECT_NO_THROW(name_change->getStateHandler(NameChangeTransaction::
+                                                 READY_ST));
+
+    // Verify that we cannot add a duplicate.
+    EXPECT_THROW(name_change->addToMap(NameChangeTransaction::READY_ST,
+                           boost::bind(&NameChangeStub::readyHandler,
+                                       name_change.get())),
+                 NameChangeTransactionError);
+
+    // Verify that we can still find the handler by its state.
+    EXPECT_NO_THROW(name_change->getStateHandler(NameChangeTransaction::
+                                                 READY_ST));
+
+
+    // Get a fresh transaction.
+    ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+    // Manually call checkHandlerMap to ensure our test map populates.
+    // This is the method startTranscation invokes.
+    ASSERT_NO_THROW(name_change->initStateHandlerMap());
+
+    // Verify that we can find all the handlers by their state.
+    EXPECT_NO_THROW(name_change->getStateHandler(NameChangeTransaction::
+                                                 READY_ST));
+    EXPECT_NO_THROW(name_change->getStateHandler(NameChangeStub::DO_WORK_ST));
+    EXPECT_NO_THROW(name_change->getStateHandler(NameChangeTransaction::
+                                                 DONE_ST));
+
+
+    // Default value for state is NEW_ST.  Attempting to run the model
+    // with an invalid state will result in status of ST_FAILED.
+    ASSERT_EQ(NameChangeTransaction::NEW_ST, name_change->getState());
+    EXPECT_NO_THROW(name_change->runStateModel(NameChangeTransaction::
+                                               START_TRANSACTION_EVT));
+
+    EXPECT_EQ(dhcp_ddns::ST_FAILED, name_change->getNcrStatus());
+
+    // Get a fresh transaction.
+    ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+    // Launch the transaction properly by calling startTranscation.
+    // Verify that this transitions through to state of DO_WORK_ST,
+    // last event is START_WORK_EVT, next event is NOP_EVT, and
+    // NCR status is ST_PENDING.
+    ASSERT_NO_THROW(name_change->startTransaction());
+
+    EXPECT_EQ(NameChangeStub::DO_WORK_ST, name_change->getState());
+    EXPECT_EQ(NameChangeStub::START_WORK_EVT, name_change->getLastEvent());
+    EXPECT_EQ(NameChangeTransaction::NOP_EVT, name_change->getNextEvent());
+    EXPECT_EQ(dhcp_ddns::ST_PENDING, name_change->getNcrStatus());
+
+    // Simulate completion of DNSClient exchange by invoking the callback.
+    EXPECT_NO_THROW((*name_change)(DNSClient::SUCCESS));
+
+    // Verify that the state model has progressed through to completion:
+    // it is in the DONE_ST, the status is ST_COMPLETED, and the next event
+    // is NOP_EVT.
+    EXPECT_EQ(NameChangeTransaction::DONE_ST, name_change->getState());
+    EXPECT_EQ(dhcp_ddns::ST_COMPLETED, name_change->getNcrStatus());
+    EXPECT_EQ(NameChangeTransaction::NOP_EVT, name_change->getNextEvent());
+}
+
+}