summaryrefslogtreecommitdiffstats
path: root/src/bin/netconf/tests/netconf_unittests.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/netconf/tests/netconf_unittests.cc')
-rw-r--r--src/bin/netconf/tests/netconf_unittests.cc1212
1 files changed, 1212 insertions, 0 deletions
diff --git a/src/bin/netconf/tests/netconf_unittests.cc b/src/bin/netconf/tests/netconf_unittests.cc
new file mode 100644
index 0000000..bd9be00
--- /dev/null
+++ b/src/bin/netconf/tests/netconf_unittests.cc
@@ -0,0 +1,1212 @@
+// Copyright (C) 2018-2021 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 <yang/yang_models.h>
+#include <yang/yang_revisions.h>
+#include <yang/tests/sysrepo_setup.h>
+#include <yang/translator_config.h>
+#include <yang/testutils/translator_test.h>
+#include <testutils/log_utils.h>
+#include <testutils/threaded_test.h>
+#include <testutils/sandbox.h>
+
+#include <gtest/gtest.h>
+
+#include <sstream>
+#include <thread>
+
+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::test;
+using namespace isc::yang;
+using namespace isc::yang::test;
+using namespace sysrepo;
+
+using isc::yang::test::SysrepoSetup;
+
+namespace {
+
+/// @brief Test unix socket file name.
+const string TEST_SOCKET = "test-socket";
+
+/// @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::checkModule;
+ using NetconfAgent::checkModules;
+ using NetconfAgent::yangConfig;
+ using NetconfAgent::subscribeConfig;
+ using NetconfAgent::conn_;
+ using NetconfAgent::startup_sess_;
+ using NetconfAgent::running_sess_;
+ using NetconfAgent::modules_;
+ 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()));
+ EXPECT_NO_THROW(agent->startup_sess_->apply_changes());
+ }
+}
+
+
+// Empirically the requested subnets have sometimes returned in decreasing
+// order of subnet ID. To avoid flaky test failures, sort them before
+// comparing.
+void sortSubnets(ConstElementPtr const& map) {
+ boost::dynamic_pointer_cast<ListElement>(
+ boost::const_pointer_cast<Element>(
+ map->get("arguments")->get("Dhcp4")->get("subnet4")))
+ ->sort("id");
+}
+
+/// @brief Test fixture class for netconf agent.
+class NetconfAgentTest : public ThreadedTest {
+public:
+ isc::test::Sandbox sandbox;
+
+ void SetUp() override {
+ SysrepoSetup::cleanSharedMemory();
+ removeUnixSocketFile();
+ io_service_.reset(new IOService());
+ agent_.reset(new NakedNetconfAgent());
+ }
+
+ void TearDown() override {
+ if (thread_) {
+ thread_->join();
+ thread_.reset();
+ }
+ // io_service must be stopped after the thread returns,
+ // otherwise the thread may never return if it is
+ // waiting for the completion of some asynchronous tasks.
+ io_service_->stop();
+ io_service_.reset();
+ if (agent_) {
+ clearYang(agent_);
+ agent_->clear();
+ }
+ agent_.reset();
+ requests_.clear();
+ responses_.clear();
+ removeUnixSocketFile();
+ SysrepoSetup::cleanSharedMemory();
+ }
+
+ /// @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.
+ string unixSocketFilePath() {
+ std::string socket_path;
+ const char* env = getenv("KEA_SOCKET_TEST_DIR");
+ if (env) {
+ socket_path = std::string(env) + "/test-socket";
+ } else {
+ socket_path = sandbox.join("test-socket");
+ }
+ return (socket_path);
+ }
+
+ /// @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 Test netconf agent.
+ NakedNetconfAgentPtr agent_;
+
+ /// @brief Request list.
+ vector<string> requests_;
+
+ /// @brief Response list.
+ vector<string> responses_;
+};
+
+/// @brief Special test fixture for logging tests.
+class NetconfAgentLogTest : public dhcp::test::LogContentTest {
+public:
+ /// @brief Constructor.
+ NetconfAgentLogTest()
+ : finished_(false),
+ io_service_(new IOService()),
+ thread_(),
+ agent_(new NakedNetconfAgent) {
+ }
+
+ /// @brief Destructor.
+ virtual ~NetconfAgentLogTest() {
+ if (agent_) {
+ clearYang(agent_);
+ agent_->clear();
+ }
+ agent_.reset();
+ // io_service must be stopped to make the thread to return.
+ io_service_->stop();
+ if (thread_) {
+ thread_->join();
+ thread_.reset();
+ }
+ io_service_.reset();
+ }
+
+ /// @brief Default change callback (print changes and return OK).
+ sr_error_t callback(sysrepo::S_Session sess,
+ const char* module_name,
+ const char* /* xpath */,
+ sr_event_t /* event */,
+ uint32_t /* request_id */) {
+ NetconfAgent::logChanges(sess, module_name);
+ finished_ = true;
+ return (SR_ERR_OK);
+ }
+
+ /// @brief logChanges is called in another thread so we have to wait for it.
+ ///
+ /// @todo The better way to get notified and get rid of the sleep is with a
+ /// conditional variable.
+ void waitForCallback() {
+ while (!finished_) {
+ usleep(1000);
+ }
+ }
+
+ /// @brief To know when the callback was called.
+ std::atomic<bool> finished_;
+
+ /// @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.
+ signalReady();
+
+ // 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();
+
+ // Close socket and acceptor.
+ if (socket.is_open()) {
+ EXPECT_NO_THROW(socket.close());
+ }
+ EXPECT_NO_THROW(acceptor.close());
+ // Removed the socket file so it can be called again immediately.
+ removeUnixSocketFile();
+
+ /// Finished.
+ EXPECT_FALSE(timeout);
+ EXPECT_TRUE(accepted);
+ EXPECT_TRUE(received);
+ EXPECT_TRUE(sent);
+ EXPECT_EQ(sent, sbuf.size());
+
+ // signalStopped can't be called here because of the 2 runs for update.
+}
+
+// Verifies that 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_);
+ EXPECT_EQ(0, agent_->subscriptions_.size());
+ EXPECT_LE(16, agent_->modules_.size());
+}
+
+// Verifies that the checkModule method emits expected errors.
+TEST_F(NetconfAgentLogTest, checkModule) {
+ // keatest-module should not be in YANG_REVISIONS.
+ EXPECT_EQ(0, YANG_REVISIONS.count("keatest-module"));
+ // But kea-dhcp[46]-server must be in.
+ ASSERT_EQ(1, YANG_REVISIONS.count("kea-dhcp4-server"));
+ ASSERT_EQ(1, YANG_REVISIONS.count("kea-dhcp6-server"));
+
+ // kea-dhcp[46]-server should be available.
+ EXPECT_NO_THROW(agent_->initSysrepo());
+ EXPECT_EQ(1, agent_->modules_.count("kea-dhcp4-server"));
+ EXPECT_EQ(1, agent_->modules_.count("kea-dhcp6-server"));
+ EXPECT_TRUE(agent_->checkModule("kea-dhcp4-server"));
+ EXPECT_TRUE(agent_->checkModule("kea-dhcp6-server"));
+
+ // Unknown module should emit a missing error.
+ EXPECT_EQ(0, agent_->modules_.count("does-not-exist"));
+ EXPECT_FALSE(agent_->checkModule("does-not-exist"));
+ addString("NETCONF_MODULE_MISSING_ERR Missing essential module "
+ "does-not-exist in sysrepo");
+
+ // Patch the found revision to get a revision error.
+ const string& module = "kea-dhcp4-server";
+ auto it4 = agent_->modules_.find(module);
+ if (it4 != agent_->modules_.end()) {
+ agent_->modules_.erase(it4);
+ }
+ // The module was written far after 20180714...
+ const string& bad_revision = "2018-07-14";
+ agent_->modules_.insert(make_pair(module, bad_revision));
+ EXPECT_FALSE(agent_->checkModule(module));
+ ostringstream msg;
+ msg << "NETCONF_MODULE_REVISION_ERR Essential module " << module
+ << " does NOT have the right revision: expected "
+ << YANG_REVISIONS.at(module) << ", got " << bad_revision;
+ addString(msg.str());
+
+ EXPECT_TRUE(checkFile());
+}
+
+// Verifies that the checkModules method emits expected warnings.
+TEST_F(NetconfAgentLogTest, checkModules) {
+ // kea-dhcp[46]-server must be in YANG_REVISIONS.
+ ASSERT_EQ(1, YANG_REVISIONS.count("kea-dhcp4-server"));
+ ASSERT_EQ(1, YANG_REVISIONS.count("kea-dhcp6-server"));
+
+ // kea-dhcp[46]-server should be available.
+ EXPECT_NO_THROW(agent_->initSysrepo());
+ EXPECT_EQ(1, agent_->modules_.count("kea-dhcp4-server"));
+ EXPECT_EQ(1, agent_->modules_.count("kea-dhcp6-server"));
+
+ // Run checkModules but it will be indirectly checked as
+ // emitting nothing.
+ ASSERT_NO_THROW_LOG(agent_->checkModules());
+
+ // Remove kea-dhcp6-server.
+ const string& module = "kea-dhcp6-server";
+ auto it6 = agent_->modules_.find(module);
+ if (it6 != agent_->modules_.end()) {
+ agent_->modules_.erase(it6);
+ }
+ ASSERT_NO_THROW_LOG(agent_->checkModules());
+ ostringstream mmsg;
+ mmsg << "NETCONF_MODULE_MISSING_WARN Missing module " << module
+ << " in sysrepo";
+ addString(mmsg.str());
+
+ // Add it back with a bad revision.
+ const string& bad_revision = "2018-07-14";
+ agent_->modules_.insert(make_pair(module, bad_revision));
+ ASSERT_NO_THROW_LOG(agent_->checkModules());
+ ostringstream rmsg;
+ rmsg << "NETCONF_MODULE_REVISION_WARN Module " << module
+ << " does NOT have the right revision: expected "
+ << YANG_REVISIONS.at(module) << ", got " << bad_revision;
+ addString(rmsg.str());
+
+ EXPECT_TRUE(checkFile());
+}
+
+// Verifies that the logChanges method handles correctly changes.
+TEST_F(NetconfAgentLogTest, logChanges) {
+ // Initial YANG configuration.
+ const YRTree tree0 = YangRepr::buildTreeFromVector({
+ { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/id",
+ "1", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/subnet",
+ "10.0.0.0/24", SR_STRING_T, true },
+ { "/kea-dhcp4-server:config/subnet4[id='2']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='2']/id",
+ "2", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='2']/subnet",
+ "10.0.2.0/24", SR_STRING_T, true }
+ });
+ // Load initial YANG configuration.
+ ASSERT_NO_THROW_LOG(agent_->initSysrepo());
+ YangRepr repr(KEA_DHCP4_SERVER);
+ ASSERT_NO_THROW_LOG(repr.set(tree0, agent_->startup_sess_));
+ EXPECT_NO_THROW(agent_->startup_sess_->apply_changes());
+
+ // Subscribe configuration changes.
+ S_Subscribe subs(new Subscribe(agent_->running_sess_));
+ auto cb = [&](sysrepo::S_Session sess, const char* module_name,
+ const char* xpath, sr_event_t event, uint32_t request_id) {
+ return callback(sess, module_name, xpath, event, request_id);
+ };
+ EXPECT_NO_THROW(subs->module_change_subscribe(KEA_DHCP4_SERVER.c_str(), cb,
+ nullptr, 0,
+ SR_SUBSCR_DONE_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 = YangRepr::buildTreeFromVector({
+ { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/id",
+ "1", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/subnet",
+ "10.0.1.0/24", SR_STRING_T, true }, // The change is here!
+ { "/kea-dhcp4-server:config/subnet4[id='2']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='2']/id",
+ "2", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/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_->apply_changes());
+
+ // Check that the debug output was correct.
+ addString(
+ "NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: created: "
+ "/kea-dhcp4-server:config/subnet4[id='2'] (list instance)");
+ addString("NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: "
+ "created: /kea-dhcp4-server:config/subnet4[id='2']/id = 2");
+ addString(
+ "NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: created: "
+ "/kea-dhcp4-server:config/subnet4[id='2']/subnet = 10.0.2.0/24");
+ addString(
+ "NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: created: "
+ "/kea-dhcp4-server:config/subnet4[id='2']/relay (container)");
+
+ waitForCallback();
+
+ EXPECT_TRUE(checkFile());
+}
+
+// Verifies that the logChanges method handles correctly changes.
+// Instead of the simple modified of the previous test, now there will
+// deleted, created and moved.
+TEST_F(NetconfAgentLogTest, logChanges2) {
+ // Initial YANG configuration.
+ const YRTree tree0 = YangRepr::buildTreeFromVector({
+ { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/id",
+ "1", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/subnet",
+ "10.0.0.0/24", SR_STRING_T, true },
+ { "/kea-dhcp4-server:config/subnet4[id='2']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='2']/id",
+ "2", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='2']/subnet",
+ "10.0.2.0/24", SR_STRING_T, true }
+ });
+ // Load initial YANG configuration.
+ ASSERT_NO_THROW_LOG(agent_->initSysrepo());
+ YangRepr repr(KEA_DHCP4_SERVER);
+ ASSERT_NO_THROW_LOG(repr.set(tree0, agent_->startup_sess_));
+ EXPECT_NO_THROW(agent_->startup_sess_->apply_changes());
+
+ // Subscribe configuration changes.
+ S_Subscribe subs(new Subscribe(agent_->running_sess_));
+ auto cb = [&](sysrepo::S_Session sess, const char* module_name,
+ const char* xpath, sr_event_t event, uint32_t request_id) {
+ return callback(sess, module_name, xpath, event, request_id);
+ };
+ EXPECT_NO_THROW(subs->module_change_subscribe(KEA_DHCP4_SERVER.c_str(), cb,
+ nullptr, 0,
+ SR_SUBSCR_DONE_ONLY));
+ thread_.reset(new thread([this]() { io_service_->run(); }));
+
+ // Change configuration (subnet #1 moved to #10).
+ string xpath = "/kea-dhcp4-server:config/subnet4[id='1']";
+ EXPECT_NO_THROW(agent_->running_sess_->delete_item(xpath.c_str()));
+ const YRTree tree1 = YangRepr::buildTreeFromVector({
+ { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='10']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='10']/id",
+ "10", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='10']/subnet",
+ "10.0.0.0/24", SR_STRING_T, true }, // The change is here!
+ { "/kea-dhcp4-server:config/subnet4[id='2']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='2']/id",
+ "2", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/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_->apply_changes());
+
+ // Check that the debug output was correct.
+ addString(
+ "NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: deleted: "
+ "/kea-dhcp4-server:config/subnet4[id='1'] (list instance)");
+ addString("NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: "
+ "deleted: /kea-dhcp4-server:config/subnet4[id='1']/id = 1");
+ addString(
+ "NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: deleted: "
+ "/kea-dhcp4-server:config/subnet4[id='1']/subnet = 10.0.1.0/24");
+ addString(
+ "NETCONF_CONFIG_CHANGED_DETAIL YANG configuration changed: deleted: "
+ "/kea-dhcp4-server:config/subnet4[id='1']/relay (container)");
+
+ waitForCallback();
+
+ EXPECT_TRUE(checkFile());
+}
+
+// Verifies that 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(); signalStopped(); }));
+
+ // Wait until the server is listening.
+ waitReady();
+
+ // Try keaConfig.
+ EXPECT_NO_THROW(agent_->keaConfig(service_pair));
+
+ // Wait server to be stopped.
+ waitStopped();
+
+ // Check request.
+ ASSERT_EQ(1, requests_.size());
+ const string& request_str = requests_[0];
+ ConstElementPtr request;
+ ASSERT_NO_THROW_LOG(request = Element::fromJSON(request_str));
+ string expected_str = "{\n"
+ "\"command\": \"config-get\"\n"
+ "}";
+ ConstElementPtr expected;
+ ASSERT_NO_THROW_LOG(expected = Element::fromJSON(expected_str));
+ EXPECT_TRUE(expected->equals(*request));
+ // Alternative showing more for debugging...
+ // EXPECT_EQ(prettyPrint(expected), prettyPrint(request));
+
+ // Check response.
+ ASSERT_EQ(1, responses_.size());
+ const string& response_str = responses_[0];
+ ConstElementPtr response;
+ ASSERT_NO_THROW_LOG(response = Element::fromJSON(response_str));
+ expected_str = "{\n"
+ "\"result\": 0,\n"
+ "\"arguments\": {\n"
+ " \"comment\": \"empty\"\n"
+ " }\n"
+ "}";
+ ASSERT_NO_THROW_LOG(expected = Element::fromJSON(expected_str));
+ EXPECT_TRUE(expected->equals(*response));
+}
+
+// Verifies that the yangConfig method works as expected: apply YANG config
+// to the server.
+TEST_F(NetconfAgentTest, yangConfig) {
+ // YANG configuration.
+ const YRTree tree = YangRepr::buildTreeFromVector({
+ { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/id",
+ "1", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/subnet",
+ "10.0.0.0/24", SR_STRING_T, true },
+ { "/kea-dhcp4-server:config/subnet4[id='2']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='2']/id",
+ "2", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='2']/subnet",
+ "10.0.2.0/24", SR_STRING_T, true }
+ });
+ // Load YANG configuration.
+ ASSERT_NO_THROW_LOG(agent_->initSysrepo());
+ YangRepr repr(KEA_DHCP4_SERVER);
+ ASSERT_NO_THROW_LOG(repr.set(tree, agent_->startup_sess_));
+ EXPECT_NO_THROW(agent_->startup_sess_->apply_changes());
+
+ // 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(); signalStopped();}));
+
+ // Wait until the server is listening.
+ waitReady();
+
+ // Try yangConfig.
+ EXPECT_NO_THROW(agent_->yangConfig(service_pair));
+
+ // Wait server to be stopped.
+ waitStopped();
+
+ // Check request.
+ ASSERT_EQ(1, requests_.size());
+ const string& request_str = requests_[0];
+ ConstElementPtr request;
+ ASSERT_NO_THROW_LOG(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_LOG(expected = Element::fromJSON(expected_str));
+
+ sortSubnets(expected);
+ sortSubnets(request);
+ EXPECT_EQ(prettyPrint(expected), prettyPrint(request));
+
+ // Check response.
+ ASSERT_EQ(1, responses_.size());
+ const string& response_str = responses_[0];
+ ConstElementPtr response;
+ ASSERT_NO_THROW_LOG(response = Element::fromJSON(response_str));
+ expected_str = "{\n"
+ "\"result\": 0\n"
+ "}";
+ ASSERT_NO_THROW_LOG(expected = Element::fromJSON(expected_str));
+ EXPECT_TRUE(expected->equals(*response));
+}
+
+// Verifies that the subscribeConfig method works as expected.
+TEST_F(NetconfAgentTest, subscribeConfig) {
+ // 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 subscribeConfig.
+ EXPECT_EQ(0, agent_->subscriptions_.size());
+ ASSERT_NO_THROW_LOG(agent_->initSysrepo());
+ EXPECT_EQ(0, agent_->subscriptions_.size());
+ EXPECT_NO_THROW(agent_->subscribeConfig(service_pair));
+ EXPECT_EQ(1, agent_->subscriptions_.size());
+
+ /// Unsubscribe.
+ EXPECT_NO_THROW(agent_->subscriptions_.clear());
+}
+
+// Verifies that the update method works as expected: apply new YANG configuration
+// to the server. Note it is called by the subscription callback.
+TEST_F(NetconfAgentTest, update) {
+ // Initial YANG configuration.
+ const YRTree tree0 = YangRepr::buildTreeFromVector({
+ { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/id",
+ "1", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/subnet",
+ "10.0.0.0/24", SR_STRING_T, true },
+ { "/kea-dhcp4-server:config/subnet4[id='2']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='2']/id",
+ "2", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='2']/subnet",
+ "10.0.2.0/24", SR_STRING_T, true }
+ });
+ // Load initial YANG configuration.
+ ASSERT_NO_THROW_LOG(agent_->initSysrepo());
+ YangRepr repr(KEA_DHCP4_SERVER);
+ ASSERT_NO_THROW_LOG(repr.set(tree0, agent_->startup_sess_));
+ EXPECT_NO_THROW(agent_->startup_sess_->apply_changes());
+
+ // 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_->subscribeConfig(service_pair));
+ EXPECT_EQ(1, agent_->subscriptions_.size());
+
+ // Launch server.
+ thread_.reset(new thread([this]() { fakeServer(); signalStopped(); }));
+
+ // Wait until the server is listening.
+ waitReady();
+
+ // Change configuration (subnet #1 moved from 10.0.0.0/24 to 10.0.1/0/24).
+ const YRTree tree1 = YangRepr::buildTreeFromVector({
+ { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/id",
+ "1", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/subnet",
+ "10.0.1.0/24", SR_STRING_T, true }, // The change is here!
+ { "/kea-dhcp4-server:config/subnet4[id='2']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='2']/id",
+ "2", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/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_->apply_changes());
+
+ // Wait server to be stopped.
+ waitStopped();
+
+ // Check request.
+ ASSERT_EQ(1, requests_.size());
+ const string& request_str = requests_[0];
+ ConstElementPtr request;
+ ASSERT_NO_THROW_LOG(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_LOG(expected = Element::fromJSON(expected_str));
+
+ sortSubnets(expected);
+ sortSubnets(request);
+ EXPECT_EQ(prettyPrint(expected), prettyPrint(request));
+
+ // Check response.
+ ASSERT_EQ(1, responses_.size());
+ const string& response_str = responses_[0];
+ ConstElementPtr response;
+ ASSERT_NO_THROW_LOG(response = Element::fromJSON(response_str));
+ expected_str = "{\n"
+ "\"result\": 0\n"
+ "}";
+ ASSERT_NO_THROW_LOG(expected = Element::fromJSON(expected_str));
+ EXPECT_TRUE(expected->equals(*response));
+}
+
+// Verifies that the validate method works as expected: test new YANG 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 = YangRepr::buildTreeFromVector({
+ { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/id",
+ "1", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/subnet",
+ "10.0.0.0/24", SR_STRING_T, true },
+ { "/kea-dhcp4-server:config/subnet4[id='2']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='2']/id",
+ "2", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='2']/subnet",
+ "10.0.2.0/24", SR_STRING_T, true }
+ });
+ // Load initial YANG configuration.
+ ASSERT_NO_THROW_LOG(agent_->initSysrepo());
+ YangRepr repr(KEA_DHCP4_SERVER);
+ ASSERT_NO_THROW_LOG(repr.set(tree0, agent_->startup_sess_));
+ EXPECT_NO_THROW(agent_->startup_sess_->apply_changes());
+
+ // 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_->subscribeConfig(service_pair));
+ EXPECT_EQ(1, agent_->subscriptions_.size());
+
+ // Launch server twice.
+ thread_.reset(new thread([this]()
+ {
+ fakeServer();
+ fakeServer();
+ signalStopped();
+ }));
+
+ // Wait until the server is listening.
+ waitReady();
+
+ // Change configuration (subnet #1 moved from 10.0.0.0/24 to 10.0.1/0/24).
+ const YRTree tree1 = YangRepr::buildTreeFromVector({
+ { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/id",
+ "1", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/subnet",
+ "10.0.1.0/24", SR_STRING_T, true }, // The change is here!
+ { "/kea-dhcp4-server:config/subnet4[id='2']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='2']/id",
+ "2", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/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_->apply_changes());
+
+ // Wait servers to be stopped.
+ waitStopped();
+
+ // Check that the fake server received the first request.
+ ASSERT_LE(1, requests_.size());
+ string request_str = requests_[0];
+ ConstElementPtr request;
+ ASSERT_NO_THROW_LOG(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_LOG(expected = Element::fromJSON(expected_str));
+
+ sortSubnets(expected);
+ sortSubnets(request);
+ EXPECT_EQ(prettyPrint(expected), prettyPrint(request));
+
+ // Check that the fake server received the second request.
+ ASSERT_EQ(2, requests_.size());
+ request_str = requests_[1];
+ ASSERT_NO_THROW_LOG(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_LOG(expected = Element::fromJSON(expected_str));
+
+ sortSubnets(expected);
+ sortSubnets(request);
+ EXPECT_EQ(prettyPrint(expected), prettyPrint(request));
+
+ // Check responses.
+ ASSERT_EQ(2, responses_.size());
+ string response_str = responses_[0];
+ ConstElementPtr response;
+ ASSERT_NO_THROW_LOG(response = Element::fromJSON(response_str));
+ expected_str = "{\n"
+ "\"result\": 0\n"
+ "}";
+ ASSERT_NO_THROW_LOG(expected = Element::fromJSON(expected_str));
+ EXPECT_TRUE(expected->equals(*response));
+
+ response_str = responses_[1];
+ ASSERT_NO_THROW_LOG(response = Element::fromJSON(response_str));
+ expected_str = "{\n"
+ "\"result\": 0\n"
+ "}";
+ ASSERT_NO_THROW_LOG(expected = Element::fromJSON(expected_str));
+ EXPECT_TRUE(expected->equals(*response));
+}
+
+// Verifies what happens when the validate method returns an error.
+TEST_F(NetconfAgentTest, noValidate) {
+ // Initial YANG configuration.
+ const YRTree tree0 = YangRepr::buildTreeFromVector({
+ { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/id",
+ "1", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/subnet",
+ "10.0.0.0/24", SR_STRING_T, true }
+ });
+ // Load initial YANG configuration.
+ ASSERT_NO_THROW_LOG(agent_->initSysrepo());
+ YangRepr repr(KEA_DHCP4_SERVER);
+ ASSERT_NO_THROW_LOG(repr.set(tree0, agent_->startup_sess_));
+ EXPECT_NO_THROW(agent_->startup_sess_->apply_changes());
+
+ // 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_->subscribeConfig(service_pair));
+ EXPECT_EQ(1, agent_->subscriptions_.size());
+
+ // Change configuration (add invalid user context).
+ const YRTree tree1 = YangRepr::buildTreeFromVector({
+ { "/kea-dhcp4-server:config", "", SR_CONTAINER_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']", "", SR_LIST_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/id",
+ "1", SR_UINT32_T, false },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/subnet",
+ "10.0.0.0/24", SR_STRING_T, true },
+ { "/kea-dhcp4-server:config/subnet4[id='1']/user-context",
+ "BOGUS", SR_STRING_T, true }
+ });
+ try {
+ repr.set(tree1, agent_->running_sess_);
+ } catch (const sysrepo_exception& ex) {
+ EXPECT_EQ("User callback failed", string(ex.what()));
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "unexpected exception: " << ex.what();
+ } catch (...) {
+ ADD_FAILURE() << "unexpected exception";
+ }
+}
+
+} // namespace