Browse Source

[3086] Merge branch 'trac3086'

Adds the initial implementation of b10-dhcp-ddns NameChangeTransaction class.
This class is an abstract, state-model driven class which provides the basic
mechanics for performing DDNS updates based on a NameChangeRequest.
Thomas Markwalder 11 years ago
parent
commit
079b862c9e

+ 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_update_mgr.cc d2_update_mgr.h
 b10_dhcp_ddns_SOURCES += d2_zone.cc d2_zone.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 += 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
 nodist_b10_dhcp_ddns_SOURCES = d2_messages.h d2_messages.cc
 EXTRA_DIST += d2_messages.mes
 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
 % DHCP_DDNS_SHUTDOWN application received shutdown command with args: %1
 This is informational message issued when the application has been instructed
 This is informational message issued when the application has been instructed
 to shut down by the controller.
 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. 

+ 1 - 1
src/bin/d2/d2_update_mgr.cc

@@ -77,7 +77,7 @@ D2UpdateMgr::checkFinishedTransactions() {
     // NOTE: One must use postfix increments of the iterator on the calls
     // NOTE: One must use postfix increments of the iterator on the calls
     // to erase.  This replaces the old iterator which becomes invalid by the
     // to erase.  This replaces the old iterator which becomes invalid by the
     // erase with a the next valid iterator.  Prefix incrementing will not
     // erase with a the next valid iterator.  Prefix incrementing will not
-    // work. 
+    // work.
     TransactionList::iterator it = transaction_list_.begin();
     TransactionList::iterator it = transaction_list_.begin();
     while (it != transaction_list_.end()) {
     while (it != transaction_list_.end()) {
         NameChangeTransactionPtr trans = (*it).second;
         NameChangeTransactionPtr trans = (*it).second;

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

@@ -22,6 +22,7 @@
 #include <d2/d2_log.h>
 #include <d2/d2_log.h>
 #include <d2/d2_queue_mgr.h>
 #include <d2/d2_queue_mgr.h>
 #include <d2/d2_cfg_mgr.h>
 #include <d2/d2_cfg_mgr.h>
+#include <d2/nc_trans.h>
 
 
 #include <boost/noncopyable.hpp>
 #include <boost/noncopyable.hpp>
 #include <boost/shared_ptr.hpp>
 #include <boost/shared_ptr.hpp>
@@ -37,58 +38,9 @@ public:
         isc::Exception(file, line, what) { };
         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.
 /// @brief Defines a list of transactions.
 typedef std::map<TransactionKey, NameChangeTransactionPtr> TransactionList;
 typedef std::map<TransactionKey, NameChangeTransactionPtr> TransactionList;
 
 
-
 /// @brief D2UpdateMgr creates and manages update transactions.
 /// @brief D2UpdateMgr creates and manages update transactions.
 ///
 ///
 /// D2UpdateMgr is the DHCP_DDNS task master, instantiating and then supervising
 /// D2UpdateMgr is the DHCP_DDNS task master, instantiating and then supervising

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

@@ -0,0 +1,289 @@
+// 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::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),
+     current_server_list_(), current_server_(), next_server_pos_(0) {
+    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();
+
+    // Test validity of the handler map. This provides an opportunity to
+    // sanity check the map prior to attempting to execute the model.
+    verifyStateHandlerMap();
+
+    // Set the current state to READY and enter the run loop.
+    setState(READY_ST);
+    runStateModel(START_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::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());
+}
+
+DdnsDomainPtr&
+NameChangeTransaction::getForwardDomain() {
+    return (forward_domain_);
+}
+
+DdnsDomainPtr&
+NameChangeTransaction::getReverseDomain() {
+    return (reverse_domain_);
+}
+
+void
+NameChangeTransaction::initServerSelection(const DdnsDomainPtr& domain) {
+    if (!domain) {
+        isc_throw(NameChangeTransactionError,
+                  "initServerSelection called with an empty domain");
+    }
+    current_server_list_ = domain->getServers();
+    next_server_pos_ = 0;
+    current_server_.reset();
+}
+
+bool
+NameChangeTransaction::selectNextServer() {
+    if ((current_server_list_) &&
+        (next_server_pos_ < current_server_list_->size())) {
+        current_server_  = (*current_server_list_)[next_server_pos_];
+        dns_update_response_.reset(new
+                                   D2UpdateMessage(D2UpdateMessage::INBOUND));
+        // @todo  Protocol is set on DNSClient constructor.  We need
+        // to propagate a configuration value downward, probably starting
+        // at global, then domain, then server
+        // Once that is supported we need to add it here.
+        dns_client_.reset(new DNSClient(dns_update_response_ , this,
+                                        DNSClient::UDP));
+        ++next_server_pos_;
+        return (true);
+    }
+
+    return (false);
+}
+
+const DNSClientPtr&
+NameChangeTransaction::getDNSClient() const {
+    return (dns_client_);
+}
+
+const DnsServerInfoPtr&
+NameChangeTransaction::getCurrentServer() const {
+    return (current_server_);
+}
+
+
+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

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

@@ -0,0 +1,571 @@
+// 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
+/// and 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.
+    ///
+    /// Within this state, the actual selection of the next forward server
+    /// to use is conducted.  Upon conclusion of this state the next server
+    /// is either selected or it should transition out with NO_MORE_SERVERS_EVT
+    /// event.
+    static const int SELECTING_FWD_SERVER_ST = 2;
+
+    /// @brief State in which reverse DNS server  selection is done.
+    ///
+    /// Within this state, the actual selection of the next reverse server
+    /// to use is conducted.  Upon conclusion of this state the next server
+    /// is either selected or it should transition out with NO_MORE_SERVERS_EVT
+    /// event.
+    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 state model has no more work left to do.
+    static const int ALL_DONE_EVT = 9;
+
+    /// @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 NameChangeTransactionError 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);
+
+    /// @brief Destructor
+    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();
+
+    /// @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 a
+    /// temporary empty, implementation.  If we make it pure virtual now
+    /// D2UpdateManager will not compile.
+    virtual void initStateHandlerMap() {};
+
+
+    /// @brief Validates the contents of the state handler map.
+    ///
+    /// This method is invoked immediately after initStateHandlerMap and
+    /// provides an opportunity for derivations to verify that the map
+    /// is correct.  If the map is determined to be invalid this method
+    /// should throw a NameChangeTransactionError.
+    ///
+    /// The simplest implementation would include a call to getStateHandler,
+    /// for each state the derivation supports.  For example, a implementation
+    /// which included three states, READY_ST, DO_WORK_ST, and DONE_ST could
+    /// implement this function as follows:
+    ///
+    /// @code
+    ///    void verifyStateHandlerMap() {
+    ///        getStateHandler(READY_ST);
+    ///        getStateHandler(DO_WORK_ST);
+    ///        getStateHandler(DONE_ST);
+    ///    }
+    /// @endcode
+    ///
+    /// @todo This method should be pure virtual but until there are
+    /// derivations for the update manager to use, we will provide a
+    /// temporary empty, implementation.  If we make it pure virtual now
+    /// D2UpdateManager will not compile.
+    /// @throw NameChangeTransactionError if the map is invalid.
+    virtual void verifyStateHandlerMap() {};
+
+    /// @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 state, 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);
+
+    /// @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 Initializes server selection from the given DDNS domain.
+    ///
+    /// Method prepares internal data to conduct server selection from the
+    /// list of servers supplied by the given domain.  This method should be
+    /// called when a transaction is ready to begin selecting servers from
+    /// a new list.  Typically this will be prior to starting the updates for
+    /// a given DNS direction.
+    ///
+    /// @param domain is the domain from which server selection is to be
+    /// conducted.
+    void initServerSelection(const DdnsDomainPtr& domain);
+
+    /// @brief Selects the next server in the current server list.
+    ///
+    /// This method is used to iterate over the list of servers.  If there are
+    /// no more servers in the list, it returns false.  Otherwise it sets the
+    /// the current server to the next server and creates a new DNSClient
+    /// instance.
+    ///
+    /// @return True if a server has been selected, false if there are no more
+    /// servers from which to select.
+    bool selectNextServer();
+
+    /// @brief Fetches the currently selected server.
+    ///
+    /// @return A const pointer reference to the DnsServerInfo of the current
+    /// server.
+    const DnsServerInfoPtr& getCurrentServer() const;
+
+    /// @brief Fetches the DNSClient instance
+    ///
+    /// @return A const pointer reference to the DNSClient
+    const DNSClientPtr& getDNSClient() const;
+
+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 its 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 Fetches the forward DdnsDomain.
+    ///
+    /// @return A pointer reference to the forward DdnsDomain.  If the
+    /// the request does not include a forward change, the pointer will empty.
+    DdnsDomainPtr& getForwardDomain();
+
+    /// @brief Fetches the reverse DdnsDomain.
+    ///
+    /// @return A pointer reference to the reverse DdnsDomain.  If the
+    /// the request does not include a reverse change, the pointer will empty.
+    DdnsDomainPtr& getReverseDomain();
+
+    /// @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 Pointer to the current server selection list.
+    DnsServerInfoStoragePtr current_server_list_;
+
+    /// @brief Pointer to the currently selected server.
+    DnsServerInfoPtr current_server_;
+
+    /// @brief Next server position in the list.
+    ///
+    /// This value is always the position of the next selection in the server
+    /// list, which may be beyond the end of the list.
+    size_t next_server_pos_;
+};
+
+/// @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_update_mgr.cc ../d2_update_mgr.h
 d2_unittests_SOURCES += ../d2_zone.cc ../d2_zone.h
 d2_unittests_SOURCES += ../d2_zone.cc ../d2_zone.h
 d2_unittests_SOURCES += ../dns_client.cc ../dns_client.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 += d_test_stubs.cc d_test_stubs.h
 d2_unittests_SOURCES += d2_unittests.cc
 d2_unittests_SOURCES += d2_unittests.cc
 d2_unittests_SOURCES += d2_process_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_update_mgr_unittests.cc
 d2_unittests_SOURCES += d2_zone_unittests.cc
 d2_unittests_SOURCES += d2_zone_unittests.cc
 d2_unittests_SOURCES += dns_client_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
 nodist_d2_unittests_SOURCES = ../d2_messages.h ../d2_messages.cc
 
 
 d2_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 d2_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)

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

@@ -0,0 +1,621 @@
+// 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 facilitates 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 DUMMY_ST = DERIVED_STATES + 1;
+
+    static const int DO_WORK_ST = DERIVED_STATES + 2;
+
+
+    // 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), dummy_called_(false) {
+    }
+
+    /// @brief Destructor
+    virtual ~NameChangeStub() {
+    }
+
+    bool getDummyCalled() {
+        return (dummy_called_);
+    }
+
+    void clearDummyCalled() {
+        dummy_called_ = false;
+    }
+
+    void dummyHandler() {
+       dummy_called_ = true;
+    }
+
+    /// @brief State handler for the READY_ST.
+    ///
+    /// Serves as the starting state handler, it consumes the
+    /// START_TRANSACTION_EVT "transitioning" 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_WORK_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));
+    }
+
+    void verifyStateHandlerMap() {
+        getStateHandler(READY_ST);
+        getStateHandler(DO_WORK_ST);
+        getStateHandler(DONE_ST);
+    }
+
+    // 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;
+    using NameChangeTransaction::initServerSelection;
+    using NameChangeTransaction::selectNextServer;
+    using NameChangeTransaction::getCurrentServer;
+    using NameChangeTransaction::getDNSClient;
+    using NameChangeTransaction::setNcrStatus;
+    using NameChangeTransaction::setDnsUpdateStatus;
+    using NameChangeTransaction::getDnsUpdateResponse;
+    using NameChangeTransaction::getForwardChangeCompleted;
+    using NameChangeTransaction::getReverseChangeCompleted;
+    using NameChangeTransaction::setForwardChangeCompleted;
+    using NameChangeTransaction::setReverseChangeCompleted;
+
+    bool dummy_called_;
+};
+
+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_;
+    DdnsDomainPtr forward_domain_;
+    DdnsDomainPtr reverse_domain_;
+
+    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\" : \"example.com.\" , "
+            " \"ip_address\" : \"192.168.2.1\" , "
+            " \"dhcid\" : \"0102030405060708\" , "
+            " \"lease_expires_on\" : \"20130121132405\" , "
+            " \"lease_length\" : 1300 "
+            "}";
+
+        dhcp_ddns::NameChangeRequestPtr ncr;
+
+        DnsServerInfoStoragePtr servers(new DnsServerInfoStorage());
+        DnsServerInfoPtr server;
+
+        ncr = dhcp_ddns::NameChangeRequest::fromJSON(msg_str);
+
+        // make forward server list
+        server.reset(new DnsServerInfo("forward.example.com",
+                                       isc::asiolink::IOAddress("1.1.1.1")));
+        servers->push_back(server);
+        forward_domain_.reset(new DdnsDomain("*", "", servers));
+
+        // make reverse server list
+        servers->clear();
+        server.reset(new DnsServerInfo("reverse.example.com",
+                                       isc::asiolink::IOAddress("2.2.2.2")));
+        servers->push_back(server);
+        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\" : \"example.com.\" , "
+        " \"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
+    // not include a forward change.
+    ncr->setForwardChange(false);
+    ncr->setReverseChange(true);
+    EXPECT_NO_THROW(NameChangeTransaction(io_service, ncr,
+                                          empty_domain, reverse_domain));
+
+    // Verify that an empty reverse domain is allowed when the requests does
+    // not include a reverse change.
+    ncr->setForwardChange(true);
+    ncr->setReverseChange(false);
+    EXPECT_NO_THROW(NameChangeTransaction(io_service, ncr,
+                                          forward_domain, empty_domain));
+}
+
+/// @brief General testing of member accessors.
+/// Most if not all of these are also tested as a byproduct off larger tests.
+TEST_F(NameChangeTransactionTest, accessors) {
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+    // Verify that fetching the NameChangeRequest works.
+    dhcp_ddns::NameChangeRequestPtr ncr = name_change->getNcr();
+    ASSERT_TRUE(ncr);
+
+    // Verify that getTransactionKey works.
+    EXPECT_EQ(ncr->getDhcid(), name_change->getTransactionKey());
+
+    // Verify that NcrStatus can be set and retrieved.
+    EXPECT_NO_THROW(name_change->setNcrStatus(dhcp_ddns::ST_FAILED));
+    EXPECT_EQ(dhcp_ddns::ST_FAILED, ncr->getStatus());
+
+    // Verify that the forward domain can be retrieved.
+    ASSERT_TRUE(name_change->getForwardDomain());
+    EXPECT_EQ(forward_domain_, name_change->getForwardDomain());
+
+    // Verify that the reverse domain can be retrieved.
+    ASSERT_TRUE(name_change->getReverseDomain());
+    EXPECT_EQ(reverse_domain_, name_change->getReverseDomain());
+
+    // Neither of these have direct setters, but are tested under server
+    // selection.
+    EXPECT_FALSE(name_change->getDNSClient());
+    EXPECT_FALSE(name_change->getCurrentServer());
+
+    // Previous state should be set by setState.
+    EXPECT_NO_THROW(name_change->setState(NameChangeTransaction::READY_ST));
+    EXPECT_NO_THROW(name_change->setState(NameChangeStub::DO_WORK_ST));
+    EXPECT_EQ(NameChangeTransaction::READY_ST, name_change->getPrevState());
+    EXPECT_EQ(NameChangeStub::DO_WORK_ST, name_change->getState());
+
+    // Last event should be set by setNextEvent.
+    EXPECT_NO_THROW(name_change->setNextEvent(NameChangeStub::
+                                              START_WORK_EVT));
+    EXPECT_NO_THROW(name_change->setNextEvent(NameChangeTransaction::
+                                              IO_COMPLETED_EVT));
+    EXPECT_EQ(NameChangeStub::START_WORK_EVT, name_change->getLastEvent());
+    EXPECT_EQ(NameChangeTransaction::IO_COMPLETED_EVT,
+              name_change->getNextEvent());
+
+    // Verify that DNS update status can be set and retrieved.
+    EXPECT_NO_THROW(name_change->setDnsUpdateStatus(DNSClient::TIMEOUT));
+    EXPECT_EQ(DNSClient::TIMEOUT, name_change->getDnsUpdateStatus());
+
+    // Verify that the DNS update response can be retrieved. 
+    EXPECT_FALSE(name_change->getDnsUpdateResponse());
+
+    // Verify that the forward change complete flag can be set and fetched.
+    EXPECT_NO_THROW(name_change->setForwardChangeCompleted(true));
+    EXPECT_TRUE(name_change->getForwardChangeCompleted());
+
+    // Verify that the reverse change complete flag can be set and fetched.
+    EXPECT_NO_THROW(name_change->setReverseChangeCompleted(true));
+    EXPECT_TRUE(name_change->getReverseChangeCompleted());
+}
+
+/// @brief Tests the fundamental methods used for state handler mapping.
+/// Verifies the ability to search for and add entries in the state handler map.
+TEST_F(NameChangeTransactionTest, basicStateMapping) {
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+    // Verify that getStateHandler will throw when, state cannot be found.
+    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::
+                                                      dummyHandler,
+                                                      name_change.get())));
+
+    // Verify that we can find the handler by its state.
+    StateHandler retreived_handler;
+    EXPECT_NO_THROW(retreived_handler =
+                    name_change->getStateHandler(NameChangeTransaction::
+                                                 READY_ST));
+
+    // Verify that retrieved handler executes the correct method.
+    name_change->clearDummyCalled();
+
+    ASSERT_NO_THROW((retreived_handler)());
+    EXPECT_TRUE(name_change->getDummyCalled());
+
+    // 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));
+}
+
+/// @brief Tests state map initialization and validation.
+/// This tests the basic concept of state map initialization and verification
+/// by manually invoking the map methods normally called by startTransaction.
+TEST_F(NameChangeTransactionTest, stubStateMapInit) {
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+    // Verify that the map validation throws prior to the map being
+    // initialized.
+    ASSERT_THROW(name_change->verifyStateHandlerMap(),
+                 NameChangeTransactionError);
+
+    // Call initStateHandlerMap to initialize the state map.
+    ASSERT_NO_THROW(name_change->initStateHandlerMap());
+
+    // Verify that the map validation succeeds now that the map is initialized.
+    ASSERT_NO_THROW(name_change->verifyStateHandlerMap());
+}
+
+/// @brief Tests that invalid states are handled gracefully.
+/// This test verifies that attempting to execute a state which has no handler
+/// results in a failed transaction.
+TEST_F(NameChangeTransactionTest, invalidState) {
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+    // Verfiy that to running the model with a state that has no handler,
+    // will result in failed transaction (status of ST_FAILED).
+    // First, verify state is NEW_ST and that NEW_ST has no handler.
+    // that the transaction failed:
+    ASSERT_EQ(NameChangeTransaction::NEW_ST, name_change->getState());
+    ASSERT_THROW(name_change->getStateHandler(NameChangeTransaction::NEW_ST),
+                 NameChangeTransactionError);
+
+    // Now call runStateModel() which should not throw.
+    EXPECT_NO_THROW(name_change->runStateModel(NameChangeTransaction::
+                                               START_TRANSACTION_EVT));
+
+    // Verify that the transaction has failed.
+    EXPECT_EQ(dhcp_ddns::ST_FAILED, name_change->getNcrStatus());
+}
+
+/// @brief Tests that invalid events are handled gracefully.
+/// This test verifies that submitting an invalid event to the state machine
+/// results in a failed transaction.
+TEST_F(NameChangeTransactionTest, invalidEvent) {
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+    // First, lets execute the state model to a known valid point, by
+    // calling startTransaction.
+    ASSERT_NO_THROW(name_change->startTransaction());
+
+    // Verify we are in the state of DO_WORK_ST.
+    EXPECT_EQ(NameChangeStub::DO_WORK_ST, name_change->getState());
+
+    // Verity that submitting an invalid event to a valid state, results
+    // in a failed transaction without a throw (Current state is DO_WORK_ST,
+    // during which START_TRANSACTION_EVT, is invalid).
+    EXPECT_NO_THROW(name_change->runStateModel(NameChangeTransaction::
+                                               START_TRANSACTION_EVT));
+
+    // Verify that the transaction has failed.
+    EXPECT_EQ(dhcp_ddns::ST_FAILED, name_change->getNcrStatus());
+}
+
+/// @brief Test the basic mechanics of state model execution.
+/// This test exercises the ability to execute state model from state to
+/// finish, including the handling of a asynchronous IO operation.
+TEST_F(NameChangeTransactionTest, stateModelTest) {
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+    // Launch the transaction by calling startTransaction.  The state model
+    // should run up until the "IO" operation is initiated in DO_WORK_ST.
+
+    ASSERT_NO_THROW(name_change->startTransaction());
+
+    // Verify that we are now in state of DO_WORK_ST, the last event was
+    // START_WORK_EVT, the next event is NOP_EVT, and NCR status is ST_PENDING.
+    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, as
+    // DNSClient would.  This should cause the state model to progress through
+    // completion.
+    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());
+}
+
+/// @brief Tests server selection methods.
+/// Each transaction has a list of one or more servers for each DNS direction
+/// it is required to update.  The transaction must be able to start at the
+/// beginning of a server list and cycle through them one at time, as needed,
+/// when a DNS exchange fails due to an IO error.  This test verifies the
+/// ability to iteratively select a server from the list as the current server.
+TEST_F(NameChangeTransactionTest, serverSelectionTest) {
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+    // Verify that the forward domain and its list of servers can be retrieved.
+    DdnsDomainPtr& domain = name_change->getForwardDomain();
+    ASSERT_TRUE(domain);
+    DnsServerInfoStoragePtr servers = domain->getServers();
+    ASSERT_TRUE(servers);
+
+    // Get the number of entries in the server list.
+    int num_servers = servers->size();
+    ASSERT_TRUE(num_servers > 0);
+
+    // Verify that we can initialize server selection.  This "resets" the
+    // selection process to start over using the list of servers in the
+    // given domain.
+    ASSERT_NO_THROW(name_change->initServerSelection(domain));
+
+    // The server selection process determines the current server,
+    // instantiates a new DNSClient, and a DNS response message buffer.
+    //  We need to save the values before each selection, so we can verify
+    // they are correct after each selection.
+    DnsServerInfoPtr prev_server = name_change->getCurrentServer();
+    DNSClientPtr prev_client = name_change->getDNSClient();
+    D2UpdateMessagePtr prev_response = name_change->getDnsUpdateResponse();
+
+    // Iteratively select through the list of servers.
+    int passes = 0;
+    while (name_change->selectNextServer()) {
+        // Get the new values after the selection has been made.
+        DnsServerInfoPtr server = name_change->getCurrentServer();
+        DNSClientPtr client = name_change->getDNSClient();
+        D2UpdateMessagePtr response = name_change->getDnsUpdateResponse();
+
+        // Verify that the new values are not empty.
+        EXPECT_TRUE(server);
+        EXPECT_TRUE(client);
+        EXPECT_TRUE(response);
+
+        // Verify that the new values are indeed new.
+        EXPECT_NE(server, prev_server);
+        EXPECT_NE(client, prev_client);
+        EXPECT_NE(response, prev_response);
+
+        // Remember the selected values for the next pass.
+        prev_server = server;
+        prev_client = client;
+        prev_response = response;
+
+        ++passes;
+    }
+
+    // Verify that the number of passes made equal the number of servers.
+    EXPECT_EQ (passes, num_servers);
+
+    // Repeat the same test using the reverse domain.
+    // Verify that the reverse domain and its list of servers can be retrieved.
+    domain = name_change->getReverseDomain();
+    ASSERT_TRUE(domain);
+    servers = domain->getServers();
+    ASSERT_TRUE(servers);
+
+    // Get the number of entries in the server list.
+    num_servers = servers->size();
+    ASSERT_TRUE(num_servers > 0);
+
+    // Verify that we can initialize server selection.  This "resets" the
+    // selection process to start over using the list of servers in the
+    // given domain.
+    ASSERT_NO_THROW(name_change->initServerSelection(domain));
+
+    // The server selection process determines the current server,
+    // instantiates a new DNSClient, and a DNS response message buffer.
+    // We need to save the values before each selection, so we can verify
+    // they are correct after each selection.
+    prev_server = name_change->getCurrentServer();
+    prev_client = name_change->getDNSClient();
+    prev_response = name_change->getDnsUpdateResponse();
+
+    // Iteratively select through the list of servers.
+    passes = 0;
+    while (name_change->selectNextServer()) {
+        // Get the new values after the selection has been made.
+        DnsServerInfoPtr server = name_change->getCurrentServer();
+        DNSClientPtr client = name_change->getDNSClient();
+        D2UpdateMessagePtr response = name_change->getDnsUpdateResponse();
+
+        // Verify that the new values are not empty.
+        EXPECT_TRUE(server);
+        EXPECT_TRUE(client);
+        EXPECT_TRUE(response);
+
+        // Verify that the new values are indeed new.
+        EXPECT_NE(server, prev_server);
+        EXPECT_NE(client, prev_client);
+        EXPECT_NE(response, prev_response);
+
+        // Remember the selected values for the next pass.
+        prev_server = server;
+        prev_client = client;
+        prev_response = response;
+
+        ++passes;
+    }
+
+    // Verify that the number of passes made equal the number of servers.
+    EXPECT_EQ (passes, num_servers);
+}
+
+}

+ 8 - 0
src/lib/dhcp_ddns/ncr_msg.h

@@ -156,6 +156,14 @@ typedef std::map<std::string, isc::data::ConstElementPtr> ElementMap;
 class NameChangeRequest {
 class NameChangeRequest {
 public:
 public:
     /// @brief Default Constructor.
     /// @brief Default Constructor.
+    ///
+    /// @todo Currently, fromWire makes use of the ability to create an empty
+    /// NameChangeRequest and then builds it bit by bit.  This means that it
+    /// is technically possible to create one and attempt to use in ways
+    /// other than intended and its invalid content may or may not be handled
+    /// gracefully by consuming code.  It might be wise to revisit this
+    /// structuring such that we do not use a default constructor and only
+    /// allow valid instantiations.
     NameChangeRequest();
     NameChangeRequest();
 
 
     /// @brief Constructor.  Full constructor, which provides parameters for
     /// @brief Constructor.  Full constructor, which provides parameters for