Commit 3ff13c72 authored by Francis Dupont's avatar Francis Dupont

[65-libyang-adaptor] Imported adaptor code from kea-yang

parent a9e9e42d
......@@ -74,6 +74,9 @@ if test "$cross_compiling" = "yes"; then
fi
AM_CONDITIONAL([CROSS_COMPILING], [test "$cross_compiling" = "yes"])
# pkg-config can be required.
AC_PATH_PROG([PKG_CONFIG], [pkg-config])
# Enable low-performing debugging facilities? This option optionally
# enables some debugging aids that perform slowly and hence aren't built
# by default.
......@@ -817,7 +820,6 @@ AC_ARG_WITH([cql],
[cql_config="$withval"])
if test "${cql_config}" = "yes" ; then
AC_PATH_PROG([PKG_CONFIG], [pkg-config])
CQL_CONFIG="$PKG_CONFIG"
elif test "${cql_config}" != "no" ; then
CQL_CONFIG="${cql_config}"
......@@ -1597,6 +1599,8 @@ AC_CONFIG_FILES([Makefile
src/lib/util/threads/Makefile
src/lib/util/threads/tests/Makefile
src/lib/util/unittests/Makefile
src/lib/yang/Makefile
src/lib/yang/tests/Makefile
src/share/Makefile
src/share/database/Makefile
src/share/database/scripts/Makefile
......
......@@ -13,6 +13,10 @@ if HAVE_CQL
SUBDIRS += cql
endif
SUBDIRS += testutils hooks dhcp config stats asiodns dhcp_ddns eval \
dhcpsrv cfgrpt \
process http
SUBDIRS += testutils hooks dhcp config stats
if HAVE_SYSREPO
SUBDIRS += yang
endif
SUBDIRS += asiodns dhcp_ddns eval dhcpsrv cfgrpt process http
SUBDIRS = . tests
AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
AM_CPPFLAGS += $(BOOST_INCLUDES) $(SYSREPO_CPPFLAGS)
AM_CXXFLAGS = $(KEA_CXXFLAGS)
lib_LTLIBRARIES = libkea-yang.la
libkea_yang_la_SOURCES = adaptor.cc adaptor.h
libkea_yang_la_LIBADD = $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
libkea_yang_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la
libkea_yang_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la
libkea_yang_la_LIBADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la
libkea_yang_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la
libkea_yang_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
libkea_yang_la_LIBADD += $(LOG4CPLUS_LIBS) $(BOOST_LIBS) $(SYSREPO_LIBS)
libkea_yang_la_LDFLAGS = -no-undefined -version-info 0:0:0
# Specify the headers for copying into the installation directory tree.
libkea_yang_includedir = $(pkgincludedir)/yang
libkea_yang_include_HEADERS = \
adaptor.h
EXTRA_DIST = yang.dox
CLEANFILES = *.gcno *.gcda
// 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 <yang/adaptor.h>
#include <boost/foreach.hpp>
#include <iostream>
using namespace std;
using namespace isc::data;
namespace isc {
namespace yang {
Adaptor::Adaptor() {
}
Adaptor::~Adaptor() {
}
ConstElementPtr
Adaptor::getContext(ConstElementPtr parent)
{
ConstElementPtr context = parent->get("user-context");
ConstElementPtr comment = parent->get("comment");
if (!comment) {
return (context);
}
ElementPtr result;
if (context) {
result = copy(context);
} else {
result = Element::createMap();
}
result->set("comment", comment);
return (result);
}
void
Adaptor::fromParent(const string& name, ConstElementPtr parent,
ConstElementPtr list) {
ConstElementPtr param = parent->get(name);
if (!param) {
return;
}
BOOST_FOREACH(ElementPtr item, list->listValue()) {
// don't override?
if (item->contains(name)) {
continue;
}
item->set(name, param);
}
}
void
Adaptor::toParent(const string& name, ElementPtr parent,
ConstElementPtr list) {
ConstElementPtr param;
bool first = true;
BOOST_FOREACH(ElementPtr item, list->listValue()) {
if (first) {
first = false;
param = item->get(name);
} else if ((!param && item->contains(name)) ||
(param && !item->contains(name)) ||
(param && item->contains(name) &&
!param->equals(*item->get(name)))) {
isc_throw(BadValue,
"inconsistent value of " << name
<< " in " << list->str());
}
}
if (!first && param) {
BOOST_FOREACH(ElementPtr item, list->listValue()) {
if (param) {
item->remove(name);
}
}
parent->set(name, param);
}
}
namespace {
/// @brief Apply insert.
///
/// @param key The key of the modification.
/// @param value The value of the modification.
/// @param scope The place to apply the insert.
void apply_insert(ConstElementPtr key, ConstElementPtr value,
ElementPtr scope) {
if (scope->getType() == Element::map) {
if (!key || !value || (key->getType() != Element::string)) {
return;
}
string name = key->stringValue();
if (!name.empty() && !scope->contains(name)) {
scope->set(name, copy(value));
}
} else if (scope->getType() == Element::list) {
if (value) {
scope->add(copy(value));
}
}
}
/// @brief Apply replace.
///
/// For maps same than insert but the new value is set even if the key
/// already exists.
///
/// @param key The key of the modification.
/// @param value The value of the modification.
/// @param scope The place to apply the replace.
void apply_replace(ConstElementPtr key, ConstElementPtr value,
ElementPtr scope) {
if ((scope->getType() != Element::map) ||
!key || !value || (key->getType() != Element::string)) {
return;
}
string name = key->stringValue();
if (!name.empty()) {
scope->set(name, copy(value));
}
}
/// @brief Apply delete.
///
/// @param last The last item of the path.
/// @param scope The place to apply the delete.
void apply_delete(ConstElementPtr last, ElementPtr scope) {
if (scope->getType() == Element::map) {
if (!last || (last->getType() != Element::string)) {
return;
}
string name = last->stringValue();
if (!name.empty()) {
scope->remove(name);
}
} else if (scope->getType() == Element::list) {
if (!last) {
return;
} else if (last->getType() == Element::integer) {
int index = last->intValue();
if ((index >= 0) && (index < scope->size())) {
scope->remove(index);
}
} else if (last->getType() == Element::map) {
ConstElementPtr key = last->get("key");
ConstElementPtr value = last->get("value");
if (!key || !value || (key->getType() != Element::string)) {
return;
}
string name = key->stringValue();
if (name.empty()) {
return;
}
for (int i = 0; i < scope->size(); ++i) {
ConstElementPtr item = scope->get(i);
if (!item || (item->getType() != Element::map)) {
continue;
}
ConstElementPtr compare = item->get(name);
if (compare && value->equals(*compare)) {
scope->remove(i);
return;
}
}
}
}
}
/// @brief Apply action.
///
/// @param actions The action list.
/// @param scope The current scope.
/// @param next The index of the next action.
void apply_action(ConstElementPtr actions, ElementPtr scope, size_t next) {
if (next == actions->size()) {
return;
}
ConstElementPtr action = actions->get(next);
++next;
if (!action || (action->getType() != Element::map) ||
!action->contains("action")) {
apply_action(actions, scope, next);
return;
}
string name = action->get("action")->stringValue();
if (name == "insert") {
apply_insert(action->get("key"), action->get("value"), scope);
} else if (name == "replace") {
apply_replace(action->get("key"), action->get("value"), scope);
} else if (name == "delete") {
apply_delete(action->get("last"), scope);
}
apply_action(actions, scope, next);
}
/// @brief Modify down.
///
/// @param path The search list.
/// @param actions The action list.
/// @param scope The current scope.
/// @param next The index of the next item to use in the path.
void path_down(ConstElementPtr path, ConstElementPtr actions, ElementPtr scope,
size_t next) {
if (!scope) {
return;
}
if (next == path->size()) {
apply_action(actions, scope, 0);
return;
}
ConstElementPtr step = path->get(next);
++next;
if (scope->getType() == Element::map) {
if (!step || (step->getType() != Element::string)) {
return;
}
string name = step->stringValue();
if (name.empty() || !scope->contains(name)) {
return;
}
ElementPtr down = boost::const_pointer_cast<Element>(scope->get(name));
if (down) {
path_down(path, actions, down, next);
}
} else if (scope->getType() == Element::list) {
if (!step) {
return;
}
auto downs = scope->listValue();
if (step->getType() == Element::map) {
ConstElementPtr key = step->get("key");
ConstElementPtr value = step->get("value");
if (!key || !value || (key->getType() != Element::string)) {
return;
}
string name = key->stringValue();
if (name.empty()) {
return;
}
for (ElementPtr down : downs) {
if (!down || (down->getType() != Element::map)) {
continue;
}
ConstElementPtr compare = down->get(name);
if (compare && value->equals(*compare)) {
path_down(path, actions, down, next);
return;
}
}
} else if (step->getType() != Element::integer) {
return;
}
int index = step->intValue();
if (index == -1) {
for (ElementPtr down : downs) {
path_down(path, actions, down, next);
}
} else if ((index >= 0) && (index < scope->size())) {
path_down(path, actions, scope->getNonConst(index), next);
}
}
}
} // end of anonymous namespace
void
Adaptor::modify(ConstElementPtr path, ConstElementPtr actions,
ElementPtr config) {
path_down(path, actions, config, 0);
}
}; // end of namespace isc::yang
}; // end of namespace isc
// 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/.
#ifndef ISC_ADAPTOR_H
#define ISC_ADAPTOR_H 1
#include <cc/data.h>
namespace isc {
namespace yang {
/// @brief JSON adaptor between canonical Kea and Yang models.
///
/// An adaptor slightly modifies a JSON configuration between canonical Kea
/// what required or rendered by a Yang model, e.g. moving a parameter
/// to/from a parent.
/// The basic adaptor provides a set of tools.
class Adaptor {
public:
/// @brief Constructor.
Adaptor();
/// @brief Destructor.
virtual ~Adaptor();
/// @brief Get user context.
///
/// Get user-context and/or comment and return it with the comment
/// if exists moved inside the user-context (without checking if
/// there is already a comment as it should never be the case).
static isc::data::ConstElementPtr
getContext(isc::data::ConstElementPtr parent);
/// @brief From parent.
///
/// Move a parameter from the parent to each item in a list.
///
/// @param name The parameter name.
/// @param parent The parent element.
/// @param list The children list.
static void fromParent(const std::string& name,
isc::data::ConstElementPtr parent,
isc::data::ConstElementPtr list);
/// @brief To parent.
///
/// Move a parameter from children to the parent.
///
/// @param name The parameter name.
/// @param parent The parent element.
/// @param list The children list.
static void toParent(const std::string& name,
isc::data::ElementPtr parent,
isc::data::ConstElementPtr list);
/// @brief Modify.
///
/// Smart merging tool, e.g. completing a from yang configuration.
///
/// A modification is a path and actions:
/// - path item can be:
/// * a string: current scope is a map, go down following the string
/// as a key.
/// * a number: current scope is a list, go down the number as an index.
/// * special value -1: current scope is a list, apply to all items.
/// * map { "<key>": <value> }: current scope is a list, go down to
/// the item using the key / value pair.
/// - an action can be: insert, replace or delete.
///
/// @param path The search list to follow down to the place to
/// apply the action list.
/// @param actions The action list
/// @param config The configuration (JSON map) to modify.
static void modify(isc::data::ConstElementPtr path,
isc::data::ConstElementPtr actions,
isc::data::ElementPtr config);
};
}; // end of namespace isc::yang
}; // end of namespace isc
#endif // ISC_ADAPTOR_H
AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
AM_CPPFLAGS += $(BOOST_INCLUDES) $(SYSREPO_CPPFLAGS)
AM_CPPFLAGS += -DCFG_EXAMPLES=\"$(abs_top_srcdir)/doc/examples\"
AM_CXXFLAGS = $(KEA_CXXFLAGS)
if USE_STATIC_LINK
AM_LDFLAGS = -static
endif
CLEANFILES = *.gcno *.gcda
TESTS_ENVIRONMENT = \
$(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
TESTS =
if HAVE_GTEST
TESTS += run_unittests
run_unittests_SOURCES = adaptor_unittests.cc
run_unittests_SOURCES += run_unittests.cc
run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
run_unittests_LDADD = $(top_builddir)/src/lib/yang/libkea-yang.la
run_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la
run_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la
run_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
run_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
run_unittests_LDADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la
run_unittests_LDADD += $(top_builddir)/src/lib/util/unittests/libutil_unittests.la
run_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la
run_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
run_unittests_LDADD += $(LOG4CPLUS_LIBS) $(BOOST_LIBS)
run_unittests_LDADD += $(SYSREPO_LIBS) $(GTEST_LDADD)
endif
noinst_PROGRAMS = $(TESTS)
// 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 <yang/adaptor.h>
#include <boost/scoped_ptr.hpp>
#include <gtest/gtest.h>
using namespace std;
using namespace isc;
using namespace isc::data;
using namespace isc::yang;
namespace {
// Test get context.
TEST(AdaptorTest, getContext) {
// Empty.
string config = "{\n"
"}\n";
ConstElementPtr json = Element::fromJSON(config);
ConstElementPtr context;
ASSERT_NO_THROW(context = Adaptor::getContext(json));
EXPECT_FALSE(context);
// No relevant.
config = "{\n"
" \"foo\": 1\n"
"}\n";
json = Element::fromJSON(config);
ASSERT_NO_THROW(context = Adaptor::getContext(json));
EXPECT_FALSE(context);
// User context.
config = "{\n"
" \"foo\": 1,\n"
" \"user-context\": { \"bar\": 2 }\n"
"}\n";
json = Element::fromJSON(config);
ASSERT_NO_THROW(context = Adaptor::getContext(json));
ASSERT_TRUE(context);
EXPECT_EQ("{ \"bar\": 2 }", context->str());
// Comment.
config = "{\n"
" \"foo\": 1,\n"
" \"comment\": \"a comment\"\n"
"}\n";
json = Element::fromJSON(config);
ASSERT_NO_THROW(context = Adaptor::getContext(json));
ASSERT_TRUE(context);
EXPECT_EQ("{ \"comment\": \"a comment\" }", context->str());
// User context and comment.
config = "{\n"
" \"foo\": 1,\n"
" \"user-context\": { \"bar\": 2 },\n"
" \"comment\": \"a comment\"\n"
"}\n";
json = Element::fromJSON(config);
ASSERT_NO_THROW(context = Adaptor::getContext(json));
ASSERT_TRUE(context);
EXPECT_EQ("{ \"bar\": 2, \"comment\": \"a comment\" }", context->str());
// User context with conflicting comment and comment.
config = "{\n"
" \"foo\": 1,\n"
" \"user-context\": {\n"
" \"bar\": 2,\n"
" \"comment\": \"conflicting\"\n"
" },\n"
" \"comment\": \"a comment\"\n"
"}\n";
json = Element::fromJSON(config);
ASSERT_NO_THROW(context = Adaptor::getContext(json));
ASSERT_TRUE(context);
EXPECT_EQ("{ \"bar\": 2, \"comment\": \"a comment\" }", context->str());
}
// Test from parent.
TEST(AdaptorTest, fromParent) {
string config = "{\n"
" \"param1\": 123,\n"
" \"param2\": \"foo\",\n"
" \"list\": [\n"
" {\n"
" \"param1\": 234\n"
" },{\n"
" \"another\": \"entry\"\n"
" }\n"
" ]\n"
"}\n";
ConstElementPtr json = Element::fromJSON(config);
EXPECT_NO_THROW(Adaptor::fromParent("param1", json, json->get("list")));
EXPECT_NO_THROW(Adaptor::fromParent("param2", json, json->get("list")));
EXPECT_NO_THROW(Adaptor::fromParent("param3", json, json->get("list")));
string expected = "{\n"
" \"param1\": 123,\n"
" \"param2\": \"foo\",\n"
" \"list\": [\n"
" {\n"
" \"param1\": 234,\n"
" \"param2\": \"foo\"\n"
" },{\n"
" \"another\": \"entry\",\n"
" \"param1\": 123,\n"
" \"param2\": \"foo\"\n"
" }\n"
" ]\n"
"}\n";
EXPECT_TRUE(json->equals(*Element::fromJSON(expected)));
}
// Test to parent.
TEST(AdaptorTest, toParent) {
string config = "{\n"
" \"list\": [\n"
" {\n"
" \"param2\": \"foo\",\n"
" \"param3\": 234,\n"
" \"param4\": true\n"
" },{\n"
" \"another\": \"entry\",\n"
" \"param2\": \"foo\",\n"
" \"param3\": 123,\n"
" \"param5\": false\n"
" }\n"
" ]\n"
"}\n";
ElementPtr json = Element::fromJSON(config);
EXPECT_NO_THROW(Adaptor::toParent("param1", json, json->get("list")));
EXPECT_TRUE(json->equals(*Element::fromJSON(config)));
string expected = "{\n"
" \"param2\": \"foo\",\n"
" \"list\": [\n"
" {\n"
" \"param3\": 234,\n"
" \"param4\": true\n"
" },{\n"
" \"another\": \"entry\",\n"
" \"param3\": 123,\n"
" \"param5\": false\n"
" }\n"
" ]\n"
"}\n";
EXPECT_NO_THROW(Adaptor::toParent("param2",json, json->get("list")));
EXPECT_TRUE(json->equals(*Element::fromJSON(expected)));
// param1 has different value so it should throw.
EXPECT_THROW(Adaptor::toParent("param3",json, json->get("list")),
BadValue);
EXPECT_THROW(Adaptor::toParent("param4",json, json->get("list")),
BadValue);
EXPECT_THROW(Adaptor::toParent("param5",json, json->get("list")),
BadValue);
// And not modify the value.
EXPECT_TRUE(json->equals(*Element::fromJSON(expected)));
}
// Test for modify (maps & insert).
TEST(AdaptorTest, modifyMapInsert) {
string config = "{\n"
" \"foo\": {\n"
" \"bar\": {\n"
"}}}\n";
ElementPtr json;
ASSERT_NO_THROW(json = Element::fromJSON(config));
string spath = "[ \"foo\", \"bar\" ]";
ConstElementPtr path;
ASSERT_NO_THROW(path = Element::fromJSON(spath));
string sactions = "[\n"
"{\n"
" \"action\": \"insert\",\n"
" \"key\": \"test\",\n"
" \"value\": 1234\n"
"}]\n";
ConstElementPtr actions;
ASSERT_NO_THROW(actions = Element::fromJSON(sactions));
string result = "{\n"
" \"foo\": {\n"
" \"bar\": {\n"
" \"test\": 1234\n"
"}}}\n";
ConstElementPtr expected;
ASSERT_NO_THROW(expected = Element::fromJSON(result));
ASSERT_NO_THROW(Adaptor::modify(path, actions, json));
EXPECT_TRUE(expected->equals(*json));
}
// Test for modify (maps & replace).
TEST(AdaptorTest, modifyMapReplace) {
string config = "{\n"
" \"foo\": {\n"
" \"bar\": {\n"
" \"test1\": 1234,\n"