From 6918234e0ad35d07c4a7395a7b51d0d6a0538380 Mon Sep 17 00:00:00 2001 From: Francis Dupont Date: Tue, 25 Sep 2018 19:17:03 +0200 Subject: [PATCH] [65-libyang-host] Brought host code from kea-yang --- src/lib/yang/Makefile.am | 2 + src/lib/yang/tests/Makefile.am | 1 + .../yang/tests/translator_host_unittests.cc | 181 ++++++++++++ src/lib/yang/translator_host.cc | 273 ++++++++++++++++++ src/lib/yang/translator_host.h | 198 +++++++++++++ 5 files changed, 655 insertions(+) create mode 100644 src/lib/yang/tests/translator_host_unittests.cc create mode 100644 src/lib/yang/translator_host.cc create mode 100644 src/lib/yang/translator_host.h diff --git a/src/lib/yang/Makefile.am b/src/lib/yang/Makefile.am index 4761a7127..e22d38855 100644 --- a/src/lib/yang/Makefile.am +++ b/src/lib/yang/Makefile.am @@ -10,6 +10,7 @@ libkea_yang_la_SOURCES += sysrepo_error.h libkea_yang_la_SOURCES += translator.cc translator.h libkea_yang_la_SOURCES += translator_option_data.cc libkea_yang_la_SOURCES += translator_option_data.h +libkea_yang_la_SOURCES += translator_host.cc translator_host.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 @@ -27,6 +28,7 @@ libkea_yang_include_HEADERS = \ adaptor.h \ sysrepo_error.h \ translator.h \ + translator_host.h \ translator_option_data.h EXTRA_DIST = yang.dox diff --git a/src/lib/yang/tests/Makefile.am b/src/lib/yang/tests/Makefile.am index 5e593f1d1..7c3e5cd1c 100644 --- a/src/lib/yang/tests/Makefile.am +++ b/src/lib/yang/tests/Makefile.am @@ -21,6 +21,7 @@ run_unittests_SOURCES = adaptor_unittests.cc run_unittests_SOURCES += sysrepo_setup.h run_unittests_SOURCES += translator_unittests.cc run_unittests_SOURCES += translator_option_data_unittests.cc +run_unittests_SOURCES += translator_host_unittests.cc run_unittests_SOURCES += run_unittests.cc run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS) diff --git a/src/lib/yang/tests/translator_host_unittests.cc b/src/lib/yang/tests/translator_host_unittests.cc new file mode 100644 index 000000000..5adce13a1 --- /dev/null +++ b/src/lib/yang/tests/translator_host_unittests.cc @@ -0,0 +1,181 @@ +// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include +#include + +#include +#include + +using namespace std; +using namespace isc; +using namespace isc::data; +using namespace isc::yang; +using namespace isc::yang::test; + +namespace { + +/// @brief Translator name. +extern char const host_reservations[] = "host reservations"; + +/// @brief Test fixture class for @ref TranslatorHosts. +class TranslatorHostsTest : + public GenericTranslatorTest { +public: + + /// Constructor. + TranslatorHostsTest() { } + + /// Destructor (does nothing). + virtual ~TranslatorHostsTest() { } +}; + +// This test verifies that an empty host reservation list can be properly +// translated from YANG to JSON. +TEST_F(TranslatorHostsTest, getEmpty) { + useModel("kea-dhcp6-server"); + + // Get the host reservation list and checks it is empty. + const string& xpath = + "/kea-dhcp6-server:config/subnet6/subnet6[id='111']/reservations"; + ConstElementPtr hosts; + EXPECT_NO_THROW(hosts = t_obj_->getHosts(xpath)); + ASSERT_TRUE(hosts); + ASSERT_EQ(Element::list, hosts->getType()); + EXPECT_EQ(0, hosts->size()); +} + +// This test verifies that one host reservation can be properly +// translated from YANG to JSON. +TEST_F(TranslatorHostsTest, get) { + useModel("kea-dhcp6-server"); + + // Create the subnet 2001:db8::/48 #111. + const string& subnet = + "/kea-dhcp6-server:config/subnet6/subnet6[id='111']"; + S_Val v_subnet(new Val("2001:db8::/48", SR_STRING_T)); + const string& xsubnet = subnet + "/subnet"; + EXPECT_NO_THROW(sess_->set_item(xsubnet.c_str(), v_subnet)); + + // Create the host reservation for 2001:db8::1. + const string& xpath = subnet + "/reservations"; + ostringstream shost; + shost << xpath + "/host[identifier-type='hw-address']" + << "[identifier='00:01:02:03:04:05']"; + const string& xaddr = shost.str() + "/ip-addresses"; + S_Val s_addr(new Val("2001:db8::1")); + EXPECT_NO_THROW(sess_->set_item(xaddr.c_str(), s_addr)); + + // Get the host. + ConstElementPtr host; + EXPECT_NO_THROW(host = t_obj_->getHost(shost.str())); + ASSERT_TRUE(host); + ElementPtr expected = Element::createMap(); + ElementPtr addresses = Element::createList(); + addresses->add(Element::create(string("2001:db8::1"))); + expected->set("hw-address", Element::create(string("00:01:02:03:04:05"))); + expected->set("ip-addresses", addresses); + EXPECT_TRUE(expected->equals(*host)); + + // Get the host reservation list and checks the host reservation is in it. + ConstElementPtr hosts; + EXPECT_NO_THROW(hosts = t_obj_->getHosts(xpath)); + ASSERT_TRUE(hosts); + ASSERT_EQ(Element::list, hosts->getType()); + ASSERT_EQ(1, hosts->size()); + EXPECT_TRUE(host->equals(*hosts->get(0))); +} + +// This test verifies that an empty host reservation list can be properly +// translated from JSON to YANG. +TEST_F(TranslatorHostsTest, setEmpty) { + useModel("kea-dhcp6-server"); + + // Create the subnet 2001:db8::/48 #111. + const string& subnet = + "/kea-dhcp6-server:config/subnet6/subnet6[id='111']"; + S_Val v_subnet(new Val("2001:db8::/48", SR_STRING_T)); + const string& xsubnet = subnet + "/subnet"; + EXPECT_NO_THROW(sess_->set_item(xsubnet.c_str(), v_subnet)); + + // Set empty list. + const string& xpath = subnet + "/reservations"; + ConstElementPtr hosts = Element::createList(); + EXPECT_NO_THROW(t_obj_->setHosts(xpath, hosts)); + + // Get it back. + hosts.reset(); + EXPECT_NO_THROW(hosts = t_obj_->getHosts(xpath)); + ASSERT_TRUE(hosts); + ASSERT_EQ(Element::list, hosts->getType()); + EXPECT_EQ(0, hosts->size()); +} + +// This test verifies that one host reservation can be properly +// translated from JSON to YANG. +TEST_F(TranslatorHostsTest, set) { + useModel("kea-dhcp4-server"); + + // Create the subnet 10.0.0.0/14 #111. + const string& subnet = + "/kea-dhcp4-server:config/subnet4/subnet4[id='111']"; + S_Val v_subnet(new Val("10.0.0.0/24", SR_STRING_T)); + const string& xsubnet = subnet + "/subnet"; + EXPECT_NO_THROW(sess_->set_item(xsubnet.c_str(), v_subnet)); + + // Set one host. + const string& xpath = subnet + "/reservations"; + ElementPtr hosts = Element::createList(); + ElementPtr host = Element::createMap(); + host->set("flex-id", Element::create(string("00:ff"))); + host->set("ip-address", Element::create(string("10.0.0.1"))); + host->set("hostname", Element::create(string("foo"))); + hosts->add(host); + EXPECT_NO_THROW(t_obj_->setHosts(xpath, hosts)); + + // Get it back. + hosts.reset(); + EXPECT_NO_THROW(hosts = t_obj_->getHosts(xpath)); + ASSERT_TRUE(hosts); + ASSERT_EQ(Element::list, hosts->getType()); + ASSERT_EQ(1, hosts->size()); + EXPECT_TRUE(host->equals(*hosts->get(0))); + + // Check the tree representation. + S_Tree tree; + EXPECT_NO_THROW(tree = sess_->get_subtree("/kea-dhcp4-server:config")); + ASSERT_TRUE(tree); + string expected = + "kea-dhcp4-server:config (container)\n" + " |\n" + " -- subnet4 (container)\n" + " |\n" + " -- subnet4 (list instance)\n" + " |\n" + " -- id = 111\n" + " |\n" + " -- subnet = 10.0.0.0/24\n" + " |\n" + " -- reservations (container)\n" + " |\n" + " -- host (list instance)\n" + " |\n" + " -- identifier-type = flex-id\n" + " |\n" + " -- identifier = 00:ff\n" + " |\n" + " -- hostname = foo\n" + " |\n" + " -- ip-address = 10.0.0.1\n"; + EXPECT_EQ(expected, tree->to_string(100)); + + // Check it validates. + EXPECT_NO_THROW(sess_->validate()); +} + +}; // end of anonymous namespace diff --git a/src/lib/yang/translator_host.cc b/src/lib/yang/translator_host.cc new file mode 100644 index 000000000..e13366be7 --- /dev/null +++ b/src/lib/yang/translator_host.cc @@ -0,0 +1,273 @@ +// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include + +using namespace std; +using namespace isc::data; + +namespace isc { +namespace yang { + +TranslatorHost::TranslatorHost(S_Session session, const string& model) + : TranslatorBasic(session), + TranslatorOptionData(session, model), + TranslatorOptionDataList(session, model), + model_(model) { +} + +TranslatorHost::~TranslatorHost() { +} + +ElementPtr + TranslatorHost::getHost(const string& xpath) { + try { + if ((model_ == "kea-dhcp4-server") || + (model_ == "kea-dhcp6-server")) { + return (getHostKea(xpath)); + } + } catch (const sysrepo_exception& ex) { + isc_throw(SysrepoError, + "sysrepo error getting host reservation at '" << xpath + << "': " << ex.what()); + } + isc_throw(NotImplemented, + "getHost not implemented for the model: " << model_); +} + +ElementPtr +TranslatorHost::getHostKea(const string& xpath) { + ConstElementPtr id_type = getItem(xpath + "/identifier-type"); + ConstElementPtr id = getItem(xpath + "/identifier"); + if (!id_type || !id) { + isc_throw(Unexpected, "getHostKea requires both identifier and " + "identifier-type"); + } + ElementPtr result = Element::createMap(); + result->set(id_type->stringValue(), id); + ConstElementPtr hostname = getItem(xpath + "/hostname"); + if (hostname) { + result->set("hostname", hostname); + } + if (model_ == "kea-dhcp4-server") { + ConstElementPtr address = getItem(xpath + "/ip-address"); + if (address) { + result->set("ip-address", address); + } + } else { + ConstElementPtr addresses = getItems(xpath + "/ip-addresses"); + if (addresses && (addresses->size() > 0)) { + result->set("ip-addresses", addresses); + } + ConstElementPtr prefixes = getItems(xpath + "/prefixes"); + if (prefixes && (prefixes->size() > 0)) { + result->set("prefixes", prefixes); + } + } + ConstElementPtr options = getOptionDataList(xpath + "/option-data-list"); + if (options && (options->size() > 0)) { + result->set("option-data", options); + } + ConstElementPtr classes = getItems(xpath + "/client-classes"); + if (classes) { + result->set("client-classes", classes); + } + if (model_ == "kea-dhcp4-server") { + ConstElementPtr next = getItem(xpath + "/next-server"); + if (next) { + result->set("next-server", next); + } + ConstElementPtr hostname = getItem(xpath + "/server-hostname"); + if (hostname) { + result->set("server-hostname", hostname); + } + ConstElementPtr boot = getItem(xpath + "/boot-file-name"); + if (boot) { + result->set("boot-file-name", boot); + } + } + ConstElementPtr context = getItem(xpath + "/user-context"); + if (context) { + result->set("user-context", Element::fromJSON(context->stringValue())); + } + return (result); +} + +void +TranslatorHost::setHost(const string& xpath, ConstElementPtr elem) { + try { + if ((model_ == "kea-dhcp4-server") || + (model_ == "kea-dhcp6-server")) { + setHostKea(xpath, elem); + } else { + isc_throw(NotImplemented, + "setHost not implemented for the model: " << model_); + } + } catch (const sysrepo_exception& ex) { + isc_throw(SysrepoError, + "sysrepo error setting host reservation '" << elem->str() + << "' at '" << xpath << "': " << ex.what()); + } +} + +void +TranslatorHost::setHostKea(const string& xpath, ConstElementPtr elem) { + ConstElementPtr hostname = elem->get("hostname"); + // Skip identifier and identifier type as they are keys. + if (hostname) { + setItem(xpath + "/hostname", hostname, SR_STRING_T); + } + if (model_ == "kea-dhcp4-server") { + ConstElementPtr address = elem->get("ip-address"); + if (address) { + setItem(xpath + "/ip-address", address, SR_STRING_T); + } + } else { + ConstElementPtr addresses = elem->get("ip-addresses"); + if (addresses && (addresses->size() > 0)) { + for (ConstElementPtr address : addresses->listValue()) { + setItem(xpath + "/ip-addresses", address, SR_STRING_T); + } + } + ConstElementPtr prefixes = elem->get("prefixes"); + if (prefixes && (prefixes->size() > 0)) { + for (ConstElementPtr prefix : prefixes->listValue()) { + setItem(xpath + "/prefixes", prefix, SR_STRING_T); + } + } + } + ConstElementPtr options = elem->get("option-data"); + if (options && (options->size() > 0)) { + setOptionDataList(xpath + "/option-data-list", options); + } + ConstElementPtr classes = elem->get("client-classes"); + if (classes && (classes->size() > 0)) { + for (ConstElementPtr cclass : classes->listValue()) { + setItem(xpath + "/client-classes", cclass, SR_STRING_T); + } + } + if (model_ == "kea-dhcp4-server") { + ConstElementPtr next = elem->get("next-server"); + if (next) { + setItem(xpath + "/next-server", next, SR_STRING_T); + } + ConstElementPtr hostname = elem->get("server-hostname"); + if (hostname) { + setItem(xpath + "/server-hostname", hostname, SR_STRING_T); + } + ConstElementPtr boot = elem->get("boot-file-name"); + if (boot) { + setItem(xpath + "/boot-file-name", boot, SR_STRING_T); + } + } + ConstElementPtr context = Adaptor::getContext(elem); + if (context) { + setItem(xpath + "/user-context", Element::create(context->str()), + SR_STRING_T); + } +} + +TranslatorHosts::TranslatorHosts(S_Session session, const string& model) + : TranslatorBasic(session), + TranslatorOptionData(session, model), + TranslatorOptionDataList(session, model), + TranslatorHost(session, model), + model_(model) { +} + +TranslatorHosts::~TranslatorHosts() { +} + +ElementPtr +TranslatorHosts::getHosts(const string& xpath) { + try { + ElementPtr result = Element::createList(); + S_Iter_Value iter = getIter(xpath + "/*"); + if (!iter) { + // Can't happen. + isc_throw(Unexpected, "getHosts can't get iterator: " << xpath); + } + for (;;) { + const string& host = getNext(iter); + if (host.empty()) { + break; + } + result->add(getHost(host)); + } + return (result); + } catch (const sysrepo_exception& ex) { + isc_throw(SysrepoError, + "sysrepo error getting host reservations at '" << xpath + << "': " << ex.what()); + } +} + +void +TranslatorHosts::setHosts(const string& xpath, ConstElementPtr elem) { + try { + if ((model_ == "kea-dhcp4-server") || + (model_ == "kea-dhcp6-server")) { + setHostsKea(xpath, elem); + } else { + isc_throw(NotImplemented, + "setHosts not implemented for the model: " << model_); + } + } catch (const sysrepo_exception& ex) { + isc_throw(SysrepoError, + "sysrepo error setting host reservations '" << elem->str() + << "' at '" << xpath << "': " << ex.what()); + } +} + +void +TranslatorHosts::setHostsKea(const string& xpath, ConstElementPtr elem) { + for (size_t i = 0; i < elem->size(); ++i) { + string id_type = "unknown"; + ConstElementPtr host = elem->get(i); + ConstElementPtr id = host->get("hw-address"); + if (id) { + id_type = "hw-address"; + goto found; + } + id = host->get("duid"); + if (id) { + id_type = "duid"; + goto found; + } + if (model_ == "kea-dhcp4-server") { + id = host->get("circuit-id"); + if (id) { + id_type = "circuit-id"; + goto found; + } + id = host->get("client-id"); + if (id) { + id_type = "client-id"; + goto found; + } + } + id = host->get("flex-id"); + if (id) { + id_type = "flex-id"; + goto found; + } + + found: + if (id_type == "unknown") { + isc_throw(BadValue, "getHosts: can't find the identifier type in " + << host->str()); + } + ostringstream key; + key << xpath << "/host[identifier-type='" << id_type + << "'][identifier='" << id->stringValue() << "']"; + setHost(key.str(), host); + } +} + +}; // end of namespace isc::yang +}; // end of namespace isc diff --git a/src/lib/yang/translator_host.h b/src/lib/yang/translator_host.h new file mode 100644 index 000000000..baf85d4f6 --- /dev/null +++ b/src/lib/yang/translator_host.h @@ -0,0 +1,198 @@ +// 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_TRANSLATOR_HOST_H +#define ISC_TRANSLATOR_HOST_H 1 + +#include +#include + +namespace isc { +namespace yang { + +// Host reservation translation between YANG and JSON +// +// JSON syntax for kea-dhcp4 is: +// @code +// { +// "hw-address": , +// "duid": , +// "circuit-id": , +// "client-id": , +// "flex-id": , +// "ip-address": , +// "hostname": , +// "next-server": "", +// "server-hostname": "", +// "boot-file-name": "", +// "client-classes": "", +// "option-data": [ ], +// "user-context": { }, +// "comment": "" +// } +// @endcode +// +// JSON syntax for kea-dhcp6 is: +// @code +// { +// "hw-address": , +// "duid": , +// "flex-id": , +// "ip-addresses": , +// "prefixes": , +// "hostname": , +// "client-classes": "", +// "option-data": [ ], +// "user-context": { }, +// "comment": "" +// } +// @endcode +// +// YANG syntax for kea-dhcp[46] is with identifier-type and identifier +// as the list keys: +// @code +// +--rw identifier-type enumeration +// +--rw identifier string +// +--rw hostname? string +// +--rw option-data-list option-data* +// +--rw client-classes* string +// +--rw user-context? string +// (DHCPv4 only) +// +--rw ip-address? inet:ipv4-address +// +--rw next-server? inet:ipv4-address +// +--rw server-hostname? string +// +--rw boot-file-name? string +// (DHCPv6 only) +// +--rw ip-addresses* inet:ipv6-address +// +--rw prefixes* inet:ipv6-prefix +// @endcode +// +// An example in JSON and YANG formats: +// @code +// [ +// { +// "flex-id": "00:ff", +// "ip-address": "10.0.0.1", +// "hostname": "foo" +// } +// ] +// @endcode +// @code +// /kea-dhcp4-server:config (container) +// /kea-dhcp4-server:config/subnet4 (container) +// /kea-dhcp4-server:config/subnet4/subnet4[id='111'] (list instance) +// /kea-dhcp4-server:config/subnet4/subnet4[id='111']/id = 111 +// /kea-dhcp4-server:config/subnet4/subnet4[id='111']/subnet = 10.0.0.0/24 +// /kea-dhcp4-server:config/subnet4/subnet4[id='111']/ +// reservations (container) +// /kea-dhcp4-server:config/subnet4/subnet4[id='111']/reservations/ +// host[identifier-type='flex-id'][identifier='00:ff'] (list instance) +// /kea-dhcp4-server:config/subnet4/subnet4[id='111']/reservations/ +// host[identifier-type='flex-id'][identifier='00:ff']/ +// identifier-type = flex-id +// /kea-dhcp4-server:config/subnet4/subnet4[id='111']/reservations/ +// host[identifier-type='flex-id'][identifier='00:ff']/ +// identifier = 00:ff +// /kea-dhcp4-server:config/subnet4/subnet4[id='111']/reservations/ +// host[identifier-type='flex-id'][identifier='00:ff']/ +// hostname = foo +// /kea-dhcp4-server:config/subnet4/subnet4[id='111']/reservations/ +// host[identifier-type='flex-id'][identifier='00:ff']/ +// ip-address = 10.0.0.1 +// @endcode + +// @brief A translator class for converting a host reservation between +// YANG and JSON. +// +// Currently supports on kea-dhcp[46]-server, not yet ietf-dhcpv6-server. +class TranslatorHost : virtual public TranslatorOptionDataList { +public: + + /// @brief Constructor. + /// + /// @param session Sysrepo session. + /// @param model Model name. + TranslatorHost(S_Session session, const std::string& model); + + /// @brief Destructor. + virtual ~TranslatorHost(); + + /// @brief Get and translate a host reservation from YANG to JSON. + /// + /// @param xpath The xpath of the host reservation. + /// @return JSON representation of the host reservation. + /// @throw SysrepoError when sysrepo raises an error. + isc::data::ElementPtr getHost(const std::string& xpath); + + /// @brief Translate and set host reservation from JSON to YANG. + /// + /// @param xpath The xpath of the host reservation. + /// @param elem The JSON element. + void setHost(const std::string& xpath, isc::data::ConstElementPtr elem); + +protected: + /// @brief getHost for kea-dhcp[46]. + /// + /// @param xpath The xpath of the host reservation. + /// @return JSON representation of the host reservation. + isc::data::ElementPtr getHostKea(const std::string& xpath); + + /// @brief setHost for kea-dhcp[46]. + /// + /// @param xpath The xpath of the host reservation. + /// @param elem The JSON element. + void setHostKea(const std::string& xpath, isc::data::ConstElementPtr elem); + + /// @brief The model. + std::string model_; +}; + +// @brief A translator class for converting host reservations between +// YANG and JSON. +// +// Currently supports on kea-dhcp[46]-server, not yet ietf-dhcpv6-server. +// +class TranslatorHosts : virtual public TranslatorHost { +public: + + /// @brief Constructor. + /// + /// @param session Sysrepo session. + /// @param model Model name. + TranslatorHosts(S_Session session, const std::string& model); + + /// @brief Destructor. + virtual ~TranslatorHosts(); + + /// @brief Get and translate host reservations from YANG to JSON. + /// + /// @param xpath The xpath of the host reservation list. + /// @throw SysrepoError when sysrepo raises an error. + isc::data::ElementPtr getHosts(const std::string& xpath); + + /// @brief Translate and set (address) host reservations from JSON to YANG. + /// + /// @param xpath The xpath of the host reservation list. + /// @param elem The JSON element. + void setHosts(const std::string& xpath, isc::data::ConstElementPtr elem); + +protected: + /// @brief setHosts for kea-dhcp[46]. + /// + /// @param xpath The xpath of the host reservation list. + /// @param elem The JSON element. + /// @throw BadValue on host reservation without known identifier type. + void setHostsKea(const std::string& xpath, + isc::data::ConstElementPtr elem); + + /// @brief The model. + std::string model_; +}; + +}; // end of namespace isc::yang +}; // end of namespace isc + +#endif // ISC_TRANSLATOR_HOST_H -- GitLab