]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[153-netconf-agent] Added netconf agent code from kea-yang
authorFrancis Dupont <fdupont@isc.org>
Wed, 17 Oct 2018 20:10:27 +0000 (22:10 +0200)
committerFrancis Dupont <fdupont@isc.org>
Tue, 30 Oct 2018 11:50:38 +0000 (07:50 -0400)
src/bin/netconf/Makefile.am
src/bin/netconf/netconf.cc [new file with mode: 0644]
src/bin/netconf/netconf.h [new file with mode: 0644]
src/bin/netconf/netconf_log.h
src/bin/netconf/netconf_messages.mes
src/bin/netconf/netconf_process.cc
src/bin/netconf/netconf_process.h
src/bin/netconf/tests/Makefile.am
src/bin/netconf/tests/netconf_unittests.cc [new file with mode: 0644]

index 73b3aa401956a0bd39c21779fbd8dfe76286d51f..a2ed58c31acca137397a9a7cac811bf6c1daa03f 100644 (file)
@@ -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 (file)
index 0000000..a125627
--- /dev/null
@@ -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 <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
diff --git a/src/bin/netconf/netconf.h b/src/bin/netconf/netconf.h
new file mode 100644 (file)
index 0000000..7142d5a
--- /dev/null
@@ -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 <netconf/netconf_cfg_mgr.h>
+#include <netconf/control_socket.h>
+#include <netconf/http_control_socket.h>
+#include <netconf/stdout_control_socket.h>
+#include <netconf/unix_control_socket.h>
+#include <sysrepo-cpp/Session.h>
+#include <map>
+
+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<NetconfAgent> 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<const std::string, S_Subscribe> subscriptions_;
+};
+
+} // namespace netconf
+} // namespace isc
+
+#endif // NETCONF_H
index 5758a51b8ccaabbef7190022c58fb2b1942e34f3..80e7ce0c70d59d489f57cd8207be68d79512bc3e 100644 (file)
 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;
 
index ecc9fe77ec2b2a2147862376e6cd052faaf1c06a..2c7222a321a9eb97e53aedd5229d19ea06e40450 100644 (file)
@@ -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.
index d3c469a610c1fa5edee0a9267323ec8683ec2ad8..94b536284660cdfa881162454f1dc8b455c30cc4 100644 (file)
@@ -6,6 +6,7 @@
 
 #include <config.h>
 #include <asiolink/asio_wrapper.h>
+#include <netconf/netconf.h>
 #include <netconf/netconf_process.h>
 #include <netconf/netconf_controller.h>
 #include <netconf/netconf_log.h>
@@ -13,6 +14,7 @@
 #include <asiolink/io_error.h>
 #include <cc/command_interpreter.h>
 #include <config/timeouts.h>
+#include <util/threads/thread.h>
 #include <boost/pointer_cast.hpp>
 
 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"));
 }
index 26824b53905ea8a6d9aba7e47b41e2cc3856df14..bcd082a6c29f088c99795a3317c84440216f00a0 100644 (file)
@@ -7,8 +7,7 @@
 #ifndef NETCONF_PROCESS_H
 #define NETCONF_PROCESS_H
 
-#include <netconf/netconf_cfg_mgr.h>
-#include <http/listener.h>
+#include <netconf/netconf.h>
 #include <process/d_process.h>
 #include <vector>
 
@@ -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.
index 1da8e6af9dcb198189f738a9d065406ac2458295..3b249a65f9ac60ddb2fe3859685da841555994d8 100644 (file)
@@ -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 (file)
index 0000000..61abe26
--- /dev/null
@@ -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 <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";
+    }
+}
+
+}