From: Thomas Markwalder Date: Wed, 17 Jul 2013 20:34:33 +0000 (-0400) Subject: [3008] Initial implementation of classes for sending and X-Git-Tag: bind10-1.2.0beta1-release~327^2~5 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=f683ad20d4a1936679ebfbc3f7eabedee1ff5182;p=thirdparty%2Fkea.git [3008] Initial implementation of classes for sending and receiving NameChangeRequests for use with DHCP-DDNS. This includes abstract listener and sender classes, as well as a derivations supporting traffic over UDP sockets. New files added to src/bin/d2 ncr_io.h - base classes ncr_io.cc ncr_udp.h - UDP derivations ncr_udp.cc tests/ncr_udp_unittests.cc --- diff --git a/src/bin/d2/Makefile.am b/src/bin/d2/Makefile.am index a00bece186..25414a6bc0 100644 --- a/src/bin/d2/Makefile.am +++ b/src/bin/d2/Makefile.am @@ -57,7 +57,9 @@ b10_dhcp_ddns_SOURCES += d2_cfg_mgr.cc d2_cfg_mgr.h b10_dhcp_ddns_SOURCES += d2_update_message.cc d2_update_message.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 += ncr_io.cc ncr_io.h b10_dhcp_ddns_SOURCES += ncr_msg.cc ncr_msg.h +b10_dhcp_ddns_SOURCES += ncr_udp.cc ncr_udp.h nodist_b10_dhcp_ddns_SOURCES = d2_messages.h d2_messages.cc EXTRA_DIST += d2_messages.mes diff --git a/src/bin/d2/d2_cfg_mgr.cc b/src/bin/d2/d2_cfg_mgr.cc index 07068d60e4..671f62447d 100644 --- a/src/bin/d2/d2_cfg_mgr.cc +++ b/src/bin/d2/d2_cfg_mgr.cc @@ -17,12 +17,6 @@ #include -using namespace std; -using namespace isc; -using namespace isc::dhcp; -using namespace isc::data; -using namespace isc::asiolink; - namespace isc { namespace d2 { @@ -102,12 +96,14 @@ D2CfgMgr::createConfigParser(const std::string& config_id) { D2CfgContextPtr context = getD2CfgContext(); // Create parser instance based on element_id. - DhcpConfigParser* parser = NULL; + isc::dhcp::DhcpConfigParser* parser = NULL; if ((config_id == "interface") || (config_id == "ip_address")) { - parser = new StringParser(config_id, context->getStringStorage()); + parser = new isc::dhcp::StringParser(config_id, + context->getStringStorage()); } else if (config_id == "port") { - parser = new Uint32Parser(config_id, context->getUint32Storage()); + parser = new isc::dhcp::Uint32Parser(config_id, + context->getUint32Storage()); } else if (config_id == "forward_ddns") { parser = new DdnsDomainListMgrParser("forward_mgr", context->getForwardMgr(), diff --git a/src/bin/d2/d2_messages.mes b/src/bin/d2/d2_messages.mes index d2358ceb11..62094c5d75 100644 --- a/src/bin/d2/d2_messages.mes +++ b/src/bin/d2/d2_messages.mes @@ -70,7 +70,7 @@ application when it is not running. % DCTL_ORDER_ERROR configuration contains more elements than the parsing order An error message which indicates that configuration being parsed includes -element ids not specified the configuration manager's parse order list. This +element ids not specified the configuration manager's parse order list. This is a programmatic error. % DCTL_ORDER_NO_ELEMENT element: %1 is in the parsing order but is missing from the configuration @@ -78,7 +78,7 @@ An error message output during a configuration update. The program is expecting an item but has not found it in the new configuration. This may mean that the BIND 10 configuration database is corrupt. -% DCTL_PARSER_FAIL configuration parsing failed for configuration element: %1, reason: %2 +% DCTL_PARSER_FAIL configuration parsing failed for configuration element: %1, reason: %2 On receipt of message containing details to a change of its configuration, the server failed to create a parser to decode the contents of the named configuration element, or the creation succeeded but the parsing actions @@ -124,16 +124,40 @@ has been invoked. This is a debug message issued when the Dhcp-Ddns application encounters an unrecoverable error from within the event loop. +% DHCP_DDNS_INVALID_NCR application received an invalid DNS update request: %1 +This is an error message that indicates that an invalid request to update +a DNS entry was recevied by the application. Either the format or the content +of the request is incorret. The request will be ignored. + +% DHCP_DDNS_INVALID_RESPONSE received response to DNS Update message is malformed: %1 +This is a debug message issued when the DHCP-DDNS application encountered an +error while decoding a response to DNS Update message. Typically, this error +will be encountered when a response message is malformed. + +% DHCP_DDNS_NCR_UDP_LISTEN_CLOSE application encountered an error while closing the UDP socket used to receive NameChangeRequests : %1 +This is an error message that indicates the application was unable to close the +UDP socket being used to receive NameChangeRequests. Socket closure may occur +during the course of error recovery or duing normal shutdown procedure. In +either case the error is unlikely to impair the application's ability to +process requests but it should be reported for analysis. + +% DHCP_DDNS_NCR_UDP_RECV_ERROR UDP socket receive error while listening for DNS Update requests: %1 +This is an error message indicating that an IO error occured while listening +over a UDP socket for DNS update requests. in the request. This could indicate +a network connectivity or system resource issue. + +% DHCP_DDNS_NCR_UDP_SEND_CLOSE application encountered an error while closing the UDP socket used to send NameChangeRequests : %1 +This is an error message that indicates the DHCP-DDNS client was unable to close +the UDP socket being used to send NameChangeRequests. Socket closure may occur +during the course of error recovery or duing normal shutdown procedure. In +either case the error is unlikely to impair the client's ability to send +requests but it should be reported for analysis. + % DHCP_DDNS_NO_MATCH No DNS servers match FQDN: %1 This is warning message issued when there are no domains in the configuration -which match the cited fully qualified domain name (FQDN). The DNS Update +which match the cited fully qualified domain name (FQDN). The DNS Update request for the FQDN cannot be processed. -% DHCP_DDNS_INVALID_RESPONSE received response to DNS Update message is malformed: %1 -This is a debug message issued when the DHCP-DDNS application encountered an error -while decoding a response to DNS Update message. Typically, this error will be -encountered when a response message is malformed. - % DHCP_DDNS_PROCESS_INIT application init invoked This is a debug message issued when the Dhcp-Ddns application enters its init method. diff --git a/src/bin/d2/ncr_io.cc b/src/bin/d2/ncr_io.cc new file mode 100644 index 0000000000..9d45bc707a --- /dev/null +++ b/src/bin/d2/ncr_io.cc @@ -0,0 +1,217 @@ +// 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 +#include + +namespace isc { +namespace d2 { + +//************************** NameChangeListener *************************** + +NameChangeListener::NameChangeListener(const RequestReceiveHandler* + recv_handler) + : listening_(false), recv_handler_(recv_handler) { + if (!recv_handler) { + isc_throw(NcrListenerError, + "NameChangeListener ctor - recv_handler cannot be null"); + } +}; + +void +NameChangeListener::startListening(isc::asiolink::IOService& io_service) { + if (amListening()) { + // This amounts to a programmatic error. + isc_throw(NcrListenerError, "NameChangeListener is already listening"); + } + + // Call implementation dependent open. + try { + open(io_service); + } catch (const isc::Exception& ex) { + stopListening(); + isc_throw(NcrListenerOpenError, "Open failed:" << ex.what()); + } + + // Set our status to listening. + setListening(true); + + // Start the first asynchronous receive. + try { + doReceive(); + } catch (const isc::Exception& ex) { + stopListening(); + isc_throw(NcrListenerReceiveError, "doReceive failed:" << ex.what()); + } +} + +void +NameChangeListener::stopListening() { + try { + // Call implementation dependent close. + close(); + } catch (const isc::Exception &ex) { + // Swallow exceptions. If we have some sort of error we'll log + // it but we won't propagate the throw. + LOG_ERROR(dctl_logger, DHCP_DDNS_NCR_UDP_LISTEN_CLOSE).arg(ex.what()); + } + + setListening(false); +} + +void +NameChangeListener::invokeRecvHandler(Result result, NameChangeRequestPtr ncr) { + // Call the registered application layer handler. + (*(const_cast(recv_handler_)))(result, ncr); + + // Start the next IO layer asynchronous receive. + doReceive(); +}; + +//************************* NameChangeSender ****************************** + +NameChangeSender::NameChangeSender(const RequestSendHandler* send_handler, + size_t send_que_max) + : sending_(false), send_handler_(send_handler), + send_que_max_(send_que_max) { + if (!send_handler) { + isc_throw(NcrSenderError, + "NameChangeSender ctor - send_handler cannot be null"); + } + + // Queue size must be big enough to hold at least 1 entry. + if (send_que_max == 0) { + isc_throw(NcrSenderError, "NameChangeSender ctor -" + " queue size must be greater than zero"); + } +}; + +void +NameChangeSender::startSending(isc::asiolink::IOService & io_service) { + if (amSending()) { + // This amounts to a programmatic error. + isc_throw(NcrSenderError, "NameChangeSender is already sending"); + } + + // Clear send marker. + ncr_to_send_.reset(); + + // Call implementation dependent open. + try { + open(io_service); + } catch (const isc::Exception& ex) { + stopSending(); + isc_throw(NcrSenderOpenError, "Open failed:" << ex.what()); + } + + // Set our status to sending. + setSending(true); +} + +void +NameChangeSender::stopSending() { + try { + // Call implementation dependent close. + close(); + } catch (const isc::Exception &ex) { + // Swallow exceptions. If we have some sort of error we'll log + // it but we won't propagate the throw. + LOG_ERROR(dctl_logger, DHCP_DDNS_NCR_UDP_SEND_CLOSE).arg(ex.what()); + } + setSending(false); +} + +void +NameChangeSender::sendRequest(NameChangeRequestPtr ncr) { + if (!amSending()) { + isc_throw(NcrSenderError, "sender is not ready to send"); + } + + if (!ncr) { + isc_throw(NcrSenderError, "request to send is empty"); + } + + if (send_que_.size() >= send_que_max_) { + isc_throw(NcrSenderQueFull, "send queue has reached maximum capacity:" + << send_que_max_ ); + } + + // Put it on the queue. + send_que_.push_back(ncr); + + // Call sendNext to schedule the next one to go. + sendNext(); +} + +void +NameChangeSender::sendNext() { + if (ncr_to_send_) { + // @todo Not sure if there is any risk of getting stuck here but + // an interval timer to defend would be good. + // In reality, the derivation should ensure they timeout themselves + return; + } + + // If queue isn't empty, then get one from the front. Note we leave + // it on the front of the queue until we successfully send it. + if (send_que_.size()) { + ncr_to_send_ = send_que_.front(); + + // @todo start defense timer + // If a send were to hang and we timed it out, then timeout + // handler need to cycle thru open/close ? + + // Call implementation dependent send. + doSend(ncr_to_send_); + } +} + +void +NameChangeSender::invokeSendHandler(NameChangeSender::Result result) { + // @todo reset defense timer + if (result == SUCCESS) { + // It shipped so pull it off the queue. + send_que_.pop_front(); + } + + // Invoke the completion handler passing in the result and a pointer + // the request involved. + (*(const_cast(send_handler_))) (result, ncr_to_send_); + + // Clear the pending ncr pointer. + ncr_to_send_.reset(); + + // Set up the next send + sendNext(); +}; + +void +NameChangeSender::skipNext() { + if (send_que_.size()) { + // Discards the request at the front of the queue. + send_que_.pop_front(); + } +} + +void +NameChangeSender::flushSendQue() { + if (amSending()) { + isc_throw(NcrSenderError, "Cannot flush queue while sending"); + } + + send_que_.clear(); +} + +} // namespace isc::d2 +} // namespace isc diff --git a/src/bin/d2/ncr_io.h b/src/bin/d2/ncr_io.h new file mode 100644 index 0000000000..26853f6360 --- /dev/null +++ b/src/bin/d2/ncr_io.h @@ -0,0 +1,611 @@ +// 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 NCR_IO_H +#define NCR_IO_H + +/// @file ncr_io.h +/// @brief This file defines abstract classes for exchanging NameChangeRequests. +/// +/// These classes are used for sending and receiving requests to update DNS +/// information for FQDNs embodied as NameChangeRequests (aka NCRs). Ultimately,/// NCRs must move through the following three layers in order to implement +/// DHCP-DDNS: +/// +/// * Application layer - the business layer which needs to +/// transport NameChangeRequests, and is unaware of the means by which +/// they are transported. +/// +/// * IO layer - the low-level layer that is directly responsible for +/// sending and receiving data asynchronously and is supplied through +/// other libraries. This layer is largely unaware of the nature of the +/// data being transmitted. In other words, it doesn't know beans about +/// NCRs. +/// +/// * NameChangeRequest layer - This is the layer which acts as the +/// intermediary between the Application layer and the IO layer. It must +/// be able to move NameChangeRequests to the IO layer as raw data and move +/// raw data from the IO layer in the Application layer as +/// NameChangeRequests. +/// +/// The abstract classes defined here implement implement the latter, middle +/// layer, the NameChangeRequest layer. There are two types of participants +/// in this middle ground: +/// +/// * listeners - Receive NCRs from one or more sources. The DHCP-DDNS +/// application, (aka D2), is a listener. Listeners are embodied by the +/// class, NameChangeListener. +/// +/// * senders - sends NCRs to a given target. DHCP servers are senders. +/// Senders are embodied by the class, NameChangeListener. +/// +/// These two classes present a public interface for asynchronous +/// communications that is independent of the IO layer mechanisms. While the +/// type and details of the IO mechanism are not relevant to either class, it +/// is presumed to use isc::asiolink library for asynchronous event processing. +/// + +#include +#include +#include +#include + +#include + +namespace isc { +namespace d2 { + +/// @brief Exception thrown if an NcrListenerError occurs. +class NcrListenerError : public isc::Exception { +public: + NcrListenerError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief Exception thrown if an error occurs during IO source open. +class NcrListenerOpenError : public isc::Exception { +public: + NcrListenerOpenError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief Exception thrown if an error occurs initiating an IO receive. +class NcrListenerReceiveError : public isc::Exception { +public: + NcrListenerReceiveError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + + +/// @brief Abstract interface for receiving NameChangeRequests. +/// +/// NameChangeListener provides the means to: +/// - Supply a callback to invoke upon receipt of an NCR or a listening +/// error +/// - Start listening using a given IOService instance to process events +/// - Stop listening +/// +/// It implements the high level logic flow to listen until a request arrives, +/// invoke the implementation's handler and return to listening for the next +/// request. +/// +/// It provides virtual methods that allow derivations supply implementations +/// to open the appropriate IO source, perform a listen, and close the IO +/// source. +/// +/// The overall design is based on a callback chain. The listener's caller (the +/// application) supplies an "application" layer callback through which it will +/// receive inbound NameChangeRequests. The listener derivation will supply +/// its own callback to the IO layer to process receive events from its IO +/// source. This is referred to as the NameChangeRequest completion handler. +/// It is through this handler that the NameChangeRequest layer gains access +/// to the low level IO read service results. It is expected to assemble +/// NameChangeRequests from the inbound data and forward them to the +/// application layer by invoking the application layer callback registered +/// with the listener. +/// +/// The application layer callback is structured around a nested class, +/// RequestReceiveHandler. It consists of single, abstract operator() which +/// accepts a result code and a pointer to a NameChangeRequest as parameters. +/// In order to receive inbound NCRs, a caller implements a derivation of the +/// RequestReceiveHandler and supplies an instance of this derivation to the +/// NameChangeListener constructor. This "registers" the handler with the +/// listener. +/// +/// To begin listening, the caller invokes the listener's startListener() +/// method, passing in an IOService instance. This in turn will pass the +/// IOService into the virtual method, open(). The open method is where the +/// listener derivation performs the steps necessary to prepare its IO source +/// for reception (e.g. opening a socket, connecting to a database). +/// +/// Assuming the open is successful, startListener will call the virtual +/// doReceive() method. The listener derivation uses this method to +/// instigate an IO layer asynchronous passing in its IO layer callback to +/// handle receive events from its IO source. +/// +/// As stated earlier, the derivation's NameChangeRequest completion handler +/// MUST invoke the application layer handler registered with the listener. +/// This is done by passing in either a success status and a populated +/// NameChangeRequest or an error status and an empty request into the +/// listener's invokeRecvHandler method. This is the mechanism by which the +/// listener's caller is handed inbound NCRs. +class NameChangeListener { +public: + + /// @brief Defines the outcome of an asynchronous NCR receive + enum Result { + SUCCESS, + TIME_OUT, + STOPPED, + ERROR + }; + + /// @brief Abstract class for defining application layer receive callbacks. + /// + /// Applications which will receive NameChangeRequests must provide a + /// derivation of this class to the listener constructor in order to + /// receive NameChangeRequests. + class RequestReceiveHandler { + public: + /// @brief Function operator implementing a NCR receive callback. + /// + /// This method allows the application to receive the inbound + /// NameChangeRequests. It is intended to function as a hand off of + /// information and should probably not be time-consuming. + /// + /// @param result contains that receive outcome status. + /// @param ncr is a pointer to the newly received NameChangeRequest if + /// result is NameChangeListener::SUCCESS. It is indeterminate other + /// wise. + /// @throw This method MUST NOT throw. + virtual void operator ()(Result result, NameChangeRequestPtr ncr) = 0; + }; + + /// @brief Constructor + /// + /// @param recv_handler is a pointer the application layer handler to be + /// invoked each time a NCR is received or a receive error occurs. + /// + /// @throw throws NcrListenerError if recv_handler is NULL. + NameChangeListener(const RequestReceiveHandler* recv_handler); + + /// @brief Destructor + virtual ~NameChangeListener() { + }; + + /// @brief Prepares the IO for reception and initiates the first receive. + /// + /// Calls the derivation's open implementation to initialize the IO layer + /// source for receiving inbound requests. If successful, it issues first + /// asynchronous read by calling the derivation's doReceive implementation. + /// + /// @param io_service is the IOService that will handle IO event processing. + /// + /// @throw throws NcrListenError if the listener is already "listening" or + /// in the event the open or doReceive methods fail. + void startListening(isc::asiolink::IOService& io_service); + + /// @brief Closes the IO source and stops listen logic. + /// + /// Calls the derivation's implementation of close and marks the state + /// as not listening. + void stopListening(); + + /// @brief Calls the NCR receive handler registered with the listener. + /// + /// This is the hook by which the listener's caller's NCR receive handler + /// is called. This method MUST be invoked by the derivation's + /// implementation of doReceive. + /// + /// @param result contains that receive outcome status. + /// @param ncr is a pointer to the newly received NameChangeRequest if + /// result is NameChangeListener::SUCCESS. It is indeterminate other + /// wise. + void invokeRecvHandler(Result result, NameChangeRequestPtr ncr); + + /// @brief Abstract method which opens the IO source for reception. + /// + /// The derivation uses this method to perform the steps needed to + /// prepare the IO source to receive requests. + /// + /// @param io_service is the IOService that process IO events. + /// + /// @throw If the implementation encounters an error it MUST + /// throw it as an isc::Exception or derivative. + virtual void open(isc::asiolink::IOService& io_service) = 0; + + /// @brief Abstract method which closes the IO source. + /// + /// The derivation uses this method to perform the steps needed to + /// "close" the IO source. + /// + /// @throw If the implementation encounters an error it MUST + /// throw it as an isc::Exception or derivative. + virtual void close() = 0; + + /// @brief Initiates an IO layer asynchronous read. + /// + /// The derivation uses this method to perform the steps needed to + /// initiate an asynchronous read of the IO source with the + /// derivation's IO layer handler as the IO completion callback. + /// + /// @throw If the implementation encounters an error it MUST + /// throw it as an isc::Exception or derivative. + virtual void doReceive() = 0; + + /// @brief Returns true if the listener is listening, false otherwise. + /// + /// A true value indicates that the IO source has been opened successfully, + /// and that receive loop logic is active. + bool amListening() const { + return (listening_); + } + +private: + /// @brief Sets the listening indicator to the given value. + /// + /// Note, this method is private as it is used the base class is solely + /// responsible for managing the state. + /// + /// @param value is the new value to assign to the indicator. + void setListening(bool value) { + listening_ = value; + } + + /// @brief Indicates if the listener is listening. + bool listening_; + + /// @brief Application level NCR receive completion handler. + const RequestReceiveHandler* recv_handler_; +}; + +/// @brief Defines a smart pointer to an instance of a listener. +typedef boost::shared_ptr NameChangeListenerPtr; + + +/// @brief Thrown when a NameChangeSender encounters an error. +class NcrSenderError : public isc::Exception { +public: + NcrSenderError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief Exception thrown if an error occurs during IO source open. +class NcrSenderOpenError : public isc::Exception { +public: + NcrSenderOpenError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief Exception thrown if an error occurs initiating an IO send. +class NcrSenderQueFull : public isc::Exception { +public: + NcrSenderQueFull(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief Exception thrown if an error occurs initiating an IO send. +class NcrSenderSendError : public isc::Exception { +public: + NcrSenderSendError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + + +/// @brief Abstract interface for sending NameChangeRequests. +/// +/// NameChangeSender provides the means to: +/// - Supply a callback to invoke upon completing the delivery of an NCR or a +/// send error +/// - Start sending using a given IOService instance to process events +/// - Queue NCRs for delivery +/// - Stop sending +/// +/// It implements the high level logic flow to queue requests for delivery, +/// and ship them one at a time, waiting for the send to complete prior to +/// sending the next request in the queue. If a send fails, the request +/// will remain at the front of queue and will be the send will be retried +/// endlessly unless the caller dequeues the request. Note, it is presumed that +/// a send failure is some form of IO error such as loss of connectivity and +/// not a message content error. It should not be possible to queue an invalid +/// message. +/// +/// It should be noted that once a request is placed onto the send queue it +/// will remain there until one of three things occur: +/// * It is successfully delivered +/// * @c NameChangeSender::skipNext() is called +/// * @c NameChangeSender::flushSendQue() is called +/// +/// The queue contents are preserved across start and stop listening +/// transitions. This is to provide for error recovery without losing +/// undelivered requests. + +/// It provides virtual methods so derivations may supply implementations to +/// open the appropriate IO sink, perform a send, and close the IO sink. +/// +/// The overall design is based on a callback chain. The sender's caller (the +/// application) supplies an "application" layer callback through which it will +/// be given send completion notifications. The sender derivation will employ +/// its own callback at the IO layer to process send events from its IO sink. +/// This callback is expected to forward the outcome of each asynchronous send +/// to the application layer by invoking the application layer callback +/// registered with the sender. +/// +/// The application layer callback is structured around a nested class, +/// RequestSendHandler. It consists of single, abstract operator() which +/// accepts a result code and a pointer to a NameChangeRequest as parameters. +/// In order to receive send completion notifications, a caller implements a +/// derivation of the RequestSendHandler and supplies an instance of this +/// derivation to the NameChangeSender constructor. This "registers" the +/// handler with the sender. +/// +/// To begin sending, the caller invokes the listener's startSending() +/// method, passing in an IOService instance. This in turn will pass the +/// IOService into the virtual method, open(). The open method is where the +/// sender derivation performs the steps necessary to prepare its IO sink for +/// output (e.g. opening a socket, connecting to a database). At this point, +/// the sender is ready to send messages. +/// +/// In order to send a request, the application layer invokes the sender +/// method, sendRequest(), passing in the NameChangeRequest to send. This +/// method places the request onto the back of the send queue, and then invokes +/// the sender method, sendNext(). +/// +/// If there is already a send in progress when sendNext() is called, the method +/// will return immediately rather than initiate the next send. This is to +/// ensure that sends are processed sequentially. +/// +/// If there is not a send in progress and the send queue is not empty, +/// the sendNext method will pass the NCR at the front of the send queue into +/// the virtual doSend() method. +/// +/// The sender derivation uses this doSend() method to instigate an IO layer +/// asynchronous send with its IO layer callback to handle send events from its +/// IO sink. +/// +/// As stated earlier, the derivation's IO layer callback MUST invoke the +/// application layer handler registered with the sender. This is done by +/// passing in a status indicating the outcome of the send into the sender's +/// invokeSendHandler method. This is the mechanism by which the sender's +/// caller is handed outbound notifications. + +/// After invoking the application layer handler, the invokeSendHandler method +/// will call the sendNext() method to initiate the next send. This ensures +/// that requests continue to dequeue and ship. +/// +class NameChangeSender { +public: + + /// @brief Defines the type used for the request send queue. + typedef std::deque SendQue; + + /// @brief Defines a default maximum number of entries in the send queue. + static const size_t MAX_QUE_DEFAULT = 1024; + + /// @brief Defines the outcome of an asynchronous NCR send. + enum Result { + SUCCESS, + TIME_OUT, + STOPPED, + ERROR + }; + + /// @brief Abstract class for defining application layer send callbacks. + /// + /// Applications which will send NameChangeRequests must provide a + /// derivation of this class to the sender constructor in order to + /// receive send outcome notifications. + class RequestSendHandler { + public: + /// @brief Function operator implementing a NCR send callback. + /// + /// This method allows the application to receive the outcome of + /// each send. It is intended to function as a hand off of information + /// and should probably not be time-consuming. + /// + /// @param result contains that send outcome status. + /// @param ncr is a pointer to the NameChangeRequest that was + /// delivered (or attempted). + /// + /// @throw This method MUST NOT throw. + virtual void operator ()(Result result, NameChangeRequestPtr ncr) = 0; + }; + + /// @brief Constructor + /// + /// @param send_handler is a pointer the application layer handler to be + /// invoked each time a NCR send attempt completes. + /// @param send_que_max is the maximum number of entries allowed in the + /// send queue. Once the maximum number is reached, all calls to + /// sendRequest will fail with an exception. + /// + /// @throw throws NcrSenderError if send_handler is NULL. + NameChangeSender(const RequestSendHandler* send_handler, + size_t send_que_max = MAX_QUE_DEFAULT); + + /// @brief Destructor + virtual ~NameChangeSender() { + } + + /// @brief Prepares the IO for transmission. + /// + /// Calls the derivation's open implementation to initialize the IO layer + /// sink for sending outbound requests. + /// + /// @param io_service is the IOService that will handle IO event processing. + /// + /// @throw throws NcrSenderError if the sender is already "sending" or + /// NcrSenderOpenError if the open fails. + void startSending(isc::asiolink::IOService & io_service); + + /// @brief Closes the IO sink and stops send logic. + /// + /// Calls the derivation's implementation of close and marks the state + /// as not sending. + void stopSending(); + + /// @brief Queues the given request to be sent. + /// + /// The given request is placed at the back of the send queue and then + /// sendNext is invoked. + /// + /// @param ncr is the NameChangeRequest to send. + /// + /// @throw throws NcrSenderError if the sender is not in sending state or + /// the request is empty; NcrSenderQueFull if the send queue has reached + /// capacity. + void sendRequest(NameChangeRequestPtr ncr); + + /// @brief Dequeues and sends the next request on the send queue. + /// + /// If there is already a send in progress just return. If there is not + /// a send in progress and the send queue is not empty the grab the next + /// message on the front of the queue and call doSend(). + /// + void sendNext(); + + /// @brief Calls the NCR send completion handler registered with the + /// sender. + /// + /// This is the hook by which the sender's caller's NCR send completion + /// handler is called. This method MUST be invoked by the derivation's + /// implementation of doSend. Note that if the send was a success, + /// the entry at the front of the queue is removed from the queue. + /// If not we leave it there so we can retry it. After we invoke the + /// handler we clear the pending ncr value and queue up the next send. + /// + /// @param result contains that send outcome status. + void invokeSendHandler(NameChangeSender::Result result); + + /// @brief Removes the request at the front of the send queue + /// + /// This method can be used to avoid further retries of a failed + /// send. It is provided primarily as a just-in-case measure. Since + /// a failed send results in the same request being retried continuously + /// this method makes it possible to remove that entry, causing the + /// subsequent entry in the queue to be attempted on the next send. + /// It is presumed that sends will only fail due to some sort of + /// communications issue. In the unlikely event that a request is + /// somehow tainted and causes an send failure based on its content, + /// this method provides a means to remove th message. + void skipNext(); + + /// @brief Flushes all entries in the send queue + /// + /// This method can be used to flush all of the NCRs currently in the + /// the send queue. Note it may not be called while the sender is in + /// the sending state. + /// @throw throws NcrSenderError if called and sender is in sending state. + void flushSendQue(); + + /// @brief Abstract method which opens the IO sink for transmission. + /// + /// The derivation uses this method to perform the steps needed to + /// prepare the IO sink to send requests. + /// + /// @param io_service is the IOService that process IO events. + /// + /// @throw If the implementation encounters an error it MUST + /// throw it as an isc::Exception or derivative. + virtual void open(isc::asiolink::IOService& io_service) = 0; + + /// @brief Abstract method which closes the IO sink. + /// + /// The derivation uses this method to perform the steps needed to + /// "close" the IO sink. + /// + /// @throw If the implementation encounters an error it MUST + /// throw it as an isc::Exception or derivative. + virtual void close() = 0; + + /// @brief Initiates an IO layer asynchronous send + /// + /// The derivation uses this method to perform the steps needed to + /// initiate an asynchronous send through the IO sink of the given NCR. + /// + /// @param ncr is a pointer to the NameChangeRequest to send. + /// derivation's IO layer handler as the IO completion callback. + /// + /// @throw If the implementation encounters an error it MUST + /// throw it as an isc::Exception or derivative. + virtual void doSend(NameChangeRequestPtr ncr) = 0; + + /// @brief Returns true if the sender is in send mode, false otherwise. + /// + /// A true value indicates that the IO sink has been opened successfully, + /// and that send loop logic is active. + bool amSending() const { + return (sending_); + } + + /// @brief Returns true when a send is in progress. + /// + /// A true value indicates that a request is actively in the process of + /// being delivered. + bool isSendInProgress() const { + return ((ncr_to_send_) ? true : false); + } + + /// @brief Returns the request that is in the process of being sent. + /// + /// The pointer returned by this method will be populated with the + /// request that has been passed into doSend() and for which the + /// completion callback has not yet been invoked. + const NameChangeRequestPtr& getNcrToSend() { + return (ncr_to_send_); + } + + /// @brief Returns the maximum number of entries allowed in the send queue. + size_t getQueMaxSize() const { + return (send_que_max_); + } + + /// @brief Returns the number of entries currently in the send queue. + size_t getQueSize() const { + return (send_que_.size()); + } + +private: + /// @brief Sets the sending indicator to the given value. + /// + /// Note, this method is private as it is used the base class is solely + /// responsible for managing the state. + /// + /// @param value is the new value to assign to the indicator. + void setSending(bool value) { + sending_ = value; + } + + /// @brief Boolean indicator which tracks sending status. + bool sending_; + + /// @brief A pointer to regisetered send completion handler. + const RequestSendHandler* send_handler_; + + /// @brief Maximum number of entries permitted in the send queue. + size_t send_que_max_; + + /// @brief Queue of the requests waiting to be sent. + SendQue send_que_; + + /// @brief Pointer to the request which is in the process of being sent. + NameChangeRequestPtr ncr_to_send_; +}; + +/// @brief Defines a smart pointer to an instance of a sender. +typedef boost::shared_ptr NameChangeSenderPtr; + +} // namespace isc::d2 +} // namespace isc + +#endif diff --git a/src/bin/d2/ncr_udp.cc b/src/bin/d2/ncr_udp.cc new file mode 100644 index 0000000000..bf14f1ff1b --- /dev/null +++ b/src/bin/d2/ncr_udp.cc @@ -0,0 +1,305 @@ +// 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 +#include + +#include +#include +#include + +namespace isc { +namespace d2 { + + +//*************************** UDPCallback *********************** + +UDPCallback::UDPCallback (RawBufferPtr buffer, size_t buf_size, + UDPEndpointPtr data_source, + const UDPCompletionHandler& handler) + : handler_(handler), data_(new Data(buffer, buf_size, data_source)) { + if (handler.empty()) { + isc_throw(NcrUDPError, "UDPCallback - handler can't be null"); + } + + if (!buffer) { + isc_throw(NcrUDPError, "UDPCallback - buffer can't be null"); + } +} + +void +UDPCallback::operator ()(const asio::error_code error_code, + const size_t bytes_transferred) { + + // Save the result state and number of bytes transferred. + setErrorCode(error_code); + setBytesTransferred(bytes_transferred); + + // Invoke the NameChangeRequest layer completion handler. + // First argument is a boolean indicating success or failure. + // The second is a pointer to "this" callback object. By passing + // ourself in, we make all of the service related data available + // to the completion handler. + handler_(!error_code, this); +} + +void +UDPCallback::putData(const uint8_t* src, size_t len) { + if (!src) { + isc_throw(NcrUDPError, "UDPCallback putData, data source is NULL"); + } + + if (len > data_->buf_size_) { + isc_throw(NcrUDPError, "UDPCallback putData, data length too large"); + } + + memcpy (data_->buffer_.get(), src, len); + data_->put_len_ = len; +} + + +//*************************** NameChangeUDPListener *********************** + +NameChangeUDPListener::NameChangeUDPListener( + const isc::asiolink::IOAddress& ip_address, const uint32_t port, + NameChangeFormat format, + const RequestReceiveHandler* ncr_recv_handler, + const bool reuse_address) + : NameChangeListener(ncr_recv_handler), ip_address_(ip_address), + port_(port), format_(format), reuse_address_(reuse_address) { + // Instantiate the receive callback. This gets passed into each receive. + // Note that the callback constructor is passed an instance method + // pointer to our recv_completion_handler. + recv_callback_.reset(new UDPCallback( + RawBufferPtr(new uint8_t[RECV_BUF_MAX]), + RECV_BUF_MAX, + UDPEndpointPtr(new asiolink:: + UDPEndpoint()), + boost::bind(&NameChangeUDPListener:: + recv_completion_handler, this, + _1, _2))); +} + +NameChangeUDPListener::~NameChangeUDPListener() { + // Clean up. + stopListening(); +} + +void +NameChangeUDPListener::open(isc::asiolink::IOService& io_service) { + // create our endpoint and bind the the low level socket to it. + isc::asiolink::UDPEndpoint endpoint(ip_address_.getAddress(), port_); + + // Create the low level socket. + try { + asio_socket_.reset(new asio::ip::udp:: + socket(io_service.get_io_service(), + (ip_address_.isV4() ? asio::ip::udp::v4() : + asio::ip::udp::v6()))); + + // If in test mode, enable address reuse. + if (reuse_address_) { + asio_socket_->set_option(asio::socket_base::reuse_address(true)); + } + + // Bind the low leve socket to our endpoint. + asio_socket_->bind(endpoint.getASIOEndpoint()); + } catch (asio::system_error& ex) { + isc_throw (NcrUDPError, ex.code().message()); + } + + // Create the asiolink socket from the low level socket. + socket_.reset(new NameChangeUDPSocket(*asio_socket_)); +} + + +void +NameChangeUDPListener::doReceive() { + // Call the socket's asychronous receiving, passing ourself in as callback. + RawBufferPtr recv_buffer = recv_callback_->getBuffer(); + socket_->asyncReceive(recv_buffer.get(), recv_callback_->getBufferSize(), + 0, recv_callback_->getDataSource().get(), + *recv_callback_); +} + +void +NameChangeUDPListener::close() { + // Whether we think we are listening or not, make sure we aren't. + // Since we are managing our own socket, we need to cancel and close + // it ourselves. + if (asio_socket_) { + try { + asio_socket_->cancel(); + asio_socket_->close(); + } catch (asio::system_error& ex) { + // It is really unlikely that this will occur. + // If we do reopen later it will be with a new socket instance. + // Repackage exception as one that is conformant with the interface. + isc_throw (NcrUDPError, ex.code().message()); + } + } +} + +void +NameChangeUDPListener::recv_completion_handler(bool successful, + const UDPCallback *callback) { + NameChangeRequestPtr ncr; + Result result = SUCCESS; + + if (successful) { + // Make an InputBuffer from our internal array + isc::util::InputBuffer input_buffer(callback->getData(), + callback->getBytesTransferred()); + + try { + ncr = NameChangeRequest::fromFormat(format_, input_buffer); + } catch (const NcrMessageError& ex) { + // log it and go back to listening + LOG_ERROR(dctl_logger, DHCP_DDNS_INVALID_NCR).arg(ex.what()); + + // Queue up the next recieve. + doReceive(); + return; + } + } else { + asio::error_code error_code = callback->getErrorCode(); + LOG_ERROR(dctl_logger, DHCP_DDNS_NCR_UDP_RECV_ERROR) + .arg(error_code.message()); + result = ERROR; + } + + // Call the application's registered request receive handler. + invokeRecvHandler(result, ncr); +} + + +//*************************** NameChangeUDPSender *********************** + +NameChangeUDPSender::NameChangeUDPSender( + const isc::asiolink::IOAddress& ip_address, const uint32_t port, + const isc::asiolink::IOAddress& server_address, + const uint32_t server_port, const NameChangeFormat format, + RequestSendHandler* ncr_send_handler, const size_t send_que_max, + const bool reuse_address) + : NameChangeSender(ncr_send_handler, send_que_max), + ip_address_(ip_address), port_(port), server_address_(server_address), + server_port_(server_port), format_(format), + reuse_address_(reuse_address) { + // Instantiate the send callback. This gets passed into each send. + // Note that the callback constructor is passed the an instance method + // pointer to our send_completion_handler. + send_callback_.reset(new UDPCallback( + RawBufferPtr(new uint8_t[SEND_BUF_MAX]), + SEND_BUF_MAX, + UDPEndpointPtr(new asiolink:: + UDPEndpoint()), + boost::bind(&NameChangeUDPSender:: + send_completion_handler, this, + _1, _2))); +} + +NameChangeUDPSender::~NameChangeUDPSender() { + // Clean up. + stopSending(); +} + +void +NameChangeUDPSender::open(isc::asiolink::IOService & io_service) { + // create our endpoint and bind the the low level socket to it. + isc::asiolink::UDPEndpoint endpoint(ip_address_.getAddress(), port_); + + // Create the low level socket. + try { + asio_socket_.reset(new asio::ip::udp:: + socket(io_service.get_io_service(), + (ip_address_.isV4() ? asio::ip::udp::v4() : + asio::ip::udp::v6()))); + + // If in test mode, enable address reuse. + if (reuse_address_) { + asio_socket_->set_option(asio::socket_base::reuse_address(true)); + } + + // Bind the low leve socket to our endpoint. + asio_socket_->bind(endpoint.getASIOEndpoint()); + } catch (asio::system_error& ex) { + isc_throw (NcrUDPError, ex.code().message()); + } + + // Create the asiolink socket from the low level socket. + socket_.reset(new NameChangeUDPSocket(*asio_socket_)); + + // Create the server endpoint + server_endpoint_.reset(new isc::asiolink:: + UDPEndpoint(server_address_.getAddress(), + server_port_)); + + send_callback_->setDataSource(server_endpoint_); +} + +void +NameChangeUDPSender::close() { + // Whether we think we are sending or not, make sure we aren't. + // Since we are managing our own socket, we need to cancel and close + // it ourselves. + if (asio_socket_) { + try { + asio_socket_->cancel(); + asio_socket_->close(); + } catch (asio::system_error& ex) { + // It is really unlikely that this will occur. + // If we do reopen later it will be with a new socket instance. + // Repackage exception as one that is conformant with the interface. + isc_throw (NcrUDPError, ex.code().message()); + } + } +} + +void +NameChangeUDPSender::doSend(NameChangeRequestPtr ncr) { + // Now use the NCR to write JSON to an output buffer. + isc::util::OutputBuffer ncr_buffer(SEND_BUF_MAX); + ncr->toFormat(format_, ncr_buffer); + + // Copy the wire-ized request to callback. This way we know after + // send completes what we sent (or attempted to send). + send_callback_->putData(static_cast(ncr_buffer.getData()), + ncr_buffer.getLength()); + + // Call the socket's asychronous send, passing our callback + socket_->asyncSend(send_callback_->getData(), send_callback_->getPutLen(), + send_callback_->getDataSource().get(), *send_callback_); +} + +void +NameChangeUDPSender::send_completion_handler(const bool successful, + const UDPCallback *send_callback) { + Result result; + if (successful) { + result = SUCCESS; + } + else { + // On a failure, log the error and set the result to ERROR. + asio::error_code error_code = send_callback->getErrorCode(); + LOG_ERROR(dctl_logger, DHCP_DDNS_NCR_UDP_RECV_ERROR) + .arg(error_code.message()); + + result = ERROR; + } + + // Call the application's registered request send handler. + invokeSendHandler(result); +} +}; // end of isc::d2 namespace +}; // end of isc namespace diff --git a/src/bin/d2/ncr_udp.h b/src/bin/d2/ncr_udp.h new file mode 100644 index 0000000000..b0b6ad5bf3 --- /dev/null +++ b/src/bin/d2/ncr_udp.h @@ -0,0 +1,564 @@ +// 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 NCR_UDP_LISTENER_H +#define NCR_UDP_LISTENER_H + +/// @file ncr_udp.h +/// @brief This file provides UDP socket based implementation for sending and +/// receiving NameChangeRequests +/// +/// These classes are derived from the abstract classes, NameChangeListener +/// and NameChangeSender (see ncr_io.h). +/// +/// The following discussion will refer to three layers of communications: +/// +/// * Application layer - This is the business layer which needs to +/// transport NameChangeRequests, and is unaware of the means by which +/// they are transported. +/// +/// * IO layer - This is the low-level layer that is directly responsible +/// for sending and receiving data asynchronously and is supplied through +/// other libraries. This layer is largely unaware of the nature of the +/// data being transmitted. In other words, it doesn't know beans about +/// NCRs. +/// +/// * NameChangeRequest layer - This is the layer which acts as the +/// intermediary between the Application layer and the IO layer. It must +/// be able to move NameChangeRequests to the IO layer as raw data and move +/// raw data from the IO layer in the Application layer as +/// NameChangeRequests. +/// +/// This file defines NameChangeUDPListener class for receiving NCRs, and +/// NameChangeUDPSender for sending NCRs. +/// +/// Both the listener and sender implementations utilize the same underlying +/// construct to move NCRs to and from a UDP socket. This construct consists +/// of a set of classes centered around isc::asiolink::UDPSocket. UDPSocket +/// is a templated class that supports asio asynchronous event processing; and +/// which accepts as its parameter, the name of a callback class. +/// +/// The asynchronous services provided by UDPSocket typically accept a buffer +/// for transferring data (either in or out depending on the service direction) +/// and an object which supplies a callback to invoke upon completion of the +/// service. +/// +/// The callback class must provide an operator() with the following signature: +/// @code +/// void operator ()(const asio::error_code error_code, +/// const size_t bytes_transferred); +/// @endcode +/// +/// Upon completion of the service, the callback instance's operator() is +/// invoked by the aiso layer. It is given both a outcome result and the +/// number of bytes either read or written, to or from the buffer supplied +/// to the service. +/// +/// Typically, an asiolink based implementation would simply implement the +/// callback operator directly. However, the nature of the asiolink library +/// is such that the callback object may be copied several times during course +/// of a service invocation. This implies that any class being used as a +/// callback class must be copyable. This is not always desirable. In order +/// to separate the callback class from the NameChangeRequest, the construct +/// defines the UDPCallback class for use as a copyable, callback object. +/// +/// The UDPCallback class provides the asiolink layer callback operator(), +/// which is invoked by the asiolink layer upon service completion. It +/// contains: +/// * a pointer to the transfer buffer +/// * the capacity of the transfer buffer +/// * a IO layer outcome result +/// * the number of bytes transferred +/// * a method pointer to a NameChangeRequest layer completion handler +/// +/// This last item, is critical. It points to an instance method that +/// will be invoked by the UDPCallback operator. This provides access to +/// the outcome of the service call to the NameChangeRequest layer without +/// that layer being used as the actual callback object. +/// +/// The completion handler method signature is codified in the typedef, +/// UDPCompletionHandler, and must be as follows: +/// +/// @code +/// void(const bool, const UDPCallback*) +/// @endcode +/// +/// Note that is accepts two parameters. The first is a boolean indicator +/// which indicates if the service call completed successfully or not. The +/// second is a pointer to the callback object invoked by the IOService upon +/// completion of the service. The callback instance will contain all of the +/// pertinent information about the invocation and outcome of the service. +/// +/// Using the contents of the callback, it is the responsibility of the +/// UDPCompletionHandler to interpret the results of the service invocation and +/// pass the interpretation to the application layer via either +/// NameChangeListener::invokeRecvHandler in the case of the UDP listener, or +/// NameChangeSender::invokeSendHandler in the case of UDP sender. +/// +#include +#include +#include +#include +#include +#include +#include + +#include + +/// responsibility of the completion handler to perform the steps necessary +/// to interpret the raw data provided by the service outcome. The +/// UDPCallback operator implementation is mostly a pass through. +/// +namespace isc { +namespace d2 { + +/// @brief Thrown when a UDP level exception occurs. +class NcrUDPError : public isc::Exception { +public: + NcrUDPError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +class UDPCallback; +/// @brief Defines a function pointer for NameChangeRequest completion handlers. +typedef boost::function + UDPCompletionHandler; + +/// @brief Defines a dynamically allocated shared array. +typedef boost::shared_array RawBufferPtr; + +typedef boost::shared_ptr UDPEndpointPtr; + +/// @brief Implements the callback class passed into UDPSocket calls. +/// +/// It serves as the link between the asiolink::UDPSocket asynchronous services +/// and the NameChangeRequest layer. The class provides the asiolink layer +/// callback operator(), which is invoked by the asiolink layer upon service +/// completion. It contains all of the data pertinent to both the invocation +/// and completion of a service, as well as a pointer to NameChangeRequest +/// layer completion handler to invoke. +/// +class UDPCallback { + +public: + /// @brief Container class which stores service invocation related data. + /// + /// Because the callback object may be copied numerous times during the + /// course of service invocation, it does not directly contain data values. + /// Rather it will retain a shared pointer to an instance of this structure + /// thus ensuring that all copies of the callback object, ultimately refer + /// to the same data values. + struct Data { + + /// @brief Constructor + /// + /// @param buffer is a pointer to the data transfer buffer. This is + /// the buffer data will be written to on a read, or read from on a + /// send. + /// @param buf_size is the capacity of the buffer + /// @param data_source storage for UDP endpoint which supplied the data + Data(RawBufferPtr buffer, size_t buf_size, UDPEndpointPtr data_source) + : buffer_(buffer), buf_size_(buf_size), data_source_(data_source), + put_len_(0), error_code_(), bytes_transferred_(0) { + }; + + /// @brief A pointer to the data transfer buffer. + RawBufferPtr buffer_; + + /// @brief Storage capacity of the buffer. + size_t buf_size_; + + /// @brief The UDP endpoint that is the origin of the data transferred. + UDPEndpointPtr data_source_; + + /// @brief Stores this size of the data within the buffer when written + /// there manually. (See UDPCallback::putData()) . + size_t put_len_; + + /// @brief Stores the IO layer result code of the completed IO service. + asio::error_code error_code_; + + /// @brief Stores the number of bytes transferred by completed IO + /// service. + /// For a read it is the number of bytes written into the + /// buffer. For a write it is the number of bytes read from the + /// buffer. + size_t bytes_transferred_; + + }; + + /// @brief Used as the callback object for UDPSocket services. + /// + /// @param buffer is a pointer to the data transfer buffer. This is + /// the buffer data will be written to on a read, or read from on a + /// send. + /// @param buf_size is the capacity of the buffer + /// @param data_source storage for UDP endpoint which supplied the data + /// @param handler is a method pointer to the completion handler that + /// is to be called by the operator() implementation. + /// + /// @throw throws a NcrUDPError if either the handler or buffer pointers + /// are invalid. + UDPCallback (RawBufferPtr buffer, size_t buf_size, + UDPEndpointPtr data_source, + const UDPCompletionHandler& handler); + + /// @brief Operator that will be invoked by the asiolink layer. + /// + /// @param error_code is the IO layer result code of the + /// completed IO service. + /// @param bytes_transferred is the number of bytes transferred by + /// completed IO. + /// For a read it is the number of bytes written into the + /// buffer. For a write it is the number of bytes read from the + /// buffer. + void operator ()(const asio::error_code error_code, + const size_t bytes_transferred); + + /// @brief Returns the number of bytes transferred by the completed IO + /// service. + /// + /// For a read it is the number of bytes written into the + /// buffer. For a write it is the number of bytes read from the + /// buffer. + size_t getBytesTransferred() const { + return (data_->bytes_transferred_); + } + + /// @brief Sets the number of bytes transferred. + /// + /// @param value is the new value to assign to bytes transferred. + void setBytesTransferred(const size_t value) { + data_->bytes_transferred_ = value; + } + + /// @brief Returns the completed IO layer service outcome status. + asio::error_code getErrorCode() const { + return (data_->error_code_); + } + + /// @brief Sets the completed IO layer service outcome status. + /// + /// @param value is the new value to assign to outcome status. + void setErrorCode(const asio::error_code value) { + data_->error_code_ = value; + } + + /// @brief Returns the data transfer buffer. + RawBufferPtr getBuffer() const { + return (data_->buffer_); + } + + /// @brief Returns the data transfer buffer capacity. + const size_t getBufferSize() const { + return (data_->buf_size_); + } + + /// @brief Returns a pointer the data transfer buffer content. + const uint8_t* getData() const { + return (data_->buffer_.get()); + } + + /// @brief Copies data into the data transfer buffer. + /// + /// Copies the given number of bytes from the given source buffer + /// into the data transfer buffer, and updates the value of put length. + /// This method may be used when performing sends to make a copy of + /// the "raw data" that was shipped (or attempted) accessible to the + /// upstream callback. + /// + /// @param src is a pointer to the data source from which to copy + /// @param len is the number of bytes to copy + /// + /// @throw throws a NcrUDPError if the number of bytes to copy exceeds + /// the buffer capacity or if the source pointer is invalid. + void putData(const uint8_t* src, size_t len); + + /// @brief Returns the number of bytes manually written into the + /// transfer buffer. + const size_t getPutLen() const { + return (data_->put_len_); + } + + /// @brief Sets the data source to the given endpoint. + /// + /// @param endpoint is the new value to assign to data source. + void setDataSource(UDPEndpointPtr endpoint) { + data_->data_source_ = endpoint; + } + + /// @brief Returns the UDP endpoint that provided the transferred data. + UDPEndpointPtr getDataSource() { + return (data_->data_source_); + } + + private: + /// @brief NameChangeRequest layer completion handler to invoke. + UDPCompletionHandler handler_; + + /// @brief Shared pointer to the service data container. + boost::shared_ptr data_; +}; + +/// @brief Convenience type for UDP socket based listener +typedef isc::asiolink::UDPSocket NameChangeUDPSocket; + +/// @brief Provides the ability to receive NameChangeRequests via UDP socket +/// +/// This class is a derivation of the NameChangeListener which is capable of +/// receiving NameChangeRequests through a UDP socket. The caller need only +/// supply network addressing and a RequestReceiveHandler instance to receive +/// NameChangeRequests asynchronously. +class NameChangeUDPListener : public NameChangeListener { +public: + /// @brief Defines the maximum size packet that can be received. + static const size_t RECV_BUF_MAX = isc::asiolink:: + UDPSocket::MIN_SIZE; + + /// @brief Constructor + /// + /// @param ip_address is the network address on which to listen + /// @param port is the IP port on which to listen + /// @param format is the wire format of the inbound requests. Currently + /// only JSON is supported + /// @param ncr_recv_handler the receive handler object to notify when + /// when a receive completes. + /// @param reuse_address enables IP address sharing when true + /// It defaults to false. + /// + /// @throw base class throws NcrListenerError if handler is invalid. + NameChangeUDPListener(const isc::asiolink::IOAddress& ip_address, + const uint32_t port, + const NameChangeFormat format, + const RequestReceiveHandler* ncr_recv_handler, + const bool reuse_address = false); + + /// @brief Destructor. + virtual ~NameChangeUDPListener(); + + /// @brief Opens a UDP socket using the given IOService. + /// + /// Creates a NameChangeUDPSocket bound to the listener's ip address + /// and port, that is monitored by the given IOService instance. + /// + /// @param io_service the IOService which will monitor the socket. + /// + /// @throw throws a NcrUDPError if the open fails. + virtual void open(isc::asiolink::IOService& io_service); + + /// @brief Closes the UDPSocket. + /// + /// It first invokes the socket's cancel method which should stop any + /// pending read and remove the socket callback from the IOService. It + /// then calls the socket's close method to actually close the socket. + /// + /// @throw throws a NcrUDPError if the open fails. + virtual void close(); + + /// @brief Initiates an asynchronous read on the socket. + /// + /// Invokes the asyncReceive() method on the socket passing in the + /// recv_callback_ member's transfer buffer as the receive buffer, and + /// recv_callback_ itself as the callback object. + /// + /// @throw throws a NcrUDPError if the open fails. + void doReceive(); + + /// @brief Implements the NameChangeRequest level receive completion + /// handler. + /// + /// This method is invoked by the UPDCallback operator() implementation, + /// passing in the boolean success indicator and pointer to itself. + /// + /// If the indicator denotes success, then the method will attempt to + /// to construct a NameChangeRequest from the received data. If the + /// construction was successful, it will send the new NCR to the + /// application layer by calling invokeRecvHandler() with a success + /// status and a pointer to the new NCR. + /// + /// If the buffer contains invalid data such that construction fails, + /// the method will log the failure and then call doReceive() to start a + /// initiate the next receive. + /// + /// If the indicator denotes failure the method will log the failure and + /// notify the application layer by calling invokeRecvHandler() with + /// an error status and an empty pointer. + /// + /// @param successful boolean indicator that should be true if the + /// socket receive completed without error, false otherwise. + /// @param recv_callback pointer to the callback instance which handled + /// the socket receive completion. + void recv_completion_handler(bool successful, + const UDPCallback* recv_callback); +private: + /// @brief IP address on which to listen for requests. + isc::asiolink::IOAddress ip_address_; + + /// @brief Port number on which to listen for requests. + uint32_t port_; + + /// @brief Wire format of the inbound requests. + NameChangeFormat format_; + + /// @brief Low level socket underneath the listening socket + boost::shared_ptr asio_socket_; + + /// @brief NameChangeUDPSocket listening socket + boost::shared_ptr socket_; + + /// @brief Pointer to the receive callback + boost::shared_ptr recv_callback_; + + /// @brief indicator that signifies listener is being used + /// in test mode + bool reuse_address_; + + /// + /// @name Copy and constructor assignment operator + /// + /// The copy constructor and assignment operator are private to avoid + /// potential issues with multiple listeners attempting to share sockets + /// and callbacks. +private: + NameChangeUDPListener(const NameChangeUDPListener& source); + NameChangeUDPListener& operator=(const NameChangeUDPListener& source); + //@} +}; + + +/// @brief Provides the ability to send NameChangeRequests via UDP socket +/// +/// This class is a derivation of the NameChangeSender which is capable of +/// sending NameChangeRequests through a UDP socket. The caller need only +/// supply network addressing and a RequestSendHandler instance to send +/// NameChangeRequests asynchronously. +class NameChangeUDPSender : public NameChangeSender { +public: + + /// @brief Defines the maximum size packet that can be sent. + static const size_t SEND_BUF_MAX = NameChangeUDPListener::RECV_BUF_MAX; + + /// @brief Constructor + /// + /// @param ip_address the IP address from which to send + /// @param port the port from which to send + /// @param server_address the IP address of the target listener + /// @param server_port is the IP port of the target listener + /// @param format is the wire format of the outbound requests. + /// @param ncr_send_handler the send handler object to notify when + /// when a send completes. + /// @param send_que_max sets the maximum number of entries allowed in + /// the send queue. + /// It defaults to NameChangeSender::MAX_QUE_DEFAULT + /// @param reuse_address enables IP address sharing when true + /// It defaults to false. + /// + /// @throw base class throws NcrSenderError if handler is invalid. + NameChangeUDPSender(const isc::asiolink::IOAddress& ip_address, + const uint32_t port, const isc::asiolink::IOAddress& server_address, + const uint32_t server_port, const NameChangeFormat format, + RequestSendHandler * ncr_send_handler, + const size_t send_que_max = NameChangeSender::MAX_QUE_DEFAULT, + const bool reuse_address = false); + + /// @brief Destructor + virtual ~NameChangeUDPSender(); + + + /// @brief Opens a UDP socket using the given IOService. + /// + /// Creates a NameChangeUDPSocket bound to the sender's IP address + /// and port, that is monitored by the given IOService instance. + /// + /// @param io_service the IOService which will monitor the socket. + /// + /// @throw throws a NcrUDPError if the open fails. + virtual void open(isc::asiolink::IOService & io_service); + + + /// @brief Closes the UDPSocket. + /// + /// It first invokes the socket's cancel method which should stop any + /// pending send and remove the socket callback from the IOService. It + /// then calls the socket's close method to actually close the socket. + /// + /// @throw throws a NcrUDPError if the open fails. + virtual void close(); + + /// @brief Sends a given request asynchronously over the socket + /// + /// The given NameChangeRequest is converted to wire format and copied + /// into the send callback's transfer buffer. Then the socket's + /// asyncSend() method is called, passing in send_callback_ member's + /// transfer buffer as the send buffer and the send_callback_ itself + /// as the callback object. + virtual void doSend(NameChangeRequestPtr ncr); + + /// @brief Implements the NameChangeRequest level send completion handler. + /// + /// This method is invoked by the UDPCallback operator() implementation, + /// passing in the boolean success indicator and pointer to itself. + /// + /// If the indicator denotes success, then the method will notify the + /// application layer by calling invokeSendHandler() with a success + /// status. + /// + /// If the indicator denotes failure the method will log the failure and + /// notify the application layer by calling invokeRecvHandler() with + /// an error status. + /// + /// @param successful boolean indicator that should be true if the + /// socket send completed without error, false otherwise. + /// @param send_callback pointer to the callback instance which handled + /// the socket receive completion. + void send_completion_handler(const bool successful, + const UDPCallback* send_callback); + +private: + /// @brief IP address from which to send. + isc::asiolink::IOAddress ip_address_; + + /// @brief Port from which to send. + uint32_t port_; + + /// @brief IP address of the target listener. + isc::asiolink::IOAddress server_address_; + + /// @brief Port of the target listener. + uint32_t server_port_; + + /// @brief Wire format of the outbound requests. + NameChangeFormat format_; + + /// @brief Low level socket underneath the sending socket. + boost::shared_ptr asio_socket_; + + /// @brief NameChangeUDPSocket sending socket. + boost::shared_ptr socket_; + + /// @brief Endpoint of the target listener. + boost::shared_ptr server_endpoint_; + + /// @brief Pointer to the send callback + boost::shared_ptr send_callback_; + + /// @brief boolean indicator that signifies sender is being used + /// in test mode + bool reuse_address_; +}; + +} // namespace isc::d2 +} // namespace isc + +#endif diff --git a/src/bin/d2/tests/Makefile.am b/src/bin/d2/tests/Makefile.am index 4e888a9e0a..a6dba7665c 100644 --- a/src/bin/d2/tests/Makefile.am +++ b/src/bin/d2/tests/Makefile.am @@ -62,7 +62,9 @@ d2_unittests_SOURCES += ../d2_cfg_mgr.cc ../d2_cfg_mgr.h d2_unittests_SOURCES += ../d2_update_message.cc ../d2_update_message.h d2_unittests_SOURCES += ../d2_zone.cc ../d2_zone.h d2_unittests_SOURCES += ../dns_client.cc ../dns_client.h +d2_unittests_SOURCES += ../ncr_io.cc ../ncr_io.h d2_unittests_SOURCES += ../ncr_msg.cc ../ncr_msg.h +d2_unittests_SOURCES += ../ncr_udp.cc ../ncr_udp.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 @@ -74,6 +76,7 @@ d2_unittests_SOURCES += d2_update_message_unittests.cc d2_unittests_SOURCES += d2_zone_unittests.cc d2_unittests_SOURCES += dns_client_unittests.cc d2_unittests_SOURCES += ncr_unittests.cc +d2_unittests_SOURCES += ncr_udp_unittests.cc nodist_d2_unittests_SOURCES = ../d2_messages.h ../d2_messages.cc d2_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) diff --git a/src/bin/d2/tests/d_test_stubs.h b/src/bin/d2/tests/d_test_stubs.h index 58ebcd44f7..99670de761 100644 --- a/src/bin/d2/tests/d_test_stubs.h +++ b/src/bin/d2/tests/d_test_stubs.h @@ -608,11 +608,12 @@ public: try { config_set_ = isc::data::Element::fromJSON(json_text); } catch (const isc::Exception &ex) { - return ::testing::AssertionFailure() - << "JSON text failed to parse:" << ex.what(); + return (::testing::AssertionFailure(::testing::Message() << + "JSON text failed to parse:" + << ex.what())); } - return ::testing::AssertionSuccess(); + return (::testing::AssertionSuccess()); } @@ -628,11 +629,12 @@ public: isc::data::ConstElementPtr comment; comment = isc::config::parseAnswer(rcode, answer_); if (rcode == should_be) { - return testing::AssertionSuccess(); + return (testing::AssertionSuccess()); } - return ::testing::AssertionFailure() << "checkAnswer rcode:" - << rcode << " comment: " << *comment; + return (::testing::AssertionFailure(::testing::Message() << + "checkAnswer rcode:" << rcode + << " comment: " << *comment)); } /// @brief Configuration set being tested. diff --git a/src/bin/d2/tests/ncr_udp_unittests.cc b/src/bin/d2/tests/ncr_udp_unittests.cc new file mode 100644 index 0000000000..9e7b8df2e4 --- /dev/null +++ b/src/bin/d2/tests/ncr_udp_unittests.cc @@ -0,0 +1,501 @@ +// 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 +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace std; +using namespace isc; +using namespace isc::d2; + +namespace { + +/// @brief Defines a list of valid JSON NameChangeRequest test messages. +const char *valid_msgs[] = +{ + // Valid Add. + "{" + " \"change_type\" : 0 , " + " \"forward_change\" : true , " + " \"reverse_change\" : false , " + " \"fqdn\" : \"walah.walah.com\" , " + " \"ip_address\" : \"192.168.2.1\" , " + " \"dhcid\" : \"010203040A7F8E3D\" , " + " \"lease_expires_on\" : \"20130121132405\" , " + " \"lease_length\" : 1300 " + "}", + // Valid Remove. + "{" + " \"change_type\" : 1 , " + " \"forward_change\" : true , " + " \"reverse_change\" : false , " + " \"fqdn\" : \"walah.walah.com\" , " + " \"ip_address\" : \"192.168.2.1\" , " + " \"dhcid\" : \"010203040A7F8E3D\" , " + " \"lease_expires_on\" : \"20130121132405\" , " + " \"lease_length\" : 1300 " + "}", + // Valid Add with IPv6 address + "{" + " \"change_type\" : 0 , " + " \"forward_change\" : true , " + " \"reverse_change\" : false , " + " \"fqdn\" : \"walah.walah.com\" , " + " \"ip_address\" : \"fe80::2acf:e9ff:fe12:e56f\" , " + " \"dhcid\" : \"010203040A7F8E3D\" , " + " \"lease_expires_on\" : \"20130121132405\" , " + " \"lease_length\" : 1300 " + "}" +}; + +const char* TEST_ADDRESS = "127.0.0.1"; +const uint32_t LISTENER_PORT = 5301; +const uint32_t SENDER_PORT = LISTENER_PORT+1; +const long TEST_TIMEOUT = 5 * 1000; + +/// @brief A NOP derivation for constructor test purposes. +class SimpleListenHandler : public NameChangeListener::RequestReceiveHandler { +public: + virtual void operator ()(NameChangeListener::Result, NameChangeRequestPtr) { + } +}; + +/// @brief Tests the NameChangeUDPListener constructors. +/// This test verifies that: +/// 1. Listener constructor requires valid completion handler +/// 2. Given valid parameters, the listener constructor works +TEST(NameChangeUDPListenerBasicTest, constructionTests) { + // Verify the default constructor works. + isc::asiolink::IOAddress ip_address(TEST_ADDRESS); + uint32_t port = LISTENER_PORT; + isc::asiolink::IOService io_service; + SimpleListenHandler ncr_handler; + + // Verify that constructing with an empty receive handler is not allowed. + EXPECT_THROW(NameChangeUDPListener(ip_address, port, FMT_JSON, NULL), + NcrListenerError); + + // Verify that valid constructor works. + EXPECT_NO_THROW(NameChangeUDPListener(ip_address, port, FMT_JSON, + &ncr_handler)); + +} + +/// @brief Tests NameChangeUDPListener starting and stopping listening . +/// This test verifies that the listener will: +/// 1. Enter listening state +/// 2. If in the listening state, does not allow calls to start listening +/// 3. Exist the listening state +/// 4. Return to the listening state after stopping +TEST(NameChangeUDPListenerBasicTest, basicListenTests) { + // Verify the default constructor works. + isc::asiolink::IOAddress ip_address(TEST_ADDRESS); + uint32_t port = LISTENER_PORT; + isc::asiolink::IOService io_service; + SimpleListenHandler ncr_handler; + + NameChangeListenerPtr listener; + ASSERT_NO_THROW(listener.reset( + new NameChangeUDPListener(ip_address, port, FMT_JSON, &ncr_handler))); + + // Verify that we can start listening. + EXPECT_NO_THROW(listener->startListening(io_service)); + EXPECT_TRUE(listener->amListening()); + + // Verify that attempting to listen when we already are is an error. + EXPECT_THROW(listener->startListening(io_service), NcrListenerError); + + // Verify that we can stop listening. + EXPECT_NO_THROW(listener->stopListening()); + EXPECT_FALSE(listener->amListening()); + + // Verify that attempting to stop listening when we are not is ok. + EXPECT_NO_THROW(listener->stopListening()); + + // Verify that we can re-enter listening. + EXPECT_NO_THROW(listener->startListening(io_service)); + EXPECT_TRUE(listener->amListening()); +} + +/// @brief Compares two NameChangeRequests for equality. +bool checkSendVsReceived(NameChangeRequestPtr sent_ncr_, + NameChangeRequestPtr received_ncr_) { + // @todo NameChangeRequest message doesn't currently have a comparison + // operator, so we will cheat and compare the text form. + return ((sent_ncr_ && received_ncr_ ) && + ((sent_ncr_->toText()) == (received_ncr_->toText()))); +} + +/// @brief Text fixture for testing NameChangeUDPListener +class NameChangeUDPListenerTest : public virtual ::testing::Test, + NameChangeListener::RequestReceiveHandler { +public: + isc::asiolink::IOService io_service_; + NameChangeListener::Result result_; + NameChangeRequestPtr sent_ncr_; + NameChangeRequestPtr received_ncr_; + NameChangeUDPListener *listener_; + isc::asiolink::IntervalTimer test_timer_; + + /// @brief Constructor + // + // Instantiates the listener member and the test timer. The timer is used + // to ensure a test doesn't go awry and hang forever. + NameChangeUDPListenerTest() + : io_service_(), result_(NameChangeListener::SUCCESS), + test_timer_(io_service_) { + isc::asiolink::IOAddress addr(TEST_ADDRESS); + listener_ = new NameChangeUDPListener(addr, LISTENER_PORT, + FMT_JSON, this, true); + + // Set the test timeout to break any running tasks if they hang. + test_timer_.setup(boost::bind(&NameChangeUDPListenerTest:: + testTimeoutHandler, this), + TEST_TIMEOUT); + } + + /// @brief Converts JSON string into an NCR and sends it to the listener. + /// + void sendNcr(const std::string& msg) { + // Build an NCR from json string. This verifies that the + // test string is valid. + ASSERT_NO_THROW(sent_ncr_ = NameChangeRequest::fromJSON(msg)); + + // Now use the NCR to write JSON to an output buffer. + isc::util::OutputBuffer ncr_buffer(1024); + ASSERT_NO_THROW(sent_ncr_->toFormat(FMT_JSON, ncr_buffer)); + + // Create a UDP socket through which our "sender" will send the NCR. + asio::ip::udp::socket + udp_socket(io_service_.get_io_service(), asio::ip::udp::v4()); + + // Create an endpoint pointed at the listener. + asio::ip::udp::endpoint + listener_endpoint(asio::ip::address::from_string(TEST_ADDRESS), + LISTENER_PORT); + + // A response message is now ready to send. Send it! + // Note this uses a synchronous send so it ships immediately. + // If listener isn't in listening mode, it will get missed. + udp_socket.send_to(asio::buffer(ncr_buffer.getData(), + ncr_buffer.getLength()), + listener_endpoint); + } + + /// @brief RequestReceiveHandler operator implementation for receiving NCRs. + /// + /// The fixture acts as the "application" layer. It derives from + /// RequestReceiveHandler and as such implements operator() in order to + /// receive NCRs. + virtual void operator ()(NameChangeListener::Result result, + NameChangeRequestPtr ncr) { + // save the result and the NCR we received + result_ = result; + received_ncr_ = ncr; + } + // @brief Handler invoked when test timeout is hit. + // + // This callback stops all running (hanging) tasks on IO service. + void testTimeoutHandler() { + io_service_.stop(); + FAIL() << "Test timeout hit."; + } +}; + +/// @brief Tests NameChangeUDPListener ability to receive NCRs. +/// This test verifies that a listener can enter listening mode and +/// receive NCRs in wire format on its UDP socket; reconstruct the +/// NCRs and delivery them to the "application" layer. +TEST_F(NameChangeUDPListenerTest, basicReceivetest) { + // Verify we can enter listening mode. + EXPECT_FALSE(listener_->amListening()); + ASSERT_NO_THROW(listener_->startListening(io_service_)); + ASSERT_TRUE(listener_->amListening()); + + // Iterate over a series of requests, sending and receiving one + /// at time. + int num_msgs = sizeof(valid_msgs)/sizeof(char*); + for (int i = 0; i < num_msgs; i++) { + // We are not verifying ability to send, so if we can't test is over. + ASSERT_NO_THROW(sendNcr(valid_msgs[i])); + + // Execute no more then one event, which should be receive complete. + EXPECT_NO_THROW(io_service_.run_one()); + + // Verify the "application" status value for a successful complete. + EXPECT_EQ(NameChangeListener::SUCCESS, result_); + + // Verify the received request matches the sent request. + EXPECT_TRUE(checkSendVsReceived(sent_ncr_, received_ncr_)); + } + + // Verify we can gracefully stop listening. + EXPECT_NO_THROW(listener_->stopListening()); + EXPECT_FALSE(listener_->amListening()); +} + +/// @brief A NOP derivation for constructor test purposes. +class SimpleSendHandler : public NameChangeSender::RequestSendHandler { +public: + virtual void operator ()(NameChangeSender::Result, NameChangeRequestPtr) { + } +}; + +/// @brief Tests the NameChangeUDPSender constructors. +/// This test verifies that: +/// 1. Sender constructor requires valid completion handler +/// 2. Given valid parameters, the sender constructor works +TEST(NameChangeUDPSenderBasicTest, constructionTests) { + isc::asiolink::IOAddress ip_address(TEST_ADDRESS); + uint32_t port = SENDER_PORT; + isc::asiolink::IOService io_service; + SimpleSendHandler ncr_handler; + + // Verify that constructing with an empty send handler is not allowed. + EXPECT_THROW(NameChangeUDPSender(ip_address, port, + ip_address, port, FMT_JSON, NULL), NcrSenderError); + + // Verify that constructing with an queue size of zero is not allowed. + EXPECT_THROW(NameChangeUDPSender(ip_address, port, + ip_address, port, FMT_JSON, &ncr_handler, 0), NcrSenderError); + + NameChangeSenderPtr sender; + // Verify that valid constructor works. + EXPECT_NO_THROW(sender.reset( + new NameChangeUDPSender(ip_address, port, ip_address, port, + FMT_JSON, &ncr_handler))); + + // Verify that send queue default max is correct. + size_t expected = NameChangeSender::MAX_QUE_DEFAULT; + EXPECT_EQ(expected, sender->getQueMaxSize()); + + // Verify that constructor with a valid custom queue size works. + EXPECT_NO_THROW(sender.reset( + new NameChangeUDPSender(ip_address, port, ip_address, port, + FMT_JSON, &ncr_handler, 100))); +} + +/// @brief Tests NameChangeUDPSender basic send functionality +/// This test verifies that: +TEST(NameChangeUDPSenderBasicTest, basicSendTests) { + isc::asiolink::IOAddress ip_address(TEST_ADDRESS); + uint32_t port = SENDER_PORT; + isc::asiolink::IOService io_service; + SimpleSendHandler ncr_handler; + + // Tests are based on a list of messages, get the count now. + int num_msgs = sizeof(valid_msgs)/sizeof(char*); + + // Create the sender, setting the queue max equal to the number of + // messages we will have in the list. + NameChangeUDPSender sender(ip_address, port, ip_address, port, + FMT_JSON, &ncr_handler, num_msgs); + + // Verify that we can start sending. + EXPECT_NO_THROW(sender.startSending(io_service)); + EXPECT_TRUE(sender.amSending()); + + // Verify that attempting to send when we already are is an error. + EXPECT_THROW(sender.startSending(io_service), NcrSenderError); + + // Verify that we can stop sending. + EXPECT_NO_THROW(sender.stopSending()); + EXPECT_FALSE(sender.amSending()); + + // Verify that attempting to stop sending when we are not is ok. + EXPECT_NO_THROW(sender.stopSending()); + + // Verify that we can re-enter sending after stopping. + EXPECT_NO_THROW(sender.startSending(io_service)); + EXPECT_TRUE(sender.amSending()); + + // Iterate over a series of messages, sending each one. Since we + // do not invoke IOService::run, then the messages should accumulate + // in the queue. + NameChangeRequestPtr ncr; + for (int i = 0; i < num_msgs; i++) { + ASSERT_NO_THROW(ncr = NameChangeRequest::fromJSON(valid_msgs[i])); + EXPECT_NO_THROW(sender.sendRequest(ncr)); + // Verify that the queue count increments in step with each send. + EXPECT_EQ(i+1, sender.getQueSize()); + } + + // Verify that attempting to send an additional message results in a + // queue full exception. + EXPECT_THROW(sender.sendRequest(ncr), NcrSenderQueFull); + + // Loop for the number of valid messages and invoke IOService::run_one. + // This should send exactly one message and the queue count should + // decrement accordingly. + for (int i = num_msgs; i > 0; i--) { + io_service.run_one(); + // Verify that the queue count decrements in step with each run. + EXPECT_EQ(i-1, sender.getQueSize()); + } + + // Verify that the queue is empty. + EXPECT_EQ(0, sender.getQueSize()); + + // Verify that we can add back to the queue + EXPECT_NO_THROW(sender.sendRequest(ncr)); + EXPECT_EQ(1, sender.getQueSize()); + + // Verify that we can remove the current entry at the front of the queue. + EXPECT_NO_THROW(sender.skipNext()); + EXPECT_EQ(0, sender.getQueSize()); + + // Verify that flushing the queue is not allowed in sending state. + EXPECT_THROW(sender.flushSendQue(), NcrSenderError); + + // Put a message on the queue. + EXPECT_NO_THROW(sender.sendRequest(ncr)); + EXPECT_EQ(1, sender.getQueSize()); + + // Verify that we can gracefully stop sending. + EXPECT_NO_THROW(sender.stopSending()); + EXPECT_FALSE(sender.amSending()); + + // Verify that the queue is preserved after leaving sending state. + EXPECT_EQ(1, sender.getQueSize()); + + // Verify that flushing the queue works when not sending. + EXPECT_NO_THROW(sender.flushSendQue()); + EXPECT_EQ(0, sender.getQueSize()); +} + +/// @brief Text fixture that allows testing a listener and sender together +/// It derives from both the receive and send handler classes and contains +/// and instance of UDP listener and UDP sender. +class NameChangeUDPTest : public virtual ::testing::Test, + NameChangeListener::RequestReceiveHandler, + NameChangeSender::RequestSendHandler { +public: + isc::asiolink::IOService io_service_; + NameChangeListener::Result recv_result_; + NameChangeSender::Result send_result_; + NameChangeListenerPtr listener_; + NameChangeSenderPtr sender_; + isc::asiolink::IntervalTimer test_timer_; + + std::vector sent_ncrs_; + std::vector received_ncrs_; + + NameChangeUDPTest() + : io_service_(), recv_result_(NameChangeListener::SUCCESS), + send_result_(NameChangeSender::SUCCESS), test_timer_(io_service_) { + isc::asiolink::IOAddress addr(TEST_ADDRESS); + // Create our listener instance. Note that reuse_address is true. + listener_.reset( + new NameChangeUDPListener(addr, LISTENER_PORT, FMT_JSON, + this, true)); + + // Create our sender instance. Note that reuse_address is true. + sender_.reset( + new NameChangeUDPSender(addr, SENDER_PORT, addr, LISTENER_PORT, + FMT_JSON, this, 100, true)); + + // Set the test timeout to break any running tasks if they hang. + test_timer_.setup(boost::bind(&NameChangeUDPTest::testTimeoutHandler, + this), + TEST_TIMEOUT); + } + + void reset_results() { + sent_ncrs_.clear(); + received_ncrs_.clear(); + } + + /// @brief Implements the receive completion handler. + virtual void operator ()(NameChangeListener::Result result, + NameChangeRequestPtr ncr) { + // save the result and the NCR received. + recv_result_ = result; + received_ncrs_.push_back(ncr); + } + + /// @brief Implements the send completion handler. + virtual void operator ()(NameChangeSender::Result result, + NameChangeRequestPtr ncr) { + // save the result and the NCR sent. + send_result_ = result; + sent_ncrs_.push_back(ncr); + } + + // @brief Handler invoked when test timeout is hit. + // + // This callback stops all running (hanging) tasks on IO service. + void testTimeoutHandler() { + io_service_.stop(); + FAIL() << "Test timeout hit."; + } +}; + +/// @brief Uses a sender and listener to test UDP-based NCR delivery +/// Conducts a "round-trip" test using a sender to transmit a set of valid +/// NCRs to a listener. The test verifies that what was sent matches what +/// was received both in quantity and in content. +TEST_F (NameChangeUDPTest, roundTripTest) { + // Place the listener into listening state. + ASSERT_NO_THROW(listener_->startListening(io_service_)); + EXPECT_TRUE(listener_->amListening()); + + // Get the number of messages in the list of test messages. + int num_msgs = sizeof(valid_msgs)/sizeof(char*); + + // Place the sender into sending state. + ASSERT_NO_THROW(sender_->startSending(io_service_)); + EXPECT_TRUE(sender_->amSending()); + + for (int i = 0; i < num_msgs; i++) { + NameChangeRequestPtr ncr; + ASSERT_NO_THROW(ncr = NameChangeRequest::fromJSON(valid_msgs[i])); + sender_->sendRequest(ncr); + EXPECT_EQ(i+1, sender_->getQueSize()); + } + + // Execute callbacks until we have sent and received all of messages. + while (sender_->getQueSize() > 0 || (received_ncrs_.size() < num_msgs)) { + EXPECT_NO_THROW(io_service_.run_one()); + } + + // Send queue should be empty. + EXPECT_EQ(0, sender_->getQueSize()); + + // We should have the same number of sends and receives as we do messages. + ASSERT_EQ(num_msgs, sent_ncrs_.size()); + ASSERT_EQ(num_msgs, received_ncrs_.size()); + + // Verify that what we sent matches what we received. + for (int i = 0; i < num_msgs; i++) { + EXPECT_TRUE (checkSendVsReceived(sent_ncrs_[i], received_ncrs_[i])); + } + + // Verify that we can gracefully stop listening. + EXPECT_NO_THROW(listener_->stopListening()); + EXPECT_FALSE(listener_->amListening()); + + // Verify that we can gracefully stop sending. + EXPECT_NO_THROW(sender_->stopSending()); + EXPECT_FALSE(sender_->amSending()); +} + +} // end of anonymous namespace