]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[65-libyang-adaptors] Added high level adaptors from lib-yang
authorFrancis Dupont <fdupont@isc.org>
Sun, 7 Oct 2018 21:56:59 +0000 (23:56 +0200)
committerFrancis Dupont <fdupont@isc.org>
Fri, 19 Oct 2018 14:05:30 +0000 (10:05 -0400)
14 files changed:
src/lib/yang/Makefile.am
src/lib/yang/adaptor_host.cc [new file with mode: 0644]
src/lib/yang/adaptor_host.h [new file with mode: 0644]
src/lib/yang/adaptor_option.cc [new file with mode: 0644]
src/lib/yang/adaptor_option.h [new file with mode: 0644]
src/lib/yang/adaptor_pool.cc [new file with mode: 0644]
src/lib/yang/adaptor_pool.h [new file with mode: 0644]
src/lib/yang/adaptor_subnet.cc [new file with mode: 0644]
src/lib/yang/adaptor_subnet.h [new file with mode: 0644]
src/lib/yang/tests/Makefile.am
src/lib/yang/tests/adaptor_host_unittests.cc [new file with mode: 0644]
src/lib/yang/tests/adaptor_option_unittests.cc [new file with mode: 0644]
src/lib/yang/tests/adaptor_pool_unittests.cc [new file with mode: 0644]
src/lib/yang/tests/adaptor_subnet_unittests.cc [new file with mode: 0644]

index a0f0de8dbe2c791373b0402c000e786726a7f846..bfba24e36af2622bed79096a3c843bfac8337cdb 100644 (file)
@@ -6,6 +6,10 @@ AM_CXXFLAGS = $(KEA_CXXFLAGS)
 
 lib_LTLIBRARIES = libkea-yang.la
 libkea_yang_la_SOURCES  = adaptor.cc adaptor.h
+libkea_yang_la_SOURCES += adaptor_host.cc adaptor_host.h
+libkea_yang_la_SOURCES += adaptor_pool.cc adaptor_pool.h
+libkea_yang_la_SOURCES += adaptor_option.cc adaptor_option.h
+libkea_yang_la_SOURCES += adaptor_subnet.cc adaptor_subnet.h
 libkea_yang_la_SOURCES += sysrepo_error.h
 libkea_yang_la_SOURCES += translator.cc translator.h
 libkea_yang_la_SOURCES += translator_control_socket.cc
@@ -36,6 +40,10 @@ libkea_yang_la_LDFLAGS = -no-undefined -version-info 0:0:0
 libkea_yang_includedir = $(pkgincludedir)/yang
 libkea_yang_include_HEADERS = \
        adaptor.h \
+       adaptor_host.h \
+       adaptor_option.h \
+       adaptor_pool.h \
+       adaptor_subnet.h \
        sysrepo_error.h \
        translator.h \
        translator_control_socket.h \
diff --git a/src/lib/yang/adaptor_host.cc b/src/lib/yang/adaptor_host.cc
new file mode 100644 (file)
index 0000000..0a7ed9e
--- /dev/null
@@ -0,0 +1,65 @@
+// 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 <util/encode/hex.h>
+#include <util/strutil.h>
+#include <yang/adaptor_host.h>
+#include <iomanip>
+#include <sstream>
+
+using namespace std;
+using namespace isc::data;
+using namespace isc::util;
+
+namespace isc {
+namespace yang {
+
+const string
+AdaptorHost::STD_CHARACTERS =
+    "0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-.@_";
+
+AdaptorHost::AdaptorHost() {
+}
+
+AdaptorHost::~AdaptorHost() {
+}
+
+void
+AdaptorHost::quoteIdentifier(ElementPtr host) {
+    ConstElementPtr flex_id = host->get("flex-id");
+    if (!flex_id) {
+        return;
+    }
+    const string& id = flex_id->stringValue();
+    if (id.empty()) {
+        isc_throw(BadValue, "empty flexible identifier in " << host->str());
+    }
+    // No special and no not printable characters?
+    if (id.find_first_not_of(STD_CHARACTERS) == string::npos) {
+        return;
+    }
+    // Quoted identifier?
+    vector<uint8_t> binary = str::quotedStringToBinary(id);
+    if (binary.empty()) {
+        binary.assign(id.begin(), id.end());
+    }
+    // Convert in hexadecimal (from DUID::toText()).
+    stringstream tmp;
+    tmp << hex;
+    bool delim = false;
+    for (vector<uint8_t>::const_iterator it = binary.begin();
+         it != binary.end(); ++it) {
+        if (delim) {
+            tmp << ":";
+        }
+        tmp << setw(2) << setfill('0') << static_cast<unsigned int>(*it);
+        delim = true;
+    }
+    host->set("flex-id", Element::create(tmp.str()));
+}
+
+}; // end of namespace isc::yang
+}; // end of namespace isc
diff --git a/src/lib/yang/adaptor_host.h b/src/lib/yang/adaptor_host.h
new file mode 100644 (file)
index 0000000..4c12098
--- /dev/null
@@ -0,0 +1,47 @@
+// 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_HOST_H
+#define ISC_ADAPTOR_HOST_H 1
+
+#include <yang/adaptor.h>
+
+namespace isc {
+namespace yang {
+
+/// @brief JSON adaptor for host reservations quoting identifiers.
+///
+/// The identifier type and identifier value are used as keys in YANG
+/// host reservation lists so soem constraints were put on their contents.
+/// For instance a quoted flex-id identifier raises an error (keys
+/// are between quotes in setItem commands).
+class AdaptorHost {
+public:
+
+    /// @brief The string of standard (vs special or not printable)
+    /// characters (digit, letters, -, ., @, _).
+    static const std::string STD_CHARACTERS;
+
+    /// @brief Constructor.
+    AdaptorHost();
+
+    /// @brief Destructor.
+    virtual ~AdaptorHost();
+
+    /// @brief Quote when needed a host identifier.
+    ///
+    /// Check if the flex-id identifier includes a special (including quote)
+    /// or not printable character. When it is the case produce and replace
+    /// by a hexadecimal identifier trying first for a quoted identifier.
+    ///
+    /// @param host The host.
+    static void quoteIdentifier(isc::data::ElementPtr host);
+};
+
+}; // end of namespace isc::yang
+}; // end of namespace isc
+
+#endif // ISC_ADAPTOR_HOST_H
diff --git a/src/lib/yang/adaptor_option.cc b/src/lib/yang/adaptor_option.cc
new file mode 100644 (file)
index 0000000..901fccb
--- /dev/null
@@ -0,0 +1,124 @@
+// 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_option.h>
+#include <dhcp/std_option_defs.h>
+#include <dhcp/docsis3_option_defs.h>
+
+using namespace std;
+using namespace isc::data;
+using namespace isc::dhcp;
+
+namespace isc {
+namespace yang {
+
+AdaptorOption::AdaptorOption() {
+}
+
+AdaptorOption::~AdaptorOption() {
+}
+
+void
+AdaptorOption::setSpace(ElementPtr option, const string& space) {
+    if (!option->contains("space")) {
+        option->set("space", Element::create(space));
+    }
+}
+
+void
+AdaptorOption::checkType(ConstElementPtr option) {
+    if (!option->contains("type")) {
+        isc_throw(MissingKey, "missing type in option definition "
+                  << option->str());
+    }
+}
+
+void
+AdaptorOption::checkCode(ConstElementPtr option) {
+    if (!option->contains("code")) {
+        isc_throw(MissingKey, "missing code in option " << option->str());
+    }
+}
+
+void
+AdaptorOption::collect(ConstElementPtr option, OptionCodes& codes) {
+    ConstElementPtr space = option->get("space");
+    ConstElementPtr code = option->get("code");
+    ConstElementPtr name = option->get("name");
+    if (name) {
+        string index = space->stringValue() + "@" + name->stringValue();
+        uint16_t val = static_cast<uint16_t>(code->intValue());
+        codes.insert(std::pair<string, uint16_t>(index, val));
+    }
+}
+
+void
+AdaptorOption::setCode(ElementPtr option, const OptionCodes& codes) {
+    ConstElementPtr code = option->get("code");
+    if (!code) {
+        ConstElementPtr name = option->get("name");
+        if (!name) {
+            isc_throw(MissingKey, "missing name and code in option "
+                      << option->str());
+        }
+        ConstElementPtr space = option->get("space");
+        string index = space->stringValue() + "@" + name->stringValue();
+        OptionCodes::const_iterator it = codes.find(index);
+        if (it == codes.end()) {
+            isc_throw(MissingKey, "can't get code from option "
+                      << option->str());
+        }
+        option->set("code", Element::create(static_cast<int>(it->second)));
+    }
+}
+
+void
+AdaptorOption::initCodes(OptionCodes& codes, const string& space) {
+    if (space == "dhcp4") {
+        initCodesInternal(codes, space, STANDARD_V4_OPTION_DEFINITIONS,
+                          STANDARD_V4_OPTION_DEFINITIONS_SIZE);
+        initCodesInternal(codes, space, LAST_RESORT_V4_OPTION_DEFINITIONS,
+                          LAST_RESORT_V4_OPTION_DEFINITIONS_SIZE);
+        initCodesInternal(codes, "vendor-4491",
+                          DOCSIS3_V4_DEFS, DOCSIS3_V4_DEFS_SIZE);
+    } else if (space == "dhcp6") {
+        initCodesInternal(codes, space, STANDARD_V6_OPTION_DEFINITIONS,
+                          STANDARD_V6_OPTION_DEFINITIONS_SIZE);
+        initCodesInternal(codes, "vendor-4491",
+                          DOCSIS3_V6_DEFS, DOCSIS3_V6_DEFS_SIZE);
+        initCodesInternal(codes, MAPE_V6_OPTION_SPACE,
+                          MAPE_V6_OPTION_DEFINITIONS,
+                          MAPE_V6_OPTION_DEFINITIONS_SIZE);
+        initCodesInternal(codes, MAPT_V6_OPTION_SPACE,
+                          MAPT_V6_OPTION_DEFINITIONS,
+                          MAPT_V6_OPTION_DEFINITIONS_SIZE);
+        initCodesInternal(codes, LW_V6_OPTION_SPACE,
+                          LW_V6_OPTION_DEFINITIONS,
+                          LW_V6_OPTION_DEFINITIONS_SIZE);
+        initCodesInternal(codes, V4V6_RULE_OPTION_SPACE,
+                          V4V6_RULE_OPTION_DEFINITIONS,
+                          V4V6_RULE_OPTION_DEFINITIONS_SIZE);
+        initCodesInternal(codes, V4V6_BIND_OPTION_SPACE,
+                          V4V6_BIND_OPTION_DEFINITIONS,
+                          V4V6_BIND_OPTION_DEFINITIONS_SIZE);
+        initCodesInternal(codes, "vendor-2495",
+                          ISC_V6_OPTION_DEFINITIONS,
+                          ISC_V6_OPTION_DEFINITIONS_SIZE);
+    }
+}
+
+void
+AdaptorOption::initCodesInternal(OptionCodes& codes, const string& space,
+                                 const OptionDefParams* params,
+                                 size_t params_size) {
+    for (size_t i = 0; i < params_size; ++i) {
+        string index = space + "@" + params[i].name;
+        codes.insert(std::pair<string, uint16_t>(index, params[i].code));
+    }
+}
+
+}; // end of namespace isc::yang
+}; // end of namespace isc
diff --git a/src/lib/yang/adaptor_option.h b/src/lib/yang/adaptor_option.h
new file mode 100644 (file)
index 0000000..69a3ce8
--- /dev/null
@@ -0,0 +1,104 @@
+// 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_OPTION_H
+#define ISC_ADAPTOR_OPTION_H 1
+
+#include <yang/adaptor.h>
+#include <map>
+#include <list>
+
+namespace isc {
+
+namespace dhcp {
+/// @brief Forward declaration of option definition parameters.
+struct OptionDefParams;
+};
+
+namespace yang {
+
+/// @brief Map for DHCP option definitions handling code and
+/// an index built from space and name.
+typedef std::map<std::string, uint16_t> OptionCodes;
+
+/// @brief JSON adaptor for option data or definition setting defaults.
+///
+/// Both option data and option definition lists are keyed by the code
+/// and space pair so both must be available in setOptionXXX methods.
+/// For space there is an implicit default so setSpace must be called
+/// to add this default to option entries without space.
+/// Note remaining adaptors assume this was done first.
+///
+/// checkCode and checkType can be used to check if code or type is present
+/// (type is mandatory in option definitions).
+///
+/// A missing code can be found in standard definitions (loaded by initCodes)
+/// or in configuration option definitions (at global and client classes
+/// scopes). setCode uses the space+name to code map to set missing codes
+/// and raises an error when it can't.
+class AdaptorOption {
+public:
+
+    /// @brief Constructor.
+    AdaptorOption();
+
+    /// @brief Destructor.
+    virtual ~AdaptorOption();
+
+    /// @brief Set space.
+    ///
+    /// @param option The option.
+    /// @param space The default space name.
+    static void setSpace(isc::data::ElementPtr option,
+                         const std::string& space);
+
+    /// @brief Check type (option definition).
+    ///
+    /// @param option The option.
+    /// @throw MissingKey if the type is not present.
+    static void checkType(isc::data::ConstElementPtr option);
+
+    /// @brief Check code.
+    ///
+    /// @param option The option.
+    /// @throw MissingKey if the code is not present.
+    static void checkCode(isc::data::ConstElementPtr option);
+
+    /// @brief Collect definition.
+    ///
+    /// @param option The option definition.
+    /// @param codes The reference to option definitions.
+    static void collect(isc::data::ConstElementPtr option, OptionCodes& codes);
+
+    /// @brief Set code from name and definitions.
+    ///
+    /// @param option The option data.
+    /// @param codes Option definitions.
+    static void setCode(isc::data::ElementPtr option,
+                        const OptionCodes& codes);
+
+    /// @brief Initialize code map.
+    ///
+    /// @param codes The reference to option definitions.
+    /// @param space The space name.
+    static void initCodes(OptionCodes& codes, const std::string& space);
+
+protected:
+    /// @brief Initialize code map from option definition parameters.
+    ///
+    /// @param codes The reference to option definitions.
+    /// @param space The space name.
+    /// @param params Array of option definition parameters
+    /// @param params_size The size of the array.
+    static void initCodesInternal(OptionCodes& codes, const std::string& space,
+                                  const isc::dhcp::OptionDefParams* params,
+                                  size_t params_size);
+};
+
+}; // end of namespace isc::yang
+}; // end of namespace isc
+
+#endif // ISC_ADAPTOR_OPTION_H
diff --git a/src/lib/yang/adaptor_pool.cc b/src/lib/yang/adaptor_pool.cc
new file mode 100644 (file)
index 0000000..56dbd07
--- /dev/null
@@ -0,0 +1,85 @@
+// 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_pool.h>
+#include <yang/yang_models.h>
+
+using namespace std;
+using namespace isc::data;
+
+namespace isc {
+namespace yang {
+
+AdaptorPool::AdaptorPool() {
+}
+
+AdaptorPool::~AdaptorPool() {
+}
+
+void
+AdaptorPool::canonizePool(ElementPtr pool) {
+    const string& orig = pool->get("pool")->stringValue();
+    vector<char> v;
+    for (char ch : orig) {
+        if ((ch == ' ') || (ch == '\t') || (ch == '\n')) {
+            continue;
+        } else if (ch == '-') {
+            v.push_back(' ');
+            v.push_back(ch);
+            v.push_back(' ');
+        } else {
+            v.push_back(ch);
+        }
+    }
+    string canon;
+    canon.assign(v.begin(), v.end());
+    if (orig != canon) {
+        pool->set("pool", Element::create(canon));
+    }
+}
+
+void
+AdaptorPool::fromSubnet(const string& model, ConstElementPtr subnet,
+                        ConstElementPtr pools) {
+    if (model == IETF_DHCPV6_SERVER) {
+        fromSubnetIetf6(subnet, pools);
+    } else if ((model != KEA_DHCP4_SERVER) &&
+               (model != KEA_DHCP6_SERVER)) {
+        isc_throw(NotImplemented,
+                  "fromSubnet not implemented for the model: " << model);
+    }
+}
+
+void
+AdaptorPool::fromSubnetIetf6(ConstElementPtr subnet, ConstElementPtr pools) {
+    Adaptor::fromParent("valid-lifetime", subnet, pools);
+    Adaptor::fromParent("preferred-lifetime", subnet, pools);
+    Adaptor::fromParent("renew-timer", subnet, pools);
+    Adaptor::fromParent("rebind-timer", subnet, pools);
+}
+
+void
+AdaptorPool::toSubnet(const string& model, ElementPtr subnet,
+                      ConstElementPtr pools) {
+    if (model == IETF_DHCPV6_SERVER) {
+        toSubnetIetf6(subnet, pools);
+    } else if ((model != KEA_DHCP4_SERVER) &&
+               (model != KEA_DHCP6_SERVER)) {
+        isc_throw(NotImplemented,
+                  "toSubnet not implemented for the model: " << model);
+    }
+}
+
+void
+AdaptorPool::toSubnetIetf6(ElementPtr subnet, ConstElementPtr pools) {
+    Adaptor::toParent("valid-lifetime", subnet, pools);
+    Adaptor::toParent("preferred-lifetime", subnet, pools);
+    Adaptor::toParent("renew-timer", subnet, pools);
+    Adaptor::toParent("rebind-timer", subnet, pools);
+}
+
+}; // end of namespace isc::yang
+}; // end of namespace isc
diff --git a/src/lib/yang/adaptor_pool.h b/src/lib/yang/adaptor_pool.h
new file mode 100644 (file)
index 0000000..9d19c23
--- /dev/null
@@ -0,0 +1,91 @@
+// 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_POOL_H
+#define ISC_ADAPTOR_POOL_H 1
+
+#include <yang/adaptor.h>
+#include <list>
+
+namespace isc {
+namespace yang {
+
+/// @brief JSON adaptor for pools between canonical Kea and YANG models.
+///
+/// First adaptor canonizePool checks and fixes if needed the pool entry
+/// to a canonical form which is no space for prefix and one space each
+/// side of the minus character for ranges.
+///
+/// Second adaptor is specific to the IETF DHCPv6 model (and does nothing
+/// with a Kea DHCP model): it moves timer definitions from the subnet scope,
+/// i.e. the scope where they are in Kea, to the pool scope, i.e. the scope
+/// where they are in the IETF model, and back. The from way leaves timers
+/// in the subnet scope as they are ignored by the translator, the to way
+/// removes timers from pools as they are not expected by Kea at this scope.
+class AdaptorPool {
+public:
+
+    /// @brief Constructor.
+    AdaptorPool();
+
+    /// @brief Destructor.
+    virtual ~AdaptorPool();
+
+    /// @brief Canonize pool.
+    ///
+    /// Remove spaces and replace "-" by " - " for readability.
+    /// @param pool The pool.
+    static void canonizePool(isc::data::ElementPtr pool);
+
+    /// @brief From subnets.
+    ///
+    /// Move parameters from the subnet to each pool.
+    ///
+    /// @param model Model name.
+    /// @param subnet The subnet element.
+    /// @param pools The children pools.
+    /// @throw NotImplemented on unexpected model.
+    static void fromSubnet(const std::string& model,
+                           isc::data::ConstElementPtr subnet,
+                           isc::data::ConstElementPtr pools);
+
+    /// @brief To subnet.
+    ///
+    /// Move parameters from pools to the subnet.
+    ///
+    /// @param model Model name.
+    /// @param subnet The subnet element.
+    /// @param pools The children pools.
+    /// @throw NotImplemented on unexpected model.
+    /// @throw BadValue on inconsistent (different timer values) pools.
+    static void toSubnet(const std::string& model,
+                         isc::data::ElementPtr subnet,
+                         isc::data::ConstElementPtr pools);
+
+protected:
+    /// @brief From subnets for ietf-dhcpv6-server.
+    ///
+    /// Use common and move valid-lifetime and preferred-lifetime.
+    ///
+    /// @param subnet The subnet element.
+    /// @param pools The children pools.
+    static void fromSubnetIetf6(isc::data::ConstElementPtr subnet,
+                                isc::data::ConstElementPtr pools);
+
+    /// @brief To subnet for ietf-dhcpv6-server.
+    ///
+    /// Use common and move valid-lifetime and preferred-lifetime.
+    ///
+    /// @param subnet The subnet element.
+    /// @param pools The children pools.
+    static void toSubnetIetf6(isc::data::ElementPtr subnet,
+                              isc::data::ConstElementPtr pools);
+};
+
+}; // end of namespace isc::yang
+}; // end of namespace isc
+
+#endif // ISC_ADAPTOR_POOL_H
diff --git a/src/lib/yang/adaptor_subnet.cc b/src/lib/yang/adaptor_subnet.cc
new file mode 100644 (file)
index 0000000..862e6b1
--- /dev/null
@@ -0,0 +1,71 @@
+// 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_subnet.h>
+
+using namespace std;
+using namespace isc::data;
+using namespace isc::dhcp;
+
+namespace isc {
+namespace yang {
+
+AdaptorSubnet::AdaptorSubnet() {
+}
+
+AdaptorSubnet::~AdaptorSubnet() {
+}
+
+bool
+AdaptorSubnet::collectID(ConstElementPtr subnet, SubnetIDSet& set) {
+    ConstElementPtr id = subnet->get("id");
+    if (id) {
+        set.insert(static_cast<SubnetID>(id->intValue()));
+        return (true);
+    }
+    return (false);
+}
+
+void
+AdaptorSubnet::assignID(ElementPtr subnet, SubnetIDSet& set, SubnetID& next) {
+    ConstElementPtr id = subnet->get("id");
+    if (!id) {
+        // Skip already used.
+        while (set.count(next) > 0) {
+            ++next;
+        }
+        subnet->set("id", Element::create(static_cast<long long>(next)));
+        set.insert(next);
+        ++next;
+    }
+}
+
+void
+AdaptorSubnet::updateRelay(ElementPtr subnet) {
+    ConstElementPtr relay = subnet->get("relay");
+    if (!relay) {
+        return;
+    }
+    ConstElementPtr addresses = relay->get("ip-addresses");
+    if (!addresses) {
+        ConstElementPtr address = relay->get("ip-address");
+        if (!address) {
+            subnet->remove("relay");
+            return;
+        }
+        ElementPtr addr = Element::create(address->stringValue());
+        ElementPtr addrs = Element::createList();
+        addrs->add(addr);
+        ElementPtr updated = Element::createMap();
+        updated->set("ip-addresses", addrs);
+        subnet->set("relay", updated);
+    } else if (addresses->size() == 0) {
+        subnet->remove("relay");
+    }
+}
+
+}; // end of namespace isc::yang
+}; // end of namespace isc
diff --git a/src/lib/yang/adaptor_subnet.h b/src/lib/yang/adaptor_subnet.h
new file mode 100644 (file)
index 0000000..d6ad80b
--- /dev/null
@@ -0,0 +1,66 @@
+// 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_SUBNET_H
+#define ISC_ADAPTOR_SUBNET_H 1
+
+#include <yang/adaptor.h>
+#include <dhcpsrv/subnet_id.h>
+#include <set>
+
+namespace isc {
+namespace yang {
+
+/// @brief Set of SubnetIDs.
+typedef std::set<isc::dhcp::SubnetID> SubnetIDSet;
+
+/// @brief JSON adaptor for subnets adding IDs and canonizes relays.
+///
+/// Adding IDs is done in two passes walking through subnets.
+///  -1- Add in the set used values and return false when there is no ID
+///      so the caller can decide if the second pass is needed.
+///  -2- For a subnet without an ID, assigned the next unused ID.
+///
+/// For relays an old syntax ip-address is translated into a new syntax
+/// ip-addresses. Note as all canonization adaptor it is optional, i.e.,
+/// code should work without it.
+class AdaptorSubnet {
+public:
+
+    /// @brief Constructor.
+    AdaptorSubnet();
+
+    /// @brief Destructor.
+    virtual ~AdaptorSubnet();
+
+    /// @brief Collect a subnet ID.
+    ///
+    /// @param subnet The subnet.
+    /// @param set The reference to the set of assigned IDs.
+    /// @return True if the subnet has an ID, false otherwise.
+    static bool collectID(isc::data::ConstElementPtr subnet, SubnetIDSet& set);
+
+    /// @brief Assign subnet ID.
+    ///
+    /// @param subnet The subnet.
+    /// @param set The reference to the set of assigned IDs.
+    /// @param next The next ID.
+    static void assignID(isc::data::ElementPtr subnet, SubnetIDSet& set,
+                         isc::dhcp::SubnetID& next);
+
+    /// @brief Update relay.
+    ///
+    /// Force the use of ip-addresses vs. ip-address.
+    /// Can be used for shared networks too.
+    ///
+    /// @param subnet The subnet.
+    static void updateRelay(isc::data::ElementPtr subnet);
+};
+
+}; // end of namespace isc::yang
+}; // end of namespace isc
+
+#endif // ISC_ADAPTOR_SUBNET_H
index 2514036a39475d87691e6fa9dd877f52fdcf2c4f..320987ef982fbd61009571110d3b79cabeb2c489 100644 (file)
@@ -18,6 +18,10 @@ TESTS =
 if HAVE_GTEST
 TESTS += run_unittests
 run_unittests_SOURCES  = adaptor_unittests.cc
+run_unittests_SOURCES += adaptor_option_unittests.cc
+run_unittests_SOURCES += adaptor_pool_unittests.cc
+run_unittests_SOURCES += adaptor_host_unittests.cc
+run_unittests_SOURCES += adaptor_subnet_unittests.cc
 run_unittests_SOURCES += sysrepo_setup.h
 run_unittests_SOURCES += translator_unittests.cc
 run_unittests_SOURCES += translator_control_socket_unittests.cc
diff --git a/src/lib/yang/tests/adaptor_host_unittests.cc b/src/lib/yang/tests/adaptor_host_unittests.cc
new file mode 100644 (file)
index 0000000..0e7f91c
--- /dev/null
@@ -0,0 +1,118 @@
+// 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_host.h>
+
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::data;
+using namespace isc::yang;
+
+namespace {
+
+// Verifies that quoteIdentifier does not touch an identifier which
+// has a type different from flex-d.
+TEST(AdaptorHostTest, notFlexId) {
+    string config = "{\n"
+        " \"hw-address\": \"1a:1b:1c:1d:1e:1f\",\n"
+        " \"ip-address\": \"192.0.2.201\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    EXPECT_NO_THROW(AdaptorHost::quoteIdentifier(json));
+    EXPECT_TRUE(copied->equals(*json));
+}
+
+// Verifies that quoteIdentifier does not touch a  flex-id identifier
+// without quotes.
+TEST(AdaptorHostTest, noQuote) {
+    string config = "{\n"
+        " \"flex-id\": \"s0mEVaLue\",\n"
+        " \"ip-address\": \"192.0.2.206\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    EXPECT_NO_THROW(AdaptorHost::quoteIdentifier(json));
+    EXPECT_TRUE(copied->equals(*json));
+}
+
+// Verifies that quoteIdentifier removes quotes from a flex-id identifier.
+TEST(AdaptorHostTest, quotes) {
+    string config = "{\n"
+        " \"flex-id\": \"'somevalue'\",\n"
+        " \"ip-addresses\": \"2001:db8:1:cafe::2\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    EXPECT_NO_THROW(AdaptorHost::quoteIdentifier(json));
+    EXPECT_FALSE(copied->equals(*json));
+    ConstElementPtr id = json->get("flex-id");
+    ASSERT_TRUE(id);
+    ASSERT_EQ(Element::string, id->getType());
+    EXPECT_EQ("73:6f:6d:65:76:61:6c:75:65", id->stringValue());
+}
+
+// Verifies that quoteIdentifier removes quotes from a flex-id identifier
+// but does not interpret a quote in the middle.
+TEST(AdaptorHostTest, extraQuote) {
+    string config = "{\n"
+        " \"flex-id\": \"'some'value'\",\n"
+        " \"ip-addresses\": \"2001:db8:1:cafe::2\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    EXPECT_NO_THROW(AdaptorHost::quoteIdentifier(json));
+    EXPECT_FALSE(copied->equals(*json));
+    ConstElementPtr id = json->get("flex-id");
+    ASSERT_TRUE(id);
+    ASSERT_EQ(Element::string, id->getType());
+    EXPECT_EQ("73:6f:6d:65:27:76:61:6c:75:65", id->stringValue());
+}
+
+// Verifies that quoteIdentifier works on not standard characters too.
+TEST(AdaptorHostTest, notStandard) {
+    string config = "{\n"
+        " \"flex-id\": \"'some\\\"value'\",\n"
+        " \"ip-addresses\": \"2001:db8:1:cafe::2\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    EXPECT_NO_THROW(AdaptorHost::quoteIdentifier(json));
+    EXPECT_FALSE(copied->equals(*json));
+    ConstElementPtr id = json->get("flex-id");
+    ASSERT_TRUE(id);
+    ASSERT_EQ(Element::string, id->getType());
+    EXPECT_EQ("73:6f:6d:65:22:76:61:6c:75:65", id->stringValue());
+}
+
+// Verifies that quoteIdentifier works on not standard characters too
+// even without quotes.
+TEST(AdaptorHostTest, notQuoted) {
+    string config = "{\n"
+        " \"flex-id\": \"some\\\"value\",\n"
+        " \"ip-addresses\": \"2001:db8:1:cafe::2\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    EXPECT_NO_THROW(AdaptorHost::quoteIdentifier(json));
+    EXPECT_FALSE(copied->equals(*json));
+    ConstElementPtr id = json->get("flex-id");
+    ASSERT_TRUE(id);
+    ASSERT_EQ(Element::string, id->getType());
+    EXPECT_EQ("73:6f:6d:65:22:76:61:6c:75:65", id->stringValue());
+}
+
+}; // end of anonymous namespace
diff --git a/src/lib/yang/tests/adaptor_option_unittests.cc b/src/lib/yang/tests/adaptor_option_unittests.cc
new file mode 100644 (file)
index 0000000..39831c3
--- /dev/null
@@ -0,0 +1,240 @@
+// 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_option.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/option_data_types.h>
+#include <dhcp/option_space.h>
+
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::yang;
+
+namespace {
+
+// Verifies that setSpace adds a space to an option without this entry.
+TEST(AdaptorOptionTest, setSpaceNoSpace) {
+    string config = "{\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    EXPECT_NO_THROW(AdaptorOption::setSpace(json, "foo"));
+    EXPECT_FALSE(copied->equals(*json));
+    ConstElementPtr space = json->get("space");
+    ASSERT_TRUE(space);
+    ASSERT_EQ(Element::string, space->getType());
+    EXPECT_EQ("foo", space->stringValue());
+}
+
+// Verifies that setSpace does not change to an option with space.
+TEST(AdaptorOptionTest, setSpace) {
+    string config = "{\n"
+        " \"space\": \"dhcp4\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    EXPECT_NO_THROW(AdaptorOption::setSpace(json, "foo"));
+    EXPECT_TRUE(copied->equals(*json));
+}
+
+// Verifies that checkType accepts an option with type.
+TEST(AdaptorOptionTest, checkType) {
+    string config = "{\n"
+        " \"type\": \"string\"\n"
+        "}";
+    ConstElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    EXPECT_NO_THROW(AdaptorOption::checkType(json));
+}
+
+// Verifies that checkType does not accept an option without type.
+TEST(AdaptorOptionTest, checkTypeNoType) {
+    string config = "{\n"
+        "}";
+    ConstElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    EXPECT_THROW(AdaptorOption::checkType(json), MissingKey);
+}
+
+// Verifies that checkCode accepts an option with code.
+TEST(AdaptorOptionTest, checkCode) {
+    string config = "{\n"
+        " \"code\": 123\n"
+        "}";
+    ConstElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    EXPECT_NO_THROW(AdaptorOption::checkCode(json));
+}
+
+// Verifies that checkCode does not accept an option without code.
+TEST(AdaptorOptionTest, checkCodeNoCode) {
+    string config = "{\n"
+        "}";
+    ConstElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    EXPECT_THROW(AdaptorOption::checkCode(json), MissingKey);
+}
+
+// Verifies that collect works as expected.
+TEST(AdaptorOptionTest, collect) {
+    string config = "{\n"
+        " \"code\": 123,\n"
+        " \"name\": \"foo\",\n"
+        " \"space\": \"bar\"\n"
+        "}";
+    ConstElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    OptionCodes codes;
+    ASSERT_NO_THROW(AdaptorOption::collect(json, codes));
+    EXPECT_EQ(1, codes.size());
+    EXPECT_EQ(123, codes["bar@foo"]);
+    EXPECT_THROW(codes.at("foo@bar"), out_of_range);
+}
+
+// Verifies that collect skips an already known option definition.
+TEST(AdaptorOptionTest, collectKnown) {
+    string config = "{\n"
+        " \"code\": 123,\n"
+        " \"name\": \"foo\",\n"
+        " \"space\": \"bar\"\n"
+        "}";
+    ConstElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    OptionCodes codes = { { "bar@foo", 111 } };
+    ASSERT_NO_THROW(AdaptorOption::collect(json, codes));
+    EXPECT_EQ(1, codes.size());
+    EXPECT_EQ(111, codes["bar@foo"]);
+}
+
+// Verifies that setCode adds a code to an option without this entry
+// when the code is available in the map.
+TEST(AdaptorOptionTest, setCodeNoCode) {
+    string config = "{\n"
+        " \"name\": \"foo\",\n"
+        " \"space\": \"bar\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    OptionCodes codes = { { "bar@foo", 123 } };
+    EXPECT_NO_THROW(AdaptorOption::setCode(json, codes));
+    EXPECT_FALSE(copied->equals(*json));
+    ConstElementPtr code = json->get("code");
+    ASSERT_TRUE(code);
+    ASSERT_EQ(Element::integer, code->getType());
+    EXPECT_EQ(123, code->intValue());
+}
+
+// Verifies that setCode does not change to an option with code.
+TEST(AdaptorOptionTest, setCode) {
+    string config = "{\n"
+        " \"code\": \"dhcp4\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    OptionCodes codes;
+    EXPECT_NO_THROW(AdaptorOption::setCode(json, codes));
+    EXPECT_TRUE(copied->equals(*json));
+}
+
+// Verifies that setCode raises an error on option without name and code.
+TEST(AdaptorOptionTest, setCodeNoName) {
+    string config = "{\n"
+        " \"space\": \"bar\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    OptionCodes codes;
+    EXPECT_THROW(AdaptorOption::setCode(json, codes), MissingKey);
+}
+
+// Note the code assumes there is a space, i.e. setSpace was called.
+
+// Verifies that setCode raises an error on option without code but
+// the code is not available in the map.
+TEST(AdaptorOptionTest, setCodeNotInMap) {
+    string config = "{\n"
+        " \"name\": \"foo\",\n"
+        " \"space\": \"bar\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    OptionCodes codes;
+    EXPECT_THROW(AdaptorOption::setCode(json, codes), MissingKey);
+}
+
+/// @brief Test class to make initCodes public.
+class TestAdaptorOption : public AdaptorOption {
+public:
+    using AdaptorOption::initCodesInternal;
+};
+
+// Verifies that initCodesInternal works as expected.
+TEST(AdaptorOptionTest, initCodesInternal) {
+    const OptionDefParams DEFS[] = {
+        { "subnet-mask", DHO_SUBNET_MASK, OPT_IPV4_ADDRESS_TYPE,
+          false, 0, 0, "" },
+        { "time-offset", DHO_TIME_OFFSET, OPT_INT32_TYPE,
+          false, 0, 0, "" }
+    };
+    const size_t DEFS_SIZE = sizeof(DEFS) / sizeof(DEFS[0]);
+    OptionCodes codes;
+    ASSERT_NO_THROW(TestAdaptorOption::initCodesInternal(codes,
+                                                         "dhcp4",
+                                                         DEFS,
+                                                         DEFS_SIZE));
+    ASSERT_EQ(2, codes.size());
+    EXPECT_EQ(DHO_SUBNET_MASK, codes["dhcp4@subnet-mask"]);
+    EXPECT_EQ(DHO_TIME_OFFSET, codes["dhcp4@time-offset"]);
+}
+
+// Verifies that initCodes works as expected with DHCPv4.
+TEST(AdaptorOptionTest, initCodes4) {
+    OptionCodes codes;
+    ASSERT_NO_THROW(AdaptorOption::initCodes(codes, DHCP4_OPTION_SPACE));
+    EXPECT_EQ(DHO_SUBNET_MASK, codes["dhcp4@subnet-mask"]);
+    EXPECT_EQ(DHO_TIME_OFFSET, codes["dhcp4@time-offset"]);
+    EXPECT_THROW(codes.at("dhcp6@clientid"), out_of_range);
+
+    // initCodes loads last resort too.
+    EXPECT_EQ(DHO_VENDOR_ENCAPSULATED_OPTIONS,
+              codes["dhcp4@vendor-encapsulated-options"]);
+
+    // and DOCSIS3.
+    EXPECT_EQ(2, codes["vendor-4491@tftp-servers"]);
+}
+
+// Verifies that initCodes works as expected with DHCPv6.
+TEST(AdaptorOptionTest, initCodes6) {
+    OptionCodes codes;
+    ASSERT_NO_THROW(AdaptorOption::initCodes(codes, DHCP6_OPTION_SPACE));
+    EXPECT_EQ(D6O_CLIENTID, codes["dhcp6@clientid"]);
+    EXPECT_EQ(D6O_SERVERID, codes["dhcp6@serverid"]);
+    EXPECT_THROW(codes.at("dhcp4@subnet-mask"), out_of_range);
+
+    // initCodes loads DOCSIS3 too.
+    EXPECT_EQ(32, codes["vendor-4491@tftp-servers"]);
+
+    // Various MAP suboptions.
+    EXPECT_EQ(D6O_S46_BR, codes["s46-cont-mape-options@s46-br"]);
+    EXPECT_EQ(D6O_S46_V4V6BIND, codes["s46-cont-lw-options@s46-v4v6bind"]);
+    EXPECT_EQ(D6O_S46_PORTPARAMS, codes["s46-v4v6bind-options@s46-portparams"]);
+
+    // And ISC space.
+    EXPECT_EQ(ISC_V6_4O6_INTERFACE, codes["vendor-2495@4o6-interface"]);
+}
+
+}; // end of anonymous namespace
diff --git a/src/lib/yang/tests/adaptor_pool_unittests.cc b/src/lib/yang/tests/adaptor_pool_unittests.cc
new file mode 100644 (file)
index 0000000..564d037
--- /dev/null
@@ -0,0 +1,270 @@
+// 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_pool.h>
+#include <yang/yang_models.h>
+
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::data;
+using namespace isc::yang;
+
+namespace {
+
+// Verifies that canonizePool does not touch a prefix without space.
+TEST(AdaptorPoolTest, canonizePoolPrefixNoSpace) {
+    string config = "{\n"
+        " \"pool\": \"192.0.2.128/28\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    EXPECT_NO_THROW(AdaptorPool::canonizePool(json));
+    EXPECT_TRUE(copied->equals(*json));
+}
+
+// Verifies that canonizePool does not touch a canonical range.
+TEST(AdaptorPoolTest, canonizePoolRange) {
+    string config = "{\n"
+        " \"pool\": \"192.0.2.1 - 192.0.2.200\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    EXPECT_NO_THROW(AdaptorPool::canonizePool(json));
+    EXPECT_TRUE(copied->equals(*json));
+}
+
+// Verifies that canonizePool removes spaces from a prefix.
+TEST(AdaptorPoolTest, canonizePoolPrefixSpaces) {
+    string config = "{\n"
+        " \"pool\": \"192.0.2.128 /\t28\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    EXPECT_NO_THROW(AdaptorPool::canonizePool(json));
+    EXPECT_FALSE(copied->equals(*json));
+    ConstElementPtr pool = json->get("pool");
+    ASSERT_TRUE(pool);
+    ASSERT_EQ(Element::string, pool->getType());
+    EXPECT_EQ("192.0.2.128/28", pool->stringValue());
+}
+
+// Verifies that canonizePool adds two spaces from a range.
+TEST(AdaptorPoolTest, canonizePoolRangeNoSpace) {
+    string config = "{\n"
+        " \"pool\": \"192.0.2.1-192.0.2.200\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    EXPECT_NO_THROW(AdaptorPool::canonizePool(json));
+    EXPECT_FALSE(copied->equals(*json));
+    ConstElementPtr pool = json->get("pool");
+    ASSERT_TRUE(pool);
+    ASSERT_EQ(Element::string, pool->getType());
+    EXPECT_EQ("192.0.2.1 - 192.0.2.200", pool->stringValue());
+}
+
+// Verifies that canonizePool removes extra spaces from a range.
+TEST(AdaptorPoolTest, canonizePoolRangeExtraSpaces) {
+    string config = "{\n"
+        " \"pool\": \"192.0.2.1  -  192.0.2.200\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    EXPECT_NO_THROW(AdaptorPool::canonizePool(json));
+    EXPECT_FALSE(copied->equals(*json));
+    ConstElementPtr pool = json->get("pool");
+    ASSERT_TRUE(pool);
+    ASSERT_EQ(Element::string, pool->getType());
+    EXPECT_EQ("192.0.2.1 - 192.0.2.200", pool->stringValue());
+}
+
+// Verifies that fromSubnet is specific to ietf-dhcpv6-server model.
+TEST(AdaptorPoolTest, fromSubnetKea) {
+    string config = "{\n"
+        " \"subnet\": \"192.0.2.0/24\",\n"
+        " \"pools\": [\n"
+        "     {\n"
+        "         \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+        "     },{\n"
+        "         \"pool\": \"192.0.2.101 - 192.0.2.200\"\n"
+        "     } ],\n"
+        " \"valid-lifetime\": 4000,\n"
+        " \"renew-timer\": 1000,\n"
+        " \"rebind-timer\": 2000\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    ConstElementPtr pools = json->get("pools");
+    EXPECT_NO_THROW(AdaptorPool::fromSubnet(KEA_DHCP4_SERVER, json, pools));
+    EXPECT_TRUE(copied->equals(*json));
+    // The model is checked first.
+    EXPECT_NO_THROW(AdaptorPool::fromSubnet(KEA_DHCP6_SERVER, json, pools));
+    EXPECT_TRUE(copied->equals(*json));
+    // Model name is not free: an error is raised if it is not expected.
+    EXPECT_THROW(AdaptorPool::fromSubnet("keatest-module", json, pools),
+                 NotImplemented);
+}
+
+// Verifies that fromSubnet works as expected.
+TEST(AdaptorPoolTest, fromSubnet) {
+    string config = "{\n"
+        " \"subnet\": \"2001:db8:1::/64\",\n"
+        " \"pools\": [\n"
+        "     {\n"
+        "         \"pool\": \"2001:db8:1::/80\"\n"
+        "     },{\n"
+        "         \"pool\": \"2001:db8:1:0:1::/80\"\n"
+        "     } ],\n"
+        " \"preferred-lifetime\": 3000,\n"
+        " \"valid-lifetime\": 4000,\n"
+        " \"renew-timer\": 1000,\n"
+        " \"rebind-timer\": 2000\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    ConstElementPtr pools = json->get("pools");
+    EXPECT_NO_THROW(AdaptorPool::fromSubnet(IETF_DHCPV6_SERVER, json, pools));
+    EXPECT_FALSE(copied->equals(*json));
+    pools = json->get("pools");
+    ASSERT_TRUE(pools);
+    ASSERT_EQ(2, pools->size());
+    ConstElementPtr pool = pools->get(0);
+    ASSERT_TRUE(pool);
+    string expected = "{"
+        " \"pool\": \"2001:db8:1::/80\","
+        " \"preferred-lifetime\": 3000,"
+        " \"rebind-timer\": 2000,"
+        " \"renew-timer\": 1000,"
+        " \"valid-lifetime\": 4000 }";
+    EXPECT_EQ(expected, pool->str());
+    pool = pools->get(1);
+    ASSERT_TRUE(pool);
+    expected = "{"
+        " \"pool\": \"2001:db8:1:0:1::/80\","
+        " \"preferred-lifetime\": 3000,"
+        " \"rebind-timer\": 2000,"
+        " \"renew-timer\": 1000,"
+        " \"valid-lifetime\": 4000 }";
+    EXPECT_EQ(expected, pool->str());
+}
+
+// Verifies that toSubnet is specific to ietf-dhcpv6-server model.
+TEST(AdaptorPoolTest, toSubnetKea) {
+    string config = "{\n"
+        " \"subnet\": \"192.0.2.0/24\",\n"
+        " \"pools\": [\n"
+        "     {\n"
+        "         \"pool\": \"192.0.2.1 - 192.0.2.100\"\n"
+        "     },{\n"
+        "         \"pool\": \"192.0.2.101 - 192.0.2.200\"\n"
+        "     } ],\n"
+        " \"valid-lifetime\": 4000,\n"
+        " \"renew-timer\": 1000,\n"
+        " \"rebind-timer\": 2000\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    ConstElementPtr pools = json->get("pools");
+    EXPECT_NO_THROW(AdaptorPool::toSubnet(KEA_DHCP4_SERVER, json, pools));
+    EXPECT_TRUE(copied->equals(*json));
+    // The model is checked first.
+    EXPECT_NO_THROW(AdaptorPool::toSubnet(KEA_DHCP6_SERVER, json, pools));
+    EXPECT_TRUE(copied->equals(*json));
+    // Model name is not free: an error is raised if it is not expected.
+    EXPECT_THROW(AdaptorPool::toSubnet("keatest-module", json, pools),
+                 NotImplemented);
+}
+
+// Verifies that toSubnet works as expected.
+TEST(AdaptorPoolTest, toSubnet) {
+    string config = "{\n"
+        " \"subnet\": \"2001:db8:1::/64\",\n"
+        " \"pools\": [\n"
+        "     {\n"
+        "         \"pool\": \"2001:db8:1::/80\",\n"
+        "         \"preferred-lifetime\": 3000,\n"
+        "         \"valid-lifetime\": 4000,\n"
+        "         \"renew-timer\": 1000,\n"
+        "         \"rebind-timer\": 2000\n"
+        "     },{\n"
+        "         \"pool\": \"2001:db8:1:0:1::/80\",\n"
+        "         \"preferred-lifetime\": 3000,\n"
+        "         \"valid-lifetime\": 4000,\n"
+        "         \"renew-timer\": 1000,\n"
+        "         \"rebind-timer\": 2000\n"
+        "     } ]\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    ConstElementPtr pools = json->get("pools");
+    EXPECT_NO_THROW(AdaptorPool::toSubnet(IETF_DHCPV6_SERVER, json, pools));
+    EXPECT_FALSE(copied->equals(*json));
+    // Check timers.
+    ConstElementPtr timer = json->get("valid-lifetime");
+    ASSERT_TRUE(timer);
+    EXPECT_EQ("4000", timer->str());
+    timer = json->get("preferred-lifetime");
+    ASSERT_TRUE(timer);
+    EXPECT_EQ("3000", timer->str());
+    timer = json->get("renew-timer");
+    ASSERT_TRUE(timer);
+    EXPECT_EQ("1000", timer->str());
+    timer = json->get("rebind-timer");
+    ASSERT_TRUE(timer);
+    EXPECT_EQ("2000", timer->str());
+    // Timers must be removed as they are not allowed here in Kea.
+    pools = json->get("pools");
+    ASSERT_TRUE(pools);
+    ASSERT_EQ(2, pools->size());
+    ConstElementPtr pool = pools->get(0);
+    ASSERT_TRUE(pool);
+    EXPECT_EQ("{ \"pool\": \"2001:db8:1::/80\" }", pool->str());
+    pool = pools->get(1);
+    ASSERT_TRUE(pool);
+    EXPECT_EQ("{ \"pool\": \"2001:db8:1:0:1::/80\" }", pool->str());
+}
+
+// Verifies that toSubnet fails on inconsistent input.
+TEST(AdaptorPoolTest, toSubnetBad) {
+    // Changed last rebind-timer to a different value.
+    string config = "{\n"
+        " \"subnet\": \"2001:db8:1::/64\",\n"
+        " \"pools\": [\n"
+        "     {\n"
+        "         \"pool\": \"2001:db8:1::/80\",\n"
+        "         \"preferred-lifetime\": 3000,\n"
+        "         \"valid-lifetime\": 4000,\n"
+        "         \"renew-timer\": 1000,\n"
+        "         \"rebind-timer\": 2000\n"
+        "     },{\n"
+        "         \"pool\": \"2001:db8:1:0:1::/80\",\n"
+        "         \"preferred-lifetime\": 3000,\n"
+        "         \"valid-lifetime\": 4000,\n"
+        "         \"renew-timer\": 1000,\n"
+        "         \"rebind-timer\": 20\n"
+        "     } ]\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr pools = json->get("pools");
+    EXPECT_THROW(AdaptorPool::toSubnet(IETF_DHCPV6_SERVER, json, pools),
+                 BadValue);
+}
+
+}; // end of anonymous namespace
diff --git a/src/lib/yang/tests/adaptor_subnet_unittests.cc b/src/lib/yang/tests/adaptor_subnet_unittests.cc
new file mode 100644 (file)
index 0000000..04bd84d
--- /dev/null
@@ -0,0 +1,212 @@
+// 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_subnet.h>
+
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::yang;
+
+namespace {
+
+// Verifies how collectID handles a subnet entry without ID.
+TEST(AdaptorSubnetTest, collectNoId) {
+    string config = "{\n"
+        " \"subnet\": \"192.0.2.0/24\"\n"
+        "}";
+    ConstElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    SubnetIDSet set;
+    bool ret = true;
+    ASSERT_NO_THROW(ret = AdaptorSubnet::collectID(json, set));
+    EXPECT_FALSE(ret);
+    EXPECT_EQ(0, set.size());
+}
+
+// Verifies how collectID handles a subnet entry with an ID.
+TEST(AdaptorSubnetTest, collectId) {
+    string config = "{\n"
+        " \"subnet\": \"192.0.2.0/24\",\n"
+        " \"id\": 123\n"
+        "}";
+    ConstElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    SubnetIDSet set;
+    bool ret = false;
+    ASSERT_NO_THROW(ret = AdaptorSubnet::collectID(json, set));
+    EXPECT_TRUE(ret);
+    EXPECT_EQ(1, set.size());
+    EXPECT_EQ(1, set.count(123));
+}
+
+// Verifies how collectID handles a subnet entry with an ID which is
+// already known: the set is not updated.
+TEST(AdaptorSubnetTest, collectKnownId) {
+    string config = "{\n"
+        " \"subnet\": \"192.0.2.0/24\",\n"
+        " \"id\": 123\n"
+        "}";
+    ConstElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    SubnetIDSet set = { 123 };
+    bool ret = false;
+    ASSERT_NO_THROW(ret = AdaptorSubnet::collectID(json, set));
+    EXPECT_TRUE(ret);
+    EXPECT_EQ(1, set.size());
+    EXPECT_EQ(1, set.count(123));
+}
+
+// Verifies how assignID handles a subnet entry without ID: the next ID
+// is assigned, the set is updated and the next ID is incremented.
+TEST(AdaptorSubnetTest, assignNoId) {
+    string config = "{\n"
+        " \"subnet\": \"192.0.2.0/24\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    SubnetIDSet set;
+    SubnetID next_id = 123;
+    ASSERT_NO_THROW(AdaptorSubnet::assignID(json, set, next_id));
+    EXPECT_FALSE(copied->equals(*json));
+    EXPECT_EQ(1, set.size());
+    EXPECT_EQ(1, set.count(123));
+    EXPECT_EQ(124, next_id);
+    ConstElementPtr id = json->get("id");
+    ASSERT_TRUE(id);
+    ASSERT_EQ(Element::integer, id->getType());
+    EXPECT_EQ(123, id->intValue());
+}
+
+// Verifies how assignID handles a subnet entry without ID but with the
+// candidate ID already used: the used value is skipped.
+TEST(AdaptorSubnetTest, assignNoIdUsed) {
+    string config = "{\n"
+        " \"subnet\": \"192.0.2.0/24\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    SubnetIDSet set = { 123 };
+    SubnetID next_id = 123;
+    ASSERT_NO_THROW(AdaptorSubnet::assignID(json, set, next_id));
+    EXPECT_FALSE(copied->equals(*json));
+    EXPECT_EQ(2, set.size());
+    EXPECT_EQ(1, set.count(123));
+    EXPECT_EQ(1, set.count(124));
+    EXPECT_EQ(125, next_id);
+    ConstElementPtr id = json->get("id");
+    ASSERT_TRUE(id);
+    ASSERT_EQ(Element::integer, id->getType());
+    EXPECT_EQ(124, id->intValue());
+}
+
+// Verifies how assignID handles a subnet entry with ID: no change.
+TEST(AdaptorSubnetTest, assignId) {
+    string config = "{\n"
+        " \"subnet\": \"192.0.2.0/24\",\n"
+        " \"id\": 123\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    SubnetIDSet set; // ignored.
+    SubnetID next_id = 123; // ignored.
+    ASSERT_NO_THROW(AdaptorSubnet::assignID(json, set, next_id));
+    EXPECT_TRUE(copied->equals(*json));
+    EXPECT_EQ(0, set.size());
+    EXPECT_EQ(123, next_id);
+}
+
+// Verifies how updateRelay handles a subnet entry without relay: no change.
+TEST(AdaptorSubnetTest, updateNoRelay) {
+    string config = "{\n"
+        " \"subnet\": \"192.0.2.0/24\"\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    ASSERT_NO_THROW(AdaptorSubnet::updateRelay(json));
+    EXPECT_TRUE(copied->equals(*json));
+}
+
+// Verifies how updateRelay handles a subnet entry with empty relay:
+// the relay entry is useless and removed.
+TEST(AdaptorSubnetTest, updateEmptyRelay) {
+    string config = "{\n"
+        " \"subnet\": \"192.0.2.0/24\",\n"
+        " \"relay\": { }\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    ASSERT_NO_THROW(AdaptorSubnet::updateRelay(json));
+    EXPECT_FALSE(copied->equals(*json));
+    EXPECT_FALSE(json->get("relay"));
+}
+
+// Verifies how updateRelay handles a subnet entry with relay which
+// has empty addresses: the relay entry is useless and removed.
+TEST(AdaptorSubnetTest, updateRelayEmptyAddresses) {
+    string config = "{\n"
+        " \"subnet\": \"192.0.2.0/24\",\n"
+        " \"relay\": {\n"
+        "     \"ip-addresses\": [ ]\n"
+        " }\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    ASSERT_NO_THROW(AdaptorSubnet::updateRelay(json));
+    EXPECT_FALSE(copied->equals(*json));
+    EXPECT_FALSE(json->get("relay"));
+}
+
+// Verifies how updateRelay handles a subnet entry with relay which
+// has addresses: no change.
+TEST(AdaptorSubnetTest, updateRelayAddresses) {
+    string config = "{\n"
+        " \"subnet\": \"192.0.2.0/24\",\n"
+        " \"relay\": {\n"
+        "     \"ip-addresses\": [ \"192.168.1.1\" ]\n"
+        " }\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    ASSERT_NO_THROW(AdaptorSubnet::updateRelay(json));
+    EXPECT_TRUE(copied->equals(*json));
+}
+
+// Verifies how updateRelay handles a subnet entry with relay which
+// has only address: the address is moved to a new list.
+TEST(AdaptorSubnetTest, updateRelayAddress) {
+    string config = "{\n"
+        " \"subnet\": \"192.0.2.0/24\",\n"
+        " \"relay\": {\n"
+        "     \"ip-address\": \"192.168.1.1\"\n"
+        " }\n"
+        "}";
+    ElementPtr json;
+    ASSERT_NO_THROW(json = Element::fromJSON(config));
+    ConstElementPtr copied = copy(json);
+    ASSERT_NO_THROW(AdaptorSubnet::updateRelay(json));
+    EXPECT_FALSE(copied->equals(*json));
+    ConstElementPtr relay = json->get("relay");
+    ASSERT_TRUE(relay);
+    string expected = "{ \"ip-addresses\": [ \"192.168.1.1\" ] }";
+    EXPECT_EQ(expected, relay->str());
+}
+
+// It does not make sense to have both ip-address and ip-addresses...
+
+}; // end of anonymous namespace