--- /dev/null
+// 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 <config.h>
+
+#include <netconf/netconf.h>
+#include <netconf/netconf_controller.h>
+#include <netconf/netconf_log.h>
+#include <cc/command_interpreter.h>
+#include <yang/translator_config.h>
+#include <boost/algorithm/string.hpp>
+#include <sstream>
+
+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
--- /dev/null
+// 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 <config.h>
+#include <netconf/netconf.h>
+#include <netconf/netconf_process.h>
+#include <netconf/parser_context.h>
+#include <netconf/simple_parser.h>
+#include <netconf/unix_control_socket.h>
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/interval_timer.h>
+#include <asiolink/io_service.h>
+#include <cc/command_interpreter.h>
+#include <util/threads/thread.h>
+#include <yang/yang_models.h>
+#include <yang/translator_config.h>
+#include <yang/testutils/translator_test.h>
+#include <testutils/log_utils.h>
+#include <gtest/gtest.h>
+#include <sstream>
+
+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<Thread> 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<NakedNetconfAgent> 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<void>(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<string> requests_;
+
+ /// @brief Response list.
+ vector<string> 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<Element>(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<Element>(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<Element>(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<Element>(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<Element>(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<Element>(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";
+ }
+}
+
+}