From: Francis Dupont Date: Wed, 17 Oct 2018 20:10:27 +0000 (+0200) Subject: [153-netconf-agent] Added netconf agent code from kea-yang X-Git-Tag: 176-update-to-sysrepo-0-7-6-release_base~16 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=888121a1feeeeff8638604edf9a528a9904d6ace;p=thirdparty%2Fkea.git [153-netconf-agent] Added netconf agent code from kea-yang --- diff --git a/src/bin/netconf/Makefile.am b/src/bin/netconf/Makefile.am index 73b3aa4019..a2ed58c31a 100644 --- a/src/bin/netconf/Makefile.am +++ b/src/bin/netconf/Makefile.am @@ -48,6 +48,7 @@ libnetconf_la_SOURCES = control_socket.cc control_socket.h libnetconf_la_SOURCES += http_control_socket.cc http_control_socket.h libnetconf_la_SOURCES += stdout_control_socket.cc stdout_control_socket.h libnetconf_la_SOURCES += unix_control_socket.cc unix_control_socket.h +libnetconf_la_SOURCES += netconf.cc netconf.h libnetconf_la_SOURCES += netconf_cfg_mgr.cc netconf_cfg_mgr.h libnetconf_la_SOURCES += netconf_config.cc netconf_config.h libnetconf_la_SOURCES += netconf_controller.cc netconf_controller.h diff --git a/src/bin/netconf/netconf.cc b/src/bin/netconf/netconf.cc new file mode 100644 index 0000000000..a125627937 --- /dev/null +++ b/src/bin/netconf/netconf.cc @@ -0,0 +1,597 @@ +// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/// @file netconf.cc +/// Contains the Netconf agent methods. + +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace isc::config; +using namespace isc::data; +using namespace isc::netconf; +using namespace isc::yang; + +namespace { + +/// @brief Subscription callback. +class NetconfAgentCallback : public Callback { +public: + /// @brief Constructor. + /// + /// @param service_pair The service name and configuration pair. + NetconfAgentCallback(const CfgServersMapPair& service_pair) + : service_pair_(service_pair) { + } + + /// @brief Server name and configuration pair. + CfgServersMapPair service_pair_; + + /// @brief Module change callback. + /// + /// @param sess The running datastore session. + /// @param module_name The module name. + /// @param event The event. + /// @param private_ctx The private context. + /// @return the sysrepo return code. + int module_change(S_Session sess, + const char* /*module_name*/, + sr_notif_event_t event, + void* /*private_ctx*/) { + if (NetconfProcess::global_shut_down_flag) { + return (SR_ERR_DISCONNECT); + } + ostringstream event_type; + switch (event) { + case SR_EV_VERIFY: + event_type << "VERIFY"; + break; + case SR_EV_APPLY: + event_type << "APPLY"; + break; + case SR_EV_ABORT: + event_type << "ABORT"; + break; + case SR_EV_ENABLED: + event_type << "ENABLED"; + break; + default: + event_type << "UNKNOWN (" << event << ")"; + break; + } + LOG_INFO(netconf_logger, NETCONF_CONFIG_CHANGE_EVENT) + .arg(event_type.str()); + string xpath = "/" + service_pair_.second->getModel() + ":"; + NetconfAgent::logChanges(sess, xpath + "config"); + if (NetconfProcess::global_shut_down_flag) { + return (SR_ERR_DISCONNECT); + } + NetconfAgent::logChanges(sess, xpath + "logging"); + if (NetconfProcess::global_shut_down_flag) { + return (SR_ERR_DISCONNECT); + } + switch (event) { + case SR_EV_VERIFY: + return (NetconfAgent::validate(sess, service_pair_)); + case SR_EV_APPLY: + return (NetconfAgent::update(sess, service_pair_)); + default: + return (SR_ERR_OK); + } + } +}; + +} // end of anonymous namespace + +namespace isc { +namespace netconf { + +NetconfAgent::NetconfAgent() { +} + +NetconfAgent::~NetconfAgent() { + clear(); +} + +void +NetconfAgent::init(NetconfCfgMgrPtr cfg_mgr) { + const CfgServersMapPtr& servers = + cfg_mgr->getNetconfConfig()->getCfgServersMap(); + for (auto pair : *servers) { + if (NetconfProcess::global_shut_down_flag) { + return; + } + keaConfig(pair); + } + if (NetconfProcess::global_shut_down_flag) { + return; + } + initSysrepo(); + for (auto pair : *servers) { + if (NetconfProcess::global_shut_down_flag) { + return; + } + yangConfig(pair); + if (NetconfProcess::global_shut_down_flag) { + return; + } + subscribe(pair); + } +} + +void +NetconfAgent::clear() { + // Should be already set to true but in case... + NetconfProcess::global_shut_down_flag = true; + for (auto subs : subscriptions_) { + subs.second.reset(); + } + subscriptions_.clear(); + running_sess_.reset(); + startup_sess_.reset(); + conn_.reset(); +} + +void +NetconfAgent::keaConfig(const CfgServersMapPair& service_pair) { + if (!service_pair.second->getBootUpdate()) { + return; + } + CfgControlSocketPtr ctrl_sock = service_pair.second->getCfgControlSocket(); + if (!ctrl_sock) { + return; + } + ControlSocketBasePtr comm; + try { + comm = createControlSocket(ctrl_sock); + } catch (const std::exception& ex) { + ostringstream msg; + msg << "createControlSocket failed with " << ex.what(); + LOG_ERROR(netconf_logger, NETCONF_GET_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return; + } + ConstElementPtr answer; + int rcode; + ConstElementPtr config; + LOG_DEBUG(netconf_logger, NETCONF_DBG_TRACE, NETCONF_GET_CONFIG) + .arg(service_pair.first); + try { + answer = comm->configGet(service_pair.first); + config = parseAnswer(rcode, answer); + } catch (const std::exception& ex) { + ostringstream msg; + msg << "configGet failed with " << ex.what(); + LOG_ERROR(netconf_logger, NETCONF_GET_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return; + } + if (NetconfProcess::global_shut_down_flag) { + return; + } + if (rcode != CONTROL_RESULT_SUCCESS) { + ostringstream msg; + msg << "configGet returned " << answerToText(answer); + LOG_ERROR(netconf_logger, NETCONF_GET_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return; + } + if (!config) { + LOG_ERROR(netconf_logger, NETCONF_GET_CONFIG_FAILED) + .arg(service_pair.first) + .arg("configGet returned an empty configuration"); + return; + } + LOG_DEBUG(netconf_logger, NETCONF_DBG_TRACE_DETAIL_DATA, + NETCONF_GET_CONFIG_CONFIG) + .arg(service_pair.first) + .arg(prettyPrint(config)); +} + +void +NetconfAgent::initSysrepo() { + try { + conn_.reset(new Connection(NetconfController::netconf_app_name_, + SR_CONN_DAEMON_REQUIRED)); + } catch (const std::exception& ex) { + isc_throw(Unexpected, "Can't connect to sysrepo: " << ex.what()); + } + + try { + startup_sess_.reset(new Session(conn_, SR_DS_STARTUP)); + running_sess_.reset(new Session(conn_, SR_DS_RUNNING)); + } catch (const std::exception& ex) { + isc_throw(Unexpected, "Can't establish a sysrepo session: " + << ex.what()); + } +} + +void +NetconfAgent::yangConfig(const CfgServersMapPair& service_pair) { + if (NetconfProcess::global_shut_down_flag || + !service_pair.second->getBootUpdate() || + service_pair.second->getModel().empty()) { + return; + } + CfgControlSocketPtr ctrl_sock = service_pair.second->getCfgControlSocket(); + if (!ctrl_sock) { + return; + } + LOG_DEBUG(netconf_logger, NETCONF_DBG_TRACE, NETCONF_SET_CONFIG) + .arg(service_pair.first); + ConstElementPtr config; + try { + TranslatorConfig tc(startup_sess_, service_pair.second->getModel()); + config = tc.getConfig(); + if (!config) { + ostringstream msg; + msg << "YANG configuration for " + << service_pair.second->getModel() + << " is empty"; + LOG_ERROR(netconf_logger, NETCONF_SET_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return; + } else { + LOG_DEBUG(netconf_logger, NETCONF_DBG_TRACE_DETAIL_DATA, + NETCONF_SET_CONFIG_CONFIG) + .arg(service_pair.first) + .arg(prettyPrint(config)); + } + } catch (const std::exception& ex) { + ostringstream msg; + msg << "YANG getConfig for " << service_pair.first + << " failed with " << ex.what(); + LOG_ERROR(netconf_logger, NETCONF_SET_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return; + } + if (NetconfProcess::global_shut_down_flag) { + return; + } + ControlSocketBasePtr comm; + try { + comm = createControlSocket(ctrl_sock); + } catch (const std::exception& ex) { + ostringstream msg; + msg << "control socket creation failed with " << ex.what(); + LOG_ERROR(netconf_logger, NETCONF_SET_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return; + } + if (NetconfProcess::global_shut_down_flag) { + return; + } + ConstElementPtr answer; + int rcode; + try { + answer = comm->configSet(config, service_pair.first); + parseAnswer(rcode, answer); + } catch (const std::exception& ex) { + ostringstream msg; + msg << "configSet failed with " << ex.what(); + LOG_ERROR(netconf_logger, NETCONF_SET_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return; + } + if (rcode != CONTROL_RESULT_SUCCESS) { + ostringstream msg; + msg << "configSet returned " << answerToText(answer); + LOG_ERROR(netconf_logger, NETCONF_SET_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + } +} + +void +NetconfAgent::subscribe(const CfgServersMapPair& service_pair) { + if (NetconfProcess::global_shut_down_flag || + !service_pair.second->getSubscribeChanges() || + service_pair.second->getModel().empty()) { + return; + } + LOG_DEBUG(netconf_logger, NETCONF_DBG_TRACE, NETCONF_SUBSCRIBE) + .arg(service_pair.first) + .arg(service_pair.second->getModel()); + S_Subscribe subs(new Subscribe(running_sess_)); + S_Callback cb(new NetconfAgentCallback(service_pair)); + try { + sr_subscr_options_t options = SR_SUBSCR_DEFAULT; + if (!service_pair.second->getValidateChanges()) { + options |= SR_SUBSCR_APPLY_ONLY; + } + // Note the API stores the module name so do not put it + // in a short lifetime variable! + subs->module_change_subscribe(service_pair.second->getModel().c_str(), + cb, 0, 0, options); + } catch (const std::exception& ex) { + ostringstream msg; + msg << "module change subscribe failed with " << ex.what(); + LOG_ERROR(netconf_logger, NETCONF_SUBSCRIBE_FAILED) + .arg(service_pair.first) + .arg(service_pair.second->getModel()) + .arg(msg.str()); + subs.reset(); + return; + } + subscriptions_.insert(make_pair(service_pair.first, subs)); +} + +int +NetconfAgent::validate(S_Session sess, const CfgServersMapPair& service_pair) { + if (NetconfProcess::global_shut_down_flag || + !service_pair.second->getSubscribeChanges() || + !service_pair.second->getValidateChanges()) { + return (SR_ERR_OK); + } + CfgControlSocketPtr ctrl_sock = service_pair.second->getCfgControlSocket(); + if (!ctrl_sock) { + return (SR_ERR_OK); + } + LOG_DEBUG(netconf_logger, NETCONF_DBG_TRACE, NETCONF_VALIDATE_CONFIG) + .arg(service_pair.first); + ConstElementPtr config; + try { + TranslatorConfig tc(sess, service_pair.second->getModel()); + config = tc.getConfig(); + if (!config) { + ostringstream msg; + msg << "YANG configuration for " + << service_pair.second->getModel() + << " is empty"; + LOG_ERROR(netconf_logger, NETCONF_VALIDATE_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return (SR_ERR_DISCONNECT); + } else { + LOG_DEBUG(netconf_logger, NETCONF_DBG_TRACE_DETAIL_DATA, + NETCONF_VALIDATE_CONFIG_CONFIG) + .arg(service_pair.first) + .arg(prettyPrint(config)); + } + } catch (const std::exception& ex) { + ostringstream msg; + msg << "YANG getConfig for " << service_pair.first + << " failed with " << ex.what(); + LOG_ERROR(netconf_logger, NETCONF_VALIDATE_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return (SR_ERR_VALIDATION_FAILED);; + } + if (NetconfProcess::global_shut_down_flag) { + return (SR_ERR_DISCONNECT); + } + ControlSocketBasePtr comm; + try { + comm = createControlSocket(ctrl_sock); + } catch (const std::exception& ex) { + ostringstream msg; + msg << "createControlSocket failed with " << ex.what(); + LOG_ERROR(netconf_logger, NETCONF_VALIDATE_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return (SR_ERR_OK); + } + ConstElementPtr answer; + int rcode; + try { + answer = comm->configTest(config, service_pair.first); + parseAnswer(rcode, answer); + } catch (const std::exception& ex) { + stringstream msg; + msg << "configTest failed with " << ex.what(); + LOG_ERROR(netconf_logger, NETCONF_VALIDATE_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return (SR_ERR_VALIDATION_FAILED); + } + if (rcode != CONTROL_RESULT_SUCCESS) { + stringstream msg; + msg << "configTest returned " << answerToText(answer); + LOG_ERROR(netconf_logger, NETCONF_VALIDATE_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return (SR_ERR_VALIDATION_FAILED); + } + return (SR_ERR_OK); +} + +int +NetconfAgent::update(S_Session sess, const CfgServersMapPair& service_pair) { + if (NetconfProcess::global_shut_down_flag || + !service_pair.second->getSubscribeChanges()) { + return (SR_ERR_OK); + } + CfgControlSocketPtr ctrl_sock = service_pair.second->getCfgControlSocket(); + if (!ctrl_sock) { + return (SR_ERR_OK); + } + LOG_DEBUG(netconf_logger, NETCONF_DBG_TRACE, NETCONF_UPDATE_CONFIG) + .arg(service_pair.first); + ConstElementPtr config; + try { + TranslatorConfig tc(sess, service_pair.second->getModel()); + config = tc.getConfig(); + if (!config) { + ostringstream msg; + msg << "YANG configuration for " + << service_pair.second->getModel() + << " is empty"; + LOG_ERROR(netconf_logger, NETCONF_UPDATE_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return (SR_ERR_VALIDATION_FAILED); + } else { + LOG_DEBUG(netconf_logger, NETCONF_DBG_TRACE_DETAIL_DATA, + NETCONF_UPDATE_CONFIG_CONFIG) + .arg(service_pair.first) + .arg(prettyPrint(config)); + } + } catch (const std::exception& ex) { + ostringstream msg; + msg << "YANG getConfig for " << service_pair.first + << " failed with " << ex.what(); + LOG_ERROR(netconf_logger, NETCONF_UPDATE_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return (SR_ERR_VALIDATION_FAILED); + } + if (NetconfProcess::global_shut_down_flag) { + return (SR_ERR_OK); + } + ControlSocketBasePtr comm; + try { + comm = createControlSocket(ctrl_sock); + } catch (const std::exception& ex) { + ostringstream msg; + msg << "createControlSocket failed with " << ex.what(); + LOG_ERROR(netconf_logger, NETCONF_UPDATE_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return (SR_ERR_OK); + } + ConstElementPtr answer; + int rcode; + try { + answer = comm->configSet(config, service_pair.first); + parseAnswer(rcode, answer); + } catch (const std::exception& ex) { + stringstream msg; + msg << "configSet failed with " << ex.what(); + LOG_ERROR(netconf_logger, NETCONF_UPDATE_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return (SR_ERR_VALIDATION_FAILED); + } + if (rcode != CONTROL_RESULT_SUCCESS) { + stringstream msg; + msg << "configSet returned " << answerToText(answer); + LOG_ERROR(netconf_logger, NETCONF_UPDATE_CONFIG_FAILED) + .arg(service_pair.first) + .arg(msg.str()); + return (SR_ERR_VALIDATION_FAILED); + } + return (SR_ERR_OK); +} + +void +NetconfAgent::logChanges(S_Session sess, const string& model) { + if (NetconfProcess::global_shut_down_flag) { + return; + } + S_Iter_Change iter = sess->get_changes_iter(model.c_str()); + if (!iter) { + LOG_WARN(netconf_logger, NETCONF_LOG_CHANGE_FAIL) + .arg("no iterator"); + return; + } + for (;;) { + if (NetconfProcess::global_shut_down_flag) { + return; + } + S_Change change; + ostringstream msg; + try { + change = sess->get_change_next(iter); + } catch (const sysrepo_exception& ex) { + msg << "get change iterator next failed: " << ex.what(); + LOG_WARN(netconf_logger, NETCONF_LOG_CHANGE_FAIL) + .arg(msg.str()); + return; + } + if (!change) { + // End of changes, not an error. + return; + } + if (NetconfProcess::global_shut_down_flag) { + return; + } + S_Val new_val = change->new_val(); + S_Val old_val = change->old_val(); + string report; + switch (change->oper()) { + case SR_OP_CREATED: + if (!new_val) { + LOG_WARN(netconf_logger, NETCONF_LOG_CHANGE_FAIL) + .arg("created but without a new value"); + break; + } + msg << "created: " << new_val->to_string(); + report = msg.str(); + boost::erase_all(report, "\n"); + LOG_DEBUG(netconf_logger, NETCONF_DBG_TRACE_DETAIL_DATA, + NETCONF_CONFIG_CHANGED_DETAIL) + .arg(report); + break; + case SR_OP_MODIFIED: + if (!old_val || !new_val) { + LOG_WARN(netconf_logger, NETCONF_LOG_CHANGE_FAIL) + .arg("modified but without an old or new value"); + break; + } + msg << "modified: " << old_val->to_string() + << " => " << new_val->to_string(); + report = msg.str(); + boost::erase_all(report, "\n"); + LOG_DEBUG(netconf_logger, NETCONF_DBG_TRACE_DETAIL_DATA, + NETCONF_CONFIG_CHANGED_DETAIL) + .arg(report); + break; + case SR_OP_DELETED: + if (!old_val) { + LOG_WARN(netconf_logger, NETCONF_LOG_CHANGE_FAIL) + .arg("deleted but without an old value"); + break; + } + msg << "deleted: " << old_val->to_string(); + report = msg.str(); + boost::erase_all(report, "\n"); + LOG_DEBUG(netconf_logger, NETCONF_DBG_TRACE_DETAIL_DATA, + NETCONF_CONFIG_CHANGED_DETAIL) + .arg(report); + break; + case SR_OP_MOVED: + if (!new_val) { + LOG_WARN(netconf_logger, NETCONF_LOG_CHANGE_FAIL) + .arg("moved but without a new value"); + break; + } + msg << "moved: " << new_val->xpath(); + if (old_val) { + msg << " first"; + } else { + msg << " after " << old_val->xpath(); + } + report = msg.str(); + boost::erase_all(report, "\n"); + LOG_DEBUG(netconf_logger, NETCONF_DBG_TRACE_DETAIL_DATA, + NETCONF_CONFIG_CHANGED_DETAIL) + .arg(report); + break; + default: + msg << "unknown operation (" << change->oper() << ")"; + LOG_WARN(netconf_logger, NETCONF_LOG_CHANGE_FAIL) + .arg(msg.str()); + } + } +} + +} // namespace netconf +} // namespace isc diff --git a/src/bin/netconf/netconf.h b/src/bin/netconf/netconf.h new file mode 100644 index 0000000000..7142d5aad5 --- /dev/null +++ b/src/bin/netconf/netconf.h @@ -0,0 +1,123 @@ +// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/// @file netconf.h + +#ifndef NETCONF_H +#define NETCONF_H + +#include +#include +#include +#include +#include +#include +#include + +namespace isc { +namespace netconf { + +/// @brief Forward declaration to the @c NetconfAgent. +class NetconfAgent; + +/// @brief Type definition for the pointer to the @c NetconfAgent. +typedef boost::shared_ptr NetconfAgentPtr; + +/// @brief Netconf agent. +/// +/// Service performed by the Netconf agent: +/// - at boot get and display Kea server configurations. +/// - load Kea server configurations from YANG datastore. +/// - validate YANG datastore changes using Kea configuration test. +/// - load updated Kea server configurations from YANG datastore. +/// - on shutdown close subscriptions. +class NetconfAgent { +public: + /// @brief Constructor. + NetconfAgent(); + + /// @brief Destructor (call clear). + virtual ~NetconfAgent(); + + /// @brief Initialize sysrepo sessions. + /// + /// Must be called before init. + void initSysrepo(); + + /// @brief Initialization. + /// + /// Get and display Kea server configurations. + /// Load Kea server configurations from YANG datastore. + /// Subscribe changes in YANG datastore. + /// + /// @param cfg_mgr The configuration manager. + void init(NetconfCfgMgrPtr cfg_mgr); + + /// @brief Clear. + /// + /// Close subscriptions and sysrepo. + void clear(); + + /// @brief Validate. + /// + /// Validate YANG datastore changes using Kea configuration test. + /// + /// @param sess The sysrepo running datastore session. + /// @param service_pair The service name and configuration pair. + /// @return return code for sysrepo. + static int validate(S_Session sess, const CfgServersMapPair& service_pair); + + /// @brief Update. + /// + /// Update a Kea configuration from YANG datastore changes. + /// + /// @param sess The sysrepo running datastore session. + /// @param service_pair The service name and configuration pair. + /// @return return code for sysrepo. + static int update(S_Session sess, const CfgServersMapPair& service_pair); + + /// @brief Print changes. + /// + /// @param sess The sysrepo running datastore session. + /// @param model The model name. + static void logChanges(S_Session sess, const std::string& model); + + /// @brief Cancel flag. + bool cancel_; + +protected: + /// @brief Get and display Kea server configuration. + /// + /// @param service_pair The service name and configuration pair. + void keaConfig(const CfgServersMapPair& service_pair); + + /// @brief Kea server configuration from YANG datastore. + /// + /// @param service_pair The service name and configuration pair. + void yangConfig(const CfgServersMapPair& service_pair); + + /// @brief Subscribe changes for a module in YANG datastore. + /// + /// @param service_pair The service name and configuration pair. + void subscribe(const CfgServersMapPair& service_pair); + + /// @brief Sysrepo connection. + S_Connection conn_; + + /// @brief Sysrepo startup datastore session. + S_Session startup_sess_; + + /// @brief Sysrepo running datastore session. + S_Session running_sess_; + + /// @brief Subscription map. + std::map subscriptions_; +}; + +} // namespace netconf +} // namespace isc + +#endif // NETCONF_H diff --git a/src/bin/netconf/netconf_log.h b/src/bin/netconf/netconf_log.h index 5758a51b8c..80e7ce0c70 100644 --- a/src/bin/netconf/netconf_log.h +++ b/src/bin/netconf/netconf_log.h @@ -17,6 +17,31 @@ namespace isc { namespace netconf { +///@{ +/// \brief Netconf agent logging levels. +/// +/// Defines the levels used to output debug messages in the Netconf agent. +/// Note that higher numbers equate to more verbose (and detailed) output. + +/// @brief Traces normal operations. +/// +/// E.g. sending a command to a server etc. +const int NETCONF_DBG_TRACE = isc::log::DBGLVL_TRACE_BASIC; + +/// @brief Records the results of the commands. +/// +/// Using the example of tracing commands to a server, this will just record +/// the summary results. +const int NETCONF_DBG_RESULTS = isc::log::DBGLVL_TRACE_BASIC_DATA; + +/// @brief Additional information. +/// +/// Record detailed tracing. This is generally reserved for tracing +/// configurations from or to a server. +const int NETCONF_DBG_TRACE_DETAIL_DATA = isc::log::DBGLVL_TRACE_DETAIL_DATA; + +///@} + /// @brief Defines the name of the root level (default) logger. extern const char* NETCONF_LOGGER_NAME; diff --git a/src/bin/netconf/netconf_messages.mes b/src/bin/netconf/netconf_messages.mes index ecc9fe77ec..2c7222a321 100644 --- a/src/bin/netconf/netconf_messages.mes +++ b/src/bin/netconf/netconf_messages.mes @@ -6,6 +6,15 @@ $NAMESPACE isc::netconf +% NETCONF_CONFIG_CHANGE_EVENT Received YANG configuration change %1 event +This informational message is issued when Netconf receives an YANG +configuration change even.t. The type of event is printed. + +% NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: %1 +This debug message indicates a YANG configuration change. The format +is the change operation (created, modified, deleted or moved) followed +by xpaths and values of old and new nodes. + % NETCONF_CONFIG_CHECK_FAIL Netconf configuration check failed: %1 This error message indicates that Netconf had failed configuration check. Details are provided. Additional details may be available @@ -18,13 +27,81 @@ in earlier log entries, possibly on lower levels. % NETCONF_FAILED application experienced a fatal error: %1 This is a fatal error message issued when the Netconf application -encounters an unrecoverable error from within the event loop. +got an unrecoverable error from within the event loop. + +% NETCONF_GET_CONFIG getting configuration from %1 server +This debug message indicates that Netconf is trying to get the +configuration from a Kea server. + +% NETCONF_GET_CONFIG_CONFIG got configuration from %1 server: %2 +This debug message indicates that Netconf got the configuration from a +Kea server. The server name and the retrieved configuration are printed. + +% NETCONF_GET_CONFIG_FAILED getting configuration from %1 server failed: %2 +The error message indicates that Netconf got an error getting the +configuration from a Kea server. The name of the server and the error +are printed. + +% NETCONF_LOG_CHANGE_FAIL Netconf configuration change logging failed: %1 +The warning message indicates that the configuration change logging +encountered a not expected condition. % NETCONF_RUN_EXIT application is exiting the event loop This is a debug message issued when the Netconf application exits its event loop. +% NETCONF_SET_CONFIG setting configuration to %1 server +This debug message indicates that Netconf is trying to set the +configuration to a Kea server. + +% NETCONF_SET_CONFIG_CONFIG set configuration to %1 server: %2 +This debug message indicates that Netconf set the configuration to a +Kea server. The server name and the applied configuration are printed. + +% NETCONF_SET_CONFIG_FAILED setting configuration to %1 server: %2 +The error message indicates that Netconf got an error setting the +configuration to a Kea server. The name of the server and the error +are printed. + % NETCONF_STARTED Netconf (version %1) started This informational message indicates that Netconf has processed all configuration information and is ready to begin processing. The version is also printed. + +% NETCONF_SUBSCRIBE subscribing configuration changes for %1 server with %2 module +This debug message indicates that Netconf is trying to subscribe +configuration changes for a Kea server. The names of the server and +the module are printed. + +% NETCONF_SUBSCRIBE_FAILED subscribe configuration changes for %1 server with %2 module failed: %3 +The error message indicates that Netconf got an error subscribing +configuration changes for a Kea server. The names of the server and +the module, and the error are printed. + +% NETCONF_VALIDATE_CONFIG validating configuration for %1 server +This debug message indicates that Netconf is trying to validate the +configuration with a Kea server. + +% NETCONF_VALIDATE_CONFIG_CONFIG validate configuration with %1 server: %2 +This debug message indicates that Netconf validate the configuration +with a Kea server. The server name and the validated configuration are +printed. + +% NETCONF_VALIDATE_CONFIG_FAILED validating configuration with %1 server: %2 +The error message indicates that Netconf got an error validating the +configuration with a Kea server. The name of the server and the error +are printed. + +% NETCONF_UPDATE_CONFIG updating configuration for %1 server +This debug message indicates that Netconf is trying to update the +configuration of a Kea server. + +% NETCONF_UPDATE_CONFIG_CONFIG update configuration with %1 server: %2 +This debug message indicates that Netconf update the configuration +of a Kea server. The server name and the updated configuration are +printed. + +% NETCONF_UPDATE_CONFIG_FAILED updating configuration with %1 server: %2 +The error message indicates that Netconf got an error updating the +configuration of a Kea server. The name of the server and the error +are printed. diff --git a/src/bin/netconf/netconf_process.cc b/src/bin/netconf/netconf_process.cc index d3c469a610..94b5362846 100644 --- a/src/bin/netconf/netconf_process.cc +++ b/src/bin/netconf/netconf_process.cc @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include using namespace isc::asiolink; @@ -20,11 +22,13 @@ using namespace isc::config; using namespace isc::data; using namespace isc::http; using namespace isc::process; - +using namespace isc::util::thread; namespace isc { namespace netconf { +bool NetconfProcess::global_shut_down_flag = false; + NetconfProcess::NetconfProcess(const char* name, const asiolink::IOServicePtr& io_service) : DProcessBase(name, io_service, DCfgMgrBasePtr(new NetconfCfgMgr())) { @@ -42,6 +46,12 @@ NetconfProcess::run() { LOG_INFO(netconf_logger, NETCONF_STARTED).arg(VERSION); try { + // Initialize sysrepo. + agent_.initSysrepo(); + + // Initialize netconf agent in a thread. + Thread th([this]() { agent_.init(getNetconfCfgMgr()); }); + // Let's process incoming data or expiring timers in a loop until // shutdown condition is detected. while (!shouldShutdown()) { @@ -73,6 +83,7 @@ NetconfProcess::runIO() { isc::data::ConstElementPtr NetconfProcess::shutdown(isc::data::ConstElementPtr /*args*/) { + global_shut_down_flag = true; setShutdownFlag(true); return (isc::config::createAnswer(0, "Netconf is shutting down")); } diff --git a/src/bin/netconf/netconf_process.h b/src/bin/netconf/netconf_process.h index 26824b5390..bcd082a6c2 100644 --- a/src/bin/netconf/netconf_process.h +++ b/src/bin/netconf/netconf_process.h @@ -7,8 +7,7 @@ #ifndef NETCONF_PROCESS_H #define NETCONF_PROCESS_H -#include -#include +#include #include #include @@ -85,6 +84,9 @@ public: /// @brief Returns a pointer to the configuration manager. NetconfCfgMgrPtr getNetconfCfgMgr(); + /// @brief Global (globally visible) shutdown flag. + static bool global_shut_down_flag; + private: /// @brief Polls all ready handlers and then runs one handler if none @@ -92,6 +94,9 @@ private: /// /// @return Number of executed handlers. size_t runIO(); + + /// @brief Netconf agent. + NetconfAgent agent_; }; /// @brief Defines a shared pointer to NetconfProcess. diff --git a/src/bin/netconf/tests/Makefile.am b/src/bin/netconf/tests/Makefile.am index 1da8e6af9d..3b249a65f9 100644 --- a/src/bin/netconf/tests/Makefile.am +++ b/src/bin/netconf/tests/Makefile.am @@ -49,6 +49,7 @@ netconf_unittests_SOURCES += get_config_unittest.cc netconf_unittests_SOURCES += netconf_cfg_mgr_unittests.cc netconf_unittests_SOURCES += netconf_controller_unittests.cc netconf_unittests_SOURCES += netconf_process_unittests.cc +netconf_unittests_SOURCES += netconf_unittests.cc netconf_unittests_SOURCES += parser_unittests.cc netconf_unittests_SOURCES += run_unittests.cc @@ -65,7 +66,7 @@ netconf_unittests_LDADD += $(top_builddir)/src/lib/cfgrpt/libcfgrpt.la #netconf_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la #netconf_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la #netconf_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la -##netconf_unittests_LDADD += $(top_builddir)/src/lib/yang/testutils/libyangtest.la +netconf_unittests_LDADD += $(top_builddir)/src/lib/yang/testutils/libyangtest.la netconf_unittests_LDADD += $(top_builddir)/src/lib/yang/libkea-yang.la #netconf_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la netconf_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la diff --git a/src/bin/netconf/tests/netconf_unittests.cc b/src/bin/netconf/tests/netconf_unittests.cc new file mode 100644 index 0000000000..61abe2686f --- /dev/null +++ b/src/bin/netconf/tests/netconf_unittests.cc @@ -0,0 +1,1228 @@ +// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace isc; +using namespace isc::netconf; +using namespace isc::asiolink; +using namespace isc::config; +using namespace isc::data; +using namespace isc::http; +using namespace isc::util::thread; +using namespace isc::yang; +using namespace isc::yang::test; + +namespace { + +/// @brief Test unix socket file name. +const string TEST_SOCKET = "test-socket"; + +/// @brief Test timeout in ms. +const long TEST_TIMEOUT = 10000; + +/// @brief Type definition for the pointer to Thread objects. +typedef boost::shared_ptr ThreadPtr; + +/// @brief Test version of the NetconfAgent class. +class NakedNetconfAgent : public NetconfAgent { +public: + /// @brief Constructor. + NakedNetconfAgent() { + } + + /// @brief Destructor. + virtual ~NakedNetconfAgent() { + } + + /// Export protected methods and fields. + using NetconfAgent::keaConfig; + using NetconfAgent::initSysrepo; + using NetconfAgent::yangConfig; + using NetconfAgent::subscribe; + using NetconfAgent::conn_; + using NetconfAgent::startup_sess_; + using NetconfAgent::running_sess_; + using NetconfAgent::subscriptions_; +}; + +/// @brief Type definition for the pointer to NakedNetconfAgent objects. +typedef boost::shared_ptr NakedNetconfAgentPtr; + +/// @brief Clear YANG configuration. +/// +/// @param agent The naked netconf agent (fr its startup datastore session). +void clearYang(NakedNetconfAgentPtr agent) { + if (agent && (agent->startup_sess_)) { + string xpath = "/kea-dhcp4-server:config"; + EXPECT_NO_THROW(agent->startup_sess_->delete_item(xpath.c_str())); + xpath = "/kea-dhcp4-server:logging"; + EXPECT_NO_THROW(agent->startup_sess_->delete_item(xpath.c_str())); + EXPECT_NO_THROW(agent->startup_sess_->commit()); + } +} + +/// @brief Prune JSON configuration. +/// +/// Typically remove defaults and other extra entries. +/// +/// @param expected The expected configuration (the model). +/// @param other The other configuration (the to be pruned). +/// @return A copy of the other configuration with extra entries in maps +/// removed so it can be directly compared to expected. +ConstElementPtr prune(ConstElementPtr expected, ConstElementPtr other) { + if (!expected || !other) { + isc_throw(BadValue, "prune on null"); + } + if ((expected->getType() == Element::list) && + (other->getType() == Element::list)) { + // Handle ordered list. + ElementPtr result = Element::createList(); + for (size_t i = 0; i < other->size(); ++i) { + if (i > expected->size()) { + // Add extra elements. + result->add(copy(other->get(i))); + } else { + // Add pruned element. + result->add(copy(prune(expected->get(i), other->get(i)))); + } + } + return (result); + } else if ((expected->getType() == Element::map) && + (other->getType() == Element::map)) { + // Handle map. + ElementPtr result = Element::createMap(); + for (auto it : expected->mapValue()) { + ConstElementPtr item = other->get(it.first); + if (item) { + // Set pruned item. + result->set(it.first, copy(prune(it.second, item))); + } + } + return (result); + } else { + // Not list or map: just return it. + return (other); + } +} + +/// @brief Test fixture class for netconf agent. +class NetconfAgentTest : public ::testing::Test { +public: + /// @brief Constructor. + NetconfAgentTest() + : io_service_(new IOService()), + thread_(), + agent_(new NakedNetconfAgent), + requests_(), + responses_(), + ready_(false) { + NetconfProcess::global_shut_down_flag = false; + removeUnixSocketFile(); + } + + /// @brief Destructor. + virtual ~NetconfAgentTest() { + NetconfProcess::global_shut_down_flag = true; + io_service_->stop(); + io_service_.reset(); + if (thread_) { + thread_->wait(); + thread_.reset(); + } + if (agent_) { + clearYang(agent_); + agent_->clear(); + } + agent_.reset(); + requests_.clear(); + responses_.clear(); + removeUnixSocketFile(); + } + + /// @brief Returns socket file path. + /// + /// If the KEA_SOCKET_TEST_DIR environment variable is specified, the + /// socket file is created in the location pointed to by this variable. + /// Otherwise, it is created in the build directory. + static string unixSocketFilePath() { + ostringstream s; + const char* env = getenv("KEA_SOCKET_TEST_DIR"); + if (env) { + s << string(env); + } else { + s << TEST_DATA_BUILDDIR; + } + + s << "/" << TEST_SOCKET; + return (s.str()); + } + + /// @brief Removes unix socket descriptor. + void removeUnixSocketFile() { + static_cast(remove(unixSocketFilePath().c_str())); + } + + /// @brief Create configuration of the control socket. + /// + /// @return a pointer to a control socket configuration. + CfgControlSocketPtr createCfgControlSocket() { + CfgControlSocketPtr cfg; + cfg.reset(new CfgControlSocket(CfgControlSocket::Type::UNIX, + unixSocketFilePath(), + Url("http://127.0.0.1:8000/"))); + return (cfg); + } + + /// @brief Fake server (returns OK answer). + void fakeServer(); + + /// @brief IOService object. + IOServicePtr io_service_; + + /// @brief Pointer to server thread. + ThreadPtr thread_; + + /// @brief Test netconf agent. + NakedNetconfAgentPtr agent_; + + /// @brief Request list. + vector requests_; + + /// @brief Response list. + vector responses_; + + /// @brief Ready flag. + bool ready_; +}; + +/// @brief Special test fixture for logging tests. +class NetconfAgentLogTest : public dhcp::test::LogContentTest { +public: + /// @brief Constructor. + NetconfAgentLogTest() + : io_service_(new IOService()), + thread_(), + agent_(new NakedNetconfAgent) { + NetconfProcess::global_shut_down_flag = false; + } + + /// @brief Destructor. + virtual ~NetconfAgentLogTest() { + NetconfProcess::global_shut_down_flag = true; + io_service_->stop(); + io_service_.reset(); + if (thread_) { + thread_->wait(); + thread_.reset(); + } + if (agent_) { + clearYang(agent_); + agent_->clear(); + } + agent_.reset(); + } + + /// @brief IOService object. + IOServicePtr io_service_; + + /// @brief Pointer to server thread. + ThreadPtr thread_; + + /// @brief Test netconf agent. + NakedNetconfAgentPtr agent_; +}; + +/// @brief Fake server (returns OK answer). +void +NetconfAgentTest::fakeServer() { + // Acceptor. + boost::asio::local::stream_protocol::acceptor + acceptor(io_service_->get_io_service()); + EXPECT_NO_THROW(acceptor.open()); + boost::asio::local::stream_protocol::endpoint + endpoint(unixSocketFilePath()); + boost::asio::socket_base::reuse_address option(true); + acceptor.set_option(option); + EXPECT_NO_THROW(acceptor.bind(endpoint)); + EXPECT_NO_THROW(acceptor.listen()); + boost::asio::local::stream_protocol::socket + socket(io_service_->get_io_service()); + + // Ready. + ready_ = true; + + // Timeout. + bool timeout = false; + IntervalTimer timer(*io_service_); + timer.setup([&timeout]() { + timeout = true; + FAIL() << "timeout"; + }, 1500, IntervalTimer::ONE_SHOT); + + // Accept. + boost::system::error_code ec; + bool accepted = false; + acceptor.async_accept(socket, + [&ec, &accepted] + (const boost::system::error_code& error) { + ec = error; + accepted = true; + }); + while (!accepted && !timeout) { + io_service_->run_one(); + } + ASSERT_FALSE(ec); + + // Receive command. + string rbuf(1024, ' '); + size_t received = 0; + socket.async_receive(boost::asio::buffer(&rbuf[0], rbuf.size()), + [&ec, &received] + (const boost::system::error_code& error, size_t cnt) { + ec = error; + received = cnt; + }); + while (!received && !timeout) { + io_service_->run_one(); + } + ASSERT_FALSE(ec); + rbuf.resize(received); + requests_.push_back(rbuf); + ConstElementPtr json; + EXPECT_NO_THROW(json = Element::fromJSON(rbuf)); + EXPECT_TRUE(json); + string command; + ConstElementPtr config; + if (json) { + ConstElementPtr arg; + EXPECT_NO_THROW(command = parseCommand(arg, json)); + if (command == "config-get") { + config = Element::fromJSON("{ \"comment\": \"empty\" }"); + } + } + + // Send answer. + string sbuf = createAnswer(CONTROL_RESULT_SUCCESS, config)->str(); + responses_.push_back(sbuf); + size_t sent = 0; + socket.async_send(boost::asio::buffer(&sbuf[0], sbuf.size()), + [&ec, &sent] + (const boost::system::error_code& error, size_t cnt) { + ec = error; + sent = cnt; + }); + while (!sent && !timeout) { + io_service_->run_one(); + } + ASSERT_FALSE(ec); + + // Stop timer. + timer.cancel(); + + /// Finished. + EXPECT_FALSE(timeout); + EXPECT_TRUE(accepted); + EXPECT_TRUE(received); + EXPECT_TRUE(sent); + EXPECT_EQ(sent, sbuf.size()); + + if (socket.is_open()) { + EXPECT_NO_THROW(socket.close()); + } + EXPECT_NO_THROW(acceptor.close()); + removeUnixSocketFile(); + + // Done. + ready_ = false; +} + +/// Verifies the initSysrepo method opens sysrepo connection and sessions. +TEST_F(NetconfAgentTest, initSysrepo) { + EXPECT_NO_THROW(agent_->initSysrepo()); + EXPECT_TRUE(agent_->conn_); + EXPECT_TRUE(agent_->startup_sess_); + EXPECT_TRUE(agent_->running_sess_); +} + +/// @brief Default change callback (print changes and return OK). +class TestCallback : public Callback { +public: + int module_change(S_Session sess, + const char* /*module_name*/, + sr_notif_event_t /*event*/, + void* /*private_ctx*/) { + NetconfAgent::logChanges(sess, "/kea-dhcp4-server:config"); + NetconfAgent::logChanges(sess, "/kea-dhcp4-server:logging"); + finished = true; + return (SR_ERR_OK); + } + + // To know when the callback was called. + static bool finished; +}; + +bool TestCallback::finished = false; + +/// Verifies the logChanges method handles correctly changes. +TEST_F(NetconfAgentLogTest, logChanges) { + // Initial YANG configuration. + const YRTree tree0 = { + { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/id", + "1", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/subnet", + "10.0.0.0/24", SR_STRING_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/id", + "2", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/subnet", + "10.0.2.0/24", SR_STRING_T, true } + }; + // Load initial YANG configuration. + ASSERT_NO_THROW(agent_->initSysrepo()); + YangRepr repr(KEA_DHCP4_SERVER); + ASSERT_NO_THROW(repr.set(tree0, agent_->startup_sess_)); + EXPECT_NO_THROW(agent_->startup_sess_->commit()); + + // Subscribe changes. + S_Subscribe subs(new Subscribe(agent_->running_sess_)); + S_Callback cb(new TestCallback()); + TestCallback::finished = false; + EXPECT_NO_THROW(subs->module_change_subscribe(KEA_DHCP4_SERVER.c_str(), + cb, 0, 0, + SR_SUBSCR_APPLY_ONLY)); + thread_.reset(new Thread([this]() { io_service_->run(); })); + + // Change configuration (subnet #1 moved from 10.0.0.0/24 to 10.0.1/0/24). + const YRTree tree1 = { + { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/id", + "1", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/subnet", + "10.0.1.0/24", SR_STRING_T, true }, // The change is here! + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/id", + "2", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/subnet", + "10.0.2.0/24", SR_STRING_T, true } + }; + EXPECT_NO_THROW(repr.set(tree1, agent_->running_sess_)); + EXPECT_NO_THROW(agent_->running_sess_->commit()); + + // Check that the debug output was correct. + addString("NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: " + "modified: " + "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/subnet = " + "10.0.0.0/24 => " + "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/subnet = " + "10.0.1.0/24"); + + // logChanges is called in another thread so we can have to wait for it. + while (!TestCallback::finished) { + usleep(1000); + } + // Enable this for debugging. +#if 0 + logCheckVerbose(true); +#endif + EXPECT_TRUE(checkFile()); +} + +/// Verifies the logChanges method handles correctly changes. +/// Instead of the simple modified of the previous etst, now there will +/// deleted, created and moved. +TEST_F(NetconfAgentLogTest, logChanges2) { + // Initial YANG configuration. + const YRTree tree0 = { + { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/id", + "1", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/subnet", + "10.0.0.0/24", SR_STRING_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/id", + "2", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/subnet", + "10.0.2.0/24", SR_STRING_T, true } + }; + // Load initial YANG configuration. + ASSERT_NO_THROW(agent_->initSysrepo()); + YangRepr repr(KEA_DHCP4_SERVER); + ASSERT_NO_THROW(repr.set(tree0, agent_->startup_sess_)); + EXPECT_NO_THROW(agent_->startup_sess_->commit()); + + // Subscribe changes. + S_Subscribe subs(new Subscribe(agent_->running_sess_)); + S_Callback cb(new TestCallback()); + TestCallback::finished = false; + EXPECT_NO_THROW(subs->module_change_subscribe(KEA_DHCP4_SERVER.c_str(), + cb, 0, 0, + SR_SUBSCR_APPLY_ONLY)); + thread_.reset(new Thread([this]() { io_service_->run(); })); + + // Change configuration (subnet #1 moved to #10). + string xpath = "/kea-dhcp4-server:config/subnet4/subnet4[id='1']"; + EXPECT_NO_THROW(agent_->running_sess_->delete_item(xpath.c_str())); + const YRTree tree1 = { + { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='10']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='10']/id", + "10", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='10']/subnet", + "10.0.0.0/24", SR_STRING_T, true }, // The change is here! + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/id", + "2", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/subnet", + "10.0.2.0/24", SR_STRING_T, true } + }; + EXPECT_NO_THROW(repr.set(tree1, agent_->running_sess_)); + EXPECT_NO_THROW(agent_->running_sess_->commit()); + + // Check that the debug output was correct. + addString("NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: " + "deleted: " + "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/id = 1"); + addString("NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: " + "deleted: " + "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/subnet = " + "10.0.0.0/24"); + addString("NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: " + "deleted: " + "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/" + "reservation-mode = all [default]"); + addString("NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: " + "deleted: " + "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/" + "match-client-id = true [default]"); + addString("NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: " + "deleted: " + "/kea-dhcp4-server:config/subnet4/subnet4[id='1'] " + "(list instance)"); + addString("NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: " + "created: " + "/kea-dhcp4-server:config/subnet4/subnet4[id='10'] " + "(list instance)"); + addString("NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: " + "created: " + "/kea-dhcp4-server:config/subnet4/subnet4[id='10']/id = 10"); + addString("NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: " + "created: " + "/kea-dhcp4-server:config/subnet4/subnet4[id='10']/subnet = " + "10.0.0.0/24"); + addString("NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: " + "created: " + "/kea-dhcp4-server:config/subnet4/subnet4[id='10']/" + "reservation-mode = all [default]"); + addString("NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: " + "created: " + "/kea-dhcp4-server:config/subnet4/subnet4[id='10']/" + "match-client-id = true [default]"); + addString("NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: " + "moved: " + "/kea-dhcp4-server:config/subnet4/subnet4[id='10'] first"); + + // logChanges is called in another thread so we can have to wait for it. + while (!TestCallback::finished) { + usleep(1000); + } + // Enable this for debugging. +#if 0 + logCheckVerbose(true); +#endif + EXPECT_TRUE(checkFile()); +} + +/// Verifies the keaConfig method works as expected. +TEST_F(NetconfAgentTest, keaConfig) { + // Netconf configuration. + string config_prefix = "{\n" + " \"Netconf\": {\n" + " \"managed-servers\": {\n" + " \"dhcp4\": {\n" + " \"control-socket\": {\n" + " \"socket-type\": \"unix\",\n" + " \"socket-name\": \""; + string config_trailer = "\"\n" + " }\n" + " }\n" + " }\n" + " }\n" + "}"; + string config = config_prefix + unixSocketFilePath() + config_trailer; + NetconfConfigPtr ctx(new NetconfConfig()); + ElementPtr json; + ParserContext parser_context; + EXPECT_NO_THROW(json = + parser_context.parseString(config, ParserContext::PARSER_NETCONF)); + ASSERT_TRUE(json); + ASSERT_EQ(Element::map, json->getType()); + ConstElementPtr netconf_json = json->get("Netconf"); + ASSERT_TRUE(netconf_json); + json = boost::const_pointer_cast(netconf_json); + ASSERT_TRUE(json); + NetconfSimpleParser::setAllDefaults(json); + NetconfSimpleParser::deriveParameters(json); + NetconfSimpleParser parser; + EXPECT_NO_THROW(parser.parse(ctx, json, false)); + + // Get service pair. + CfgServersMapPtr servers_map = ctx->getCfgServersMap(); + ASSERT_TRUE(servers_map); + ASSERT_EQ(1, servers_map->size()); + CfgServersMapPair service_pair = *servers_map->begin(); + + // Launch server. + thread_.reset(new Thread([this]() { fakeServer(); })); + while (!ready_) { + usleep(1000); + } + + // Try keaConfig. + EXPECT_NO_THROW(agent_->keaConfig(service_pair)); + + // Wait server. + while (ready_) { + usleep(1000); + } + + // Check request. + ASSERT_EQ(1, requests_.size()); + const string& request_str = requests_[0]; + ConstElementPtr request; + ASSERT_NO_THROW(request = Element::fromJSON(request_str)); + string expected_str = "{\n" + "\"command\": \"config-get\"\n" + "}"; + ConstElementPtr expected; + ASSERT_NO_THROW(expected = Element::fromJSON(expected_str)); + ConstElementPtr pruned = prune(expected, request); + EXPECT_TRUE(expected->equals(*pruned)); + // Alternative showing more for debugging... +#if 0 + EXPECT_EQ(prettyPrint(expected), prettyPrint(pruned)); +#endif + + // Check response. + ASSERT_EQ(1, responses_.size()); + const string& response_str = responses_[0]; + ConstElementPtr response; + ASSERT_NO_THROW(response = Element::fromJSON(response_str)); + expected_str = "{\n" + "\"result\": 0,\n" + "\"arguments\": {\n" + " \"comment\": \"empty\"\n" + " }\n" + "}"; + ASSERT_NO_THROW(expected = Element::fromJSON(expected_str)); + pruned = prune(expected, response); + EXPECT_TRUE(expected->equals(*pruned)); +} + +/// Verifies the yangConfig method works as expected: apply YANG config +/// to the server. +TEST_F(NetconfAgentTest, yangConfig) { + // YANG configuration. + const YRTree tree = { + { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/id", + "1", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/subnet", + "10.0.0.0/24", SR_STRING_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/id", + "2", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/subnet", + "10.0.2.0/24", SR_STRING_T, true } + }; + // Load YANG configuration. + ASSERT_NO_THROW(agent_->initSysrepo()); + YangRepr repr(KEA_DHCP4_SERVER); + ASSERT_NO_THROW(repr.set(tree, agent_->startup_sess_)); + EXPECT_NO_THROW(agent_->startup_sess_->commit()); + + // Netconf configuration. + string config_prefix = "{\n" + " \"Netconf\": {\n" + " \"managed-servers\": {\n" + " \"dhcp4\": {\n" + " \"control-socket\": {\n" + " \"socket-type\": \"unix\",\n" + " \"socket-name\": \""; + string config_trailer = "\"\n" + " }\n" + " }\n" + " }\n" + " }\n" + "}"; + string config = config_prefix + unixSocketFilePath() + config_trailer; + NetconfConfigPtr ctx(new NetconfConfig()); + ElementPtr json; + ParserContext parser_context; + EXPECT_NO_THROW(json = + parser_context.parseString(config, ParserContext::PARSER_NETCONF)); + ASSERT_TRUE(json); + ASSERT_EQ(Element::map, json->getType()); + ConstElementPtr netconf_json = json->get("Netconf"); + ASSERT_TRUE(netconf_json); + json = boost::const_pointer_cast(netconf_json); + ASSERT_TRUE(json); + NetconfSimpleParser::setAllDefaults(json); + NetconfSimpleParser::deriveParameters(json); + NetconfSimpleParser parser; + EXPECT_NO_THROW(parser.parse(ctx, json, false)); + + // Get service pair. + CfgServersMapPtr servers_map = ctx->getCfgServersMap(); + ASSERT_TRUE(servers_map); + ASSERT_EQ(1, servers_map->size()); + CfgServersMapPair service_pair = *servers_map->begin(); + + // Launch server. + thread_.reset(new Thread([this]() { fakeServer(); })); + while (!ready_) { + usleep(1000); + } + + // Try yangConfig. + EXPECT_NO_THROW(agent_->yangConfig(service_pair)); + + // Wait server. + while (ready_) { + usleep(1000); + } + + // Check request. + ASSERT_EQ(1, requests_.size()); + const string& request_str = requests_[0]; + ConstElementPtr request; + ASSERT_NO_THROW(request = Element::fromJSON(request_str)); + string expected_str = "{\n" + "\"command\": \"config-set\",\n" + "\"arguments\": {\n" + " \"Dhcp4\": {\n" + " \"subnet4\": [\n" + " {\n" + " \"id\": 1,\n" + " \"subnet\": \"10.0.0.0/24\"\n" + " },\n" + " {\n" + " \"id\": 2,\n" + " \"subnet\": \"10.0.2.0/24\"\n" + " }\n" + " ]\n" + " }\n" + " }\n" + "}"; + ConstElementPtr expected; + ASSERT_NO_THROW(expected = Element::fromJSON(expected_str)); + ConstElementPtr pruned = prune(expected, request); + EXPECT_TRUE(expected->equals(*pruned)); + + // Check response. + ASSERT_EQ(1, responses_.size()); + const string& response_str = responses_[0]; + ConstElementPtr response; + ASSERT_NO_THROW(response = Element::fromJSON(response_str)); + expected_str = "{\n" + "\"result\": 0\n" + "}"; + ASSERT_NO_THROW(expected = Element::fromJSON(expected_str)); + pruned = prune(expected, response); + EXPECT_TRUE(expected->equals(*pruned)); +} + +/// Verifies the subscribe method works as expected. +TEST_F(NetconfAgentTest, subscribe) { + // Netconf configuration. + string config_prefix = "{\n" + " \"Netconf\": {\n" + " \"managed-servers\": {\n" + " \"dhcp4\": {\n" + " \"control-socket\": {\n" + " \"socket-type\": \"unix\",\n" + " \"socket-name\": \""; + string config_trailer = "\"\n" + " }\n" + " }\n" + " }\n" + " }\n" + "}"; + string config = config_prefix + unixSocketFilePath() + config_trailer; + NetconfConfigPtr ctx(new NetconfConfig()); + ElementPtr json; + ParserContext parser_context; + EXPECT_NO_THROW(json = + parser_context.parseString(config, ParserContext::PARSER_NETCONF)); + ASSERT_TRUE(json); + ASSERT_EQ(Element::map, json->getType()); + ConstElementPtr netconf_json = json->get("Netconf"); + ASSERT_TRUE(netconf_json); + json = boost::const_pointer_cast(netconf_json); + ASSERT_TRUE(json); + NetconfSimpleParser::setAllDefaults(json); + NetconfSimpleParser::deriveParameters(json); + NetconfSimpleParser parser; + EXPECT_NO_THROW(parser.parse(ctx, json, false)); + + // Get service pair. + CfgServersMapPtr servers_map = ctx->getCfgServersMap(); + ASSERT_TRUE(servers_map); + ASSERT_EQ(1, servers_map->size()); + CfgServersMapPair service_pair = *servers_map->begin(); + + // Try subscribe. + EXPECT_EQ(0, agent_->subscriptions_.size()); + ASSERT_NO_THROW(agent_->initSysrepo()); + EXPECT_NO_THROW(agent_->subscribe(service_pair)); + EXPECT_EQ(1, agent_->subscriptions_.size()); + + /// Unsubscribe. + EXPECT_NO_THROW(agent_->subscriptions_.clear()); +} + +/// Verifies the update method works as expected: apply new YNAG configuration +/// to the server. Note it is called by the subscription callback. +TEST_F(NetconfAgentTest, update) { + // Initial YANG configuration. + const YRTree tree0 = { + { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/id", + "1", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/subnet", + "10.0.0.0/24", SR_STRING_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/id", + "2", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/subnet", + "10.0.2.0/24", SR_STRING_T, true } + }; + // Load initial YANG configuration. + ASSERT_NO_THROW(agent_->initSysrepo()); + YangRepr repr(KEA_DHCP4_SERVER); + ASSERT_NO_THROW(repr.set(tree0, agent_->startup_sess_)); + EXPECT_NO_THROW(agent_->startup_sess_->commit()); + + // Netconf configuration. + // Set validate-changes to false to avoid validate() to be called. + string config_prefix = "{\n" + " \"Netconf\": {\n" + " \"validate-changes\": false,\n" + " \"managed-servers\": {\n" + " \"dhcp4\": {\n" + " \"control-socket\": {\n" + " \"socket-type\": \"unix\",\n" + " \"socket-name\": \""; + string config_trailer = "\"\n" + " }\n" + " }\n" + " }\n" + " }\n" + "}"; + string config = config_prefix + unixSocketFilePath() + config_trailer; + NetconfConfigPtr ctx(new NetconfConfig()); + ElementPtr json; + ParserContext parser_context; + EXPECT_NO_THROW(json = + parser_context.parseString(config, ParserContext::PARSER_NETCONF)); + ASSERT_TRUE(json); + ASSERT_EQ(Element::map, json->getType()); + ConstElementPtr netconf_json = json->get("Netconf"); + ASSERT_TRUE(netconf_json); + json = boost::const_pointer_cast(netconf_json); + ASSERT_TRUE(json); + NetconfSimpleParser::setAllDefaults(json); + NetconfSimpleParser::deriveParameters(json); + NetconfSimpleParser parser; + EXPECT_NO_THROW(parser.parse(ctx, json, false)); + + // Get service pair. + CfgServersMapPtr servers_map = ctx->getCfgServersMap(); + ASSERT_TRUE(servers_map); + ASSERT_EQ(1, servers_map->size()); + CfgServersMapPair service_pair = *servers_map->begin(); + + // Subscribe YANG changes. + EXPECT_EQ(0, agent_->subscriptions_.size()); + EXPECT_NO_THROW(agent_->subscribe(service_pair)); + EXPECT_EQ(1, agent_->subscriptions_.size()); + + // Launch server. + thread_.reset(new Thread([this]() { fakeServer(); })); + while (!ready_) { + usleep(1000); + } + + // Change configuration (subnet #1 moved from 10.0.0.0/24 to 10.0.1/0/24). + const YRTree tree1 = { + { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/id", + "1", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/subnet", + "10.0.1.0/24", SR_STRING_T, true }, // The change is here! + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/id", + "2", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/subnet", + "10.0.2.0/24", SR_STRING_T, true } + }; + EXPECT_NO_THROW(repr.set(tree1, agent_->running_sess_)); + EXPECT_NO_THROW(agent_->running_sess_->commit()); + + // Wait server. + while (ready_) { + usleep(1000); + } + + // Check request. + ASSERT_EQ(1, requests_.size()); + const string& request_str = requests_[0]; + ConstElementPtr request; + ASSERT_NO_THROW(request = Element::fromJSON(request_str)); + string expected_str = "{\n" + "\"command\": \"config-set\",\n" + "\"arguments\": {\n" + " \"Dhcp4\": {\n" + " \"subnet4\": [\n" + " {\n" + " \"id\": 1,\n" + " \"subnet\": \"10.0.1.0/24\"\n" + " },\n" + " {\n" + " \"id\": 2,\n" + " \"subnet\": \"10.0.2.0/24\"\n" + " }\n" + " ]\n" + " }\n" + " }\n" + "}"; + ConstElementPtr expected; + ASSERT_NO_THROW(expected = Element::fromJSON(expected_str)); + ConstElementPtr pruned = prune(expected, request); + EXPECT_TRUE(expected->equals(*pruned)); + + // Check response. + ASSERT_EQ(1, responses_.size()); + const string& response_str = responses_[0]; + ConstElementPtr response; + ASSERT_NO_THROW(response = Element::fromJSON(response_str)); + expected_str = "{\n" + "\"result\": 0\n" + "}"; + ASSERT_NO_THROW(expected = Element::fromJSON(expected_str)); + pruned = prune(expected, response); + EXPECT_TRUE(expected->equals(*pruned)); +} + +/// Verifies the validate method works as expected: test new YNAG configuration +/// with the server. Note it is called by the subscription callback and +/// update is called after. +TEST_F(NetconfAgentTest, validate) { + // Initial YANG configuration. + const YRTree tree0 = { + { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/id", + "1", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/subnet", + "10.0.0.0/24", SR_STRING_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/id", + "2", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/subnet", + "10.0.2.0/24", SR_STRING_T, true } + }; + // Load initial YANG configuration. + ASSERT_NO_THROW(agent_->initSysrepo()); + YangRepr repr(KEA_DHCP4_SERVER); + ASSERT_NO_THROW(repr.set(tree0, agent_->startup_sess_)); + EXPECT_NO_THROW(agent_->startup_sess_->commit()); + + // Netconf configuration. + string config_prefix = "{\n" + " \"Netconf\": {\n" + " \"managed-servers\": {\n" + " \"dhcp4\": {\n" + " \"control-socket\": {\n" + " \"socket-type\": \"unix\",\n" + " \"socket-name\": \""; + string config_trailer = "\"\n" + " }\n" + " }\n" + " }\n" + " }\n" + "}"; + string config = config_prefix + unixSocketFilePath() + config_trailer; + NetconfConfigPtr ctx(new NetconfConfig()); + ElementPtr json; + ParserContext parser_context; + EXPECT_NO_THROW(json = + parser_context.parseString(config, ParserContext::PARSER_NETCONF)); + ASSERT_TRUE(json); + ASSERT_EQ(Element::map, json->getType()); + ConstElementPtr netconf_json = json->get("Netconf"); + ASSERT_TRUE(netconf_json); + json = boost::const_pointer_cast(netconf_json); + ASSERT_TRUE(json); + NetconfSimpleParser::setAllDefaults(json); + NetconfSimpleParser::deriveParameters(json); + NetconfSimpleParser parser; + EXPECT_NO_THROW(parser.parse(ctx, json, false)); + + // Get service pair. + CfgServersMapPtr servers_map = ctx->getCfgServersMap(); + ASSERT_TRUE(servers_map); + ASSERT_EQ(1, servers_map->size()); + CfgServersMapPair service_pair = *servers_map->begin(); + + // Subscribe YANG changes. + EXPECT_EQ(0, agent_->subscriptions_.size()); + EXPECT_NO_THROW(agent_->subscribe(service_pair)); + EXPECT_EQ(1, agent_->subscriptions_.size()); + + // Launch server twice. + thread_.reset(new Thread([this]() { fakeServer(); fakeServer(); })); + while (!ready_) { + usleep(1000); + } + + // Change configuration (subnet #1 moved from 10.0.0.0/24 to 10.0.1/0/24). + const YRTree tree1 = { + { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/id", + "1", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/subnet", + "10.0.1.0/24", SR_STRING_T, true }, // The change is here! + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/id", + "2", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='2']/subnet", + "10.0.2.0/24", SR_STRING_T, true } + }; + EXPECT_NO_THROW(repr.set(tree1, agent_->running_sess_)); + EXPECT_NO_THROW(agent_->running_sess_->commit()); + + // Wait servers. + while (ready_) { + usleep(1000); + } + usleep(1000); + while (ready_) { + usleep(1000); + } + + // Check requests. + ASSERT_EQ(2, requests_.size()); + string request_str = requests_[0]; + ConstElementPtr request; + ASSERT_NO_THROW(request = Element::fromJSON(request_str)); + string expected_str = "{\n" + "\"command\": \"config-test\",\n" + "\"arguments\": {\n" + " \"Dhcp4\": {\n" + " \"subnet4\": [\n" + " {\n" + " \"id\": 1,\n" + " \"subnet\": \"10.0.1.0/24\"\n" + " },\n" + " {\n" + " \"id\": 2,\n" + " \"subnet\": \"10.0.2.0/24\"\n" + " }\n" + " ]\n" + " }\n" + " }\n" + "}"; + ConstElementPtr expected; + ASSERT_NO_THROW(expected = Element::fromJSON(expected_str)); + ConstElementPtr pruned = prune(expected, request); + EXPECT_TRUE(expected->equals(*pruned)); + + request_str = requests_[1]; + ASSERT_NO_THROW(request = Element::fromJSON(request_str)); + expected_str = "{\n" + "\"command\": \"config-set\",\n" + "\"arguments\": {\n" + " \"Dhcp4\": {\n" + " \"subnet4\": [\n" + " {\n" + " \"id\": 1,\n" + " \"subnet\": \"10.0.1.0/24\"\n" + " },\n" + " {\n" + " \"id\": 2,\n" + " \"subnet\": \"10.0.2.0/24\"\n" + " }\n" + " ]\n" + " }\n" + " }\n" + "}"; + ASSERT_NO_THROW(expected = Element::fromJSON(expected_str)); + pruned = prune(expected, request); + EXPECT_TRUE(expected->equals(*pruned)); + + // Check responses. + ASSERT_EQ(2, responses_.size()); + string response_str = responses_[0]; + ConstElementPtr response; + ASSERT_NO_THROW(response = Element::fromJSON(response_str)); + expected_str = "{\n" + "\"result\": 0\n" + "}"; + ASSERT_NO_THROW(expected = Element::fromJSON(expected_str)); + pruned = prune(expected, response); + EXPECT_TRUE(expected->equals(*pruned)); + + response_str = responses_[1]; + ASSERT_NO_THROW(response = Element::fromJSON(response_str)); + expected_str = "{\n" + "\"result\": 0\n" + "}"; + ASSERT_NO_THROW(expected = Element::fromJSON(expected_str)); + pruned = prune(expected, response); + EXPECT_TRUE(expected->equals(*pruned)); + +} + +/// Verifies what happens when the validate method returns an error. +TEST_F(NetconfAgentTest, noValidate) { + // Initial YANG configuration. + const YRTree tree0 = { + { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/id", + "1", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/subnet", + "10.0.0.0/24", SR_STRING_T, true } + }; + // Load initial YANG configuration. + ASSERT_NO_THROW(agent_->initSysrepo()); + YangRepr repr(KEA_DHCP4_SERVER); + ASSERT_NO_THROW(repr.set(tree0, agent_->startup_sess_)); + EXPECT_NO_THROW(agent_->startup_sess_->commit()); + + // Netconf configuration. + string config_prefix = "{\n" + " \"Netconf\": {\n" + " \"managed-servers\": {\n" + " \"dhcp4\": {\n" + " \"control-socket\": {\n" + " \"socket-type\": \"unix\",\n" + " \"socket-name\": \""; + string config_trailer = "\"\n" + " }\n" + " }\n" + " }\n" + " }\n" + "}"; + string config = config_prefix + unixSocketFilePath() + config_trailer; + NetconfConfigPtr ctx(new NetconfConfig()); + ElementPtr json; + ParserContext parser_context; + EXPECT_NO_THROW(json = + parser_context.parseString(config, ParserContext::PARSER_NETCONF)); + ASSERT_TRUE(json); + ASSERT_EQ(Element::map, json->getType()); + ConstElementPtr netconf_json = json->get("Netconf"); + ASSERT_TRUE(netconf_json); + json = boost::const_pointer_cast(netconf_json); + ASSERT_TRUE(json); + NetconfSimpleParser::setAllDefaults(json); + NetconfSimpleParser::deriveParameters(json); + NetconfSimpleParser parser; + EXPECT_NO_THROW(parser.parse(ctx, json, false)); + + // Get service pair. + CfgServersMapPtr servers_map = ctx->getCfgServersMap(); + ASSERT_TRUE(servers_map); + ASSERT_EQ(1, servers_map->size()); + CfgServersMapPair service_pair = *servers_map->begin(); + + // Subscribe YANG changes. + EXPECT_EQ(0, agent_->subscriptions_.size()); + EXPECT_NO_THROW(agent_->subscribe(service_pair)); + EXPECT_EQ(1, agent_->subscriptions_.size()); + + // Change configuration (add invalid user context). + const YRTree tree1 = { + { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4", "", SR_CONTAINER_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']", "", + SR_LIST_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/id", + "1", SR_UINT32_T, false }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/subnet", + "10.0.0.0/24", SR_STRING_T, true }, + { "/kea-dhcp4-server:config/subnet4/subnet4[id='1']/user-context", + "BOGUS", SR_STRING_T, true } + }; + EXPECT_NO_THROW(repr.set(tree1, agent_->running_sess_)); + try { + agent_->running_sess_->commit(); + } catch (const sysrepo_exception& ex) { + EXPECT_EQ("Validation of the changes failed", string(ex.what())); + } catch (const std::exception& ex) { + ADD_FAILURE() << "unexpected exception: " << ex.what(); + } catch (...) { + ADD_FAILURE() << "unexpected exception"; + } +} + +}