1 // Copyright (C) 2018-2021 Internet Systems Consortium, Inc. ("ISC")
2 //
3 // This Source Code Form is subject to the terms of the Mozilla Public
4 // License, v. 2.0. If a copy of the MPL was not distributed with this
5 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 
7 #include <config.h>
8 
9 #include <testutils/io_utils.h>
10 #include <testutils/user_context_utils.h>
11 #include <yang/translator_config.h>
12 #include <yang/yang_models.h>
13 #include <yang/tests/json_configs.h>
14 #include <yang/tests/yang_configs.h>
15 #include <yang/tests/sysrepo_setup.h>
16 
17 #include <boost/algorithm/string.hpp>
18 
19 #include <gtest/gtest.h>
20 
21 #include <iostream>
22 
23 using namespace std;
24 using namespace isc;
25 using namespace isc::data;
26 using namespace isc::yang;
27 using namespace isc::yang::test;
28 using namespace sysrepo;
29 
30 namespace {
31 
32 /// @brief Return the difference between two strings
33 ///
34 /// Use the gtest >= 1.8.0 tool which builds the difference between
35 /// two vectors of lines.
36 ///
37 /// @param left left string
38 /// @param right right string
39 /// @return the unified diff between left and right
40 #ifdef HAVE_CREATE_UNIFIED_DIFF
generateDiff(std::string left,std::string right)41 std::string generateDiff(std::string left, std::string right) {
42     std::vector<std::string> left_lines;
43     boost::split(left_lines, left, boost::is_any_of("\n"));
44     std::vector<std::string> right_lines;
45     boost::split(right_lines, right, boost::is_any_of("\n"));
46     using namespace testing::internal;
47     return (edit_distance::CreateUnifiedDiff(left_lines, right_lines));
48 }
49 #else
50 std::string generateDiff(std::string, std::string) {
51     return ("");
52 }
53 #endif
54 
55 /// @brief Test Fixture class for Yang <-> JSON configs.
56 class ConfigTest : public ::testing::Test {
57 public:
58     virtual ~ConfigTest() = default;
59 
SetUp()60     void SetUp() override {
61         SysrepoSetup::cleanSharedMemory();
62         connection_ = std::make_shared<Connection>();
63         session_.reset(new Session(connection_, SR_DS_CANDIDATE));
64         translator_.reset(new TranslatorBasic(session_, model_));
65         cleanModelData();
66     }
67 
TearDown()68     void TearDown() override {
69         cleanModelData();
70         translator_.reset();
71         session_.reset();
72         connection_.reset();
73         SysrepoSetup::cleanSharedMemory();
74     }
75 
cleanModelData()76     void cleanModelData() {
77         std::string toplevel_node("config");
78         if (model_ == IETF_DHCPV6_SERVER) {
79             toplevel_node = "server";
80         }
81         translator_->delItem("/" + model_ + ":" + toplevel_node);
82     }
83 
84     /// @brief Reset session.
resetSession()85     void resetSession() {
86         SetUp();
87     }
88 
89     /// @brief Loads YANG configuration from specified tree.
90     ///
91     /// @param tree The Yang tree to load.
load(const YRTree & tree)92     void load(const YRTree& tree) {
93         YangRepr repr(model_);
94         repr.set(tree, session_);
95     }
96 
97     /// @brief Loads JSON configuration from specified Element tree.
98     ///
99     /// @param json The JSON tree to load.
load(ConstElementPtr json)100     void load(ConstElementPtr json) {
101         TranslatorConfig tc(session_, model_);
102         tc.setConfig(json);
103     }
104 
105     /// @brief Load a cofiguration from a string containing JSON.
106     ///
107     /// @param config The JSON tree to load in textual format.
load(const string & config)108     void load(const string& config) {
109         ElementPtr json;
110         ASSERT_NO_THROW_LOG(json = Element::fromJSON(config));
111         load(json);
112     }
113 
114     /// @brief Load a configuration from JSON file.
115     ///
116     /// @param filename The name of the JSON file to load,
loadFile(const string & filename)117     ConstElementPtr loadFile(const string& filename) {
118         string decommented = isc::test::decommentJSONfile(filename);
119         ConstElementPtr json = Element::fromJSONFile(decommented, true);
120         ::remove(decommented.c_str());
121         load(json);
122         return (json);
123     }
124 
125     /// @brief Returns YANG tree configuration.
getYang()126     YRTree getYang() {
127         YangRepr repr(model_);
128         return (repr.get(session_));
129     }
130 
131     /// @brief Returns configuration in JSON (translated by TranslatorConfig)
getJSON()132     ConstElementPtr getJSON() {
133         TranslatorConfig tc(session_, model_);
134         return (tc.getConfig());
135     }
136 
137     /// @brief Retrieves configuration as text (in pretty JSON format).
getText()138     string getText() {
139         return (isc::data::prettyPrint(getJSON()));
140     }
141 
142     /// @brief Verify Yang.
143     ///
144     /// @param expected The expected Yang tree.
verify(const YRTree & expected)145     bool verify(const YRTree& expected) {
146         YangRepr repr(model_);
147         return (repr.verify(expected, session_, cerr));
148     }
149 
150     /// @brief Verify JSON.
151     ///
152     /// @param expected The expected JSON tree.
verify(ConstElementPtr expected)153     bool verify(ConstElementPtr expected) {
154         TranslatorConfig tc(session_, model_);
155         ConstElementPtr content = tc.getConfig();
156         if (isEquivalent(expected, content)) {
157             return (true);
158         }
159         string wanted = prettyPrint(expected);
160         string got = prettyPrint(content);
161         cerr << "Expected:\n" << wanted << "\n"
162              << "Actual:\n" << got
163 #ifdef HAVE_CREATE_UNIFIED_DIFF
164              << "\nDiff:\n" << generateDiff(wanted, got)
165 #endif
166              << "\n";
167         return (false);
168     }
169 
170     /// @brief Verify JSON.
171     ///
172     /// @param expected The expected JSON tree in textual format.
verify(const string & config)173     bool verify(const string& config) {
174         ConstElementPtr expected;
175         expected = Element::fromJSON(config);
176         return (verify(expected));
177     }
178 
179     /// @brief Validate.
180     ///
181     /// @note A tree must be loaded first.
182     ///
validate()183     bool validate() {
184         YangRepr repr(model_);
185         return (repr.validate(session_, cerr));
186     }
187 
188     /// @brief The model.
189     string model_;
190 
191     /// @brief The sysrepo connection.
192     S_Connection connection_;
193 
194     /// @brief The sysrepo session.
195     S_Session session_;
196 
197     std::unique_ptr<TranslatorBasic> translator_;
198 };
199 
200 struct ConfigTestKeaV4 : ConfigTest {
ConfigTestKeaV4__anone0204bf50111::ConfigTestKeaV4201     ConfigTestKeaV4() {
202         model_ = KEA_DHCP4_SERVER;
203     }
204 };
205 struct ConfigTestKeaV6 : ConfigTest {
ConfigTestKeaV6__anone0204bf50111::ConfigTestKeaV6206     ConfigTestKeaV6() {
207         model_ = KEA_DHCP6_SERVER;
208     }
209 };
210 struct ConfigTestIetfV6 : ConfigTest {
ConfigTestIetfV6__anone0204bf50111::ConfigTestIetfV6211     ConfigTestIetfV6() {
212         model_ = IETF_DHCPV6_SERVER;
213     }
214 };
215 
216 // Check empty config with ietf-dhcpv6-server model.
TEST_F(ConfigTestIetfV6,emptyIetf6)217 TEST_F(ConfigTestIetfV6, emptyIetf6) {
218     YRTree tree;
219     ASSERT_NO_THROW_LOG(load(tree));
220     EXPECT_TRUE(verify(tree));
221 
222     ConstElementPtr json = Element::fromJSON(emptyJson6);
223     EXPECT_TRUE(verify(json));
224     ASSERT_NO_THROW_LOG(load(json));
225     EXPECT_TRUE(verify(emptyJson6));
226     EXPECT_TRUE(verify(tree));
227 }
228 
229 // Check empty config with kea-dhcp4-server:config model.
TEST_F(ConfigTestKeaV4,emptyKeaDhcp4)230 TEST_F(ConfigTestKeaV4, emptyKeaDhcp4) {
231     YRTree tree;
232     ASSERT_NO_THROW_LOG(load(tree));
233     EXPECT_TRUE(verify(emptyTreeKeaDhcp4));
234 
235     ConstElementPtr json = Element::fromJSON(emptyJson4);
236     EXPECT_TRUE(verify(json));
237     ASSERT_NO_THROW_LOG(load(json));
238     EXPECT_TRUE(verify(emptyJson4));
239     EXPECT_TRUE(verify(emptyTreeKeaDhcp4));
240 }
241 
242 // Check empty config with kea-dhcp6-server:config model.
TEST_F(ConfigTestKeaV6,emptyKeaDhcp6)243 TEST_F(ConfigTestKeaV6, emptyKeaDhcp6) {
244     YRTree tree;
245     ASSERT_NO_THROW_LOG(load(tree));
246     EXPECT_TRUE(verify(emptyTreeKeaDhcp6));
247 
248     ConstElementPtr json = Element::fromJSON(emptyJson6);
249     EXPECT_TRUE(verify(json));
250     ASSERT_NO_THROW_LOG(load(json));
251     EXPECT_TRUE(verify(emptyJson6));
252     EXPECT_TRUE(verify(emptyTreeKeaDhcp6));
253 }
254 
255 // Check subnet with two pools with ietf-dhcpv6-server model.
256 // Validation will fail because the current model has a vendor-info
257 // container with a mandatory ent-num leaf and no presence flag,
258 // and of course the candidate YANG tree has nothing for this.
TEST_F(ConfigTestIetfV6,subnetTwoPoolsIetf6)259 TEST_F(ConfigTestIetfV6, subnetTwoPoolsIetf6) {
260     ASSERT_NO_THROW_LOG(load(subnetTwoPoolsTreeIetf6));
261     EXPECT_TRUE(verify(subnetTwoPoolsJson6));
262 
263     resetSession();
264 
265     ASSERT_NO_THROW_LOG(load(subnetTwoPoolsJson6));
266     EXPECT_TRUE(verify(subnetTwoPoolsTreeIetf6));
267 
268     EXPECT_FALSE(validate());
269 }
270 
271 // Check subnet with a pool and option data lists with
272 // kea-dhcp4-server:config model.
TEST_F(ConfigTestKeaV4,subnetOptionsKeaDhcp4)273 TEST_F(ConfigTestKeaV4, subnetOptionsKeaDhcp4) {
274     ASSERT_NO_THROW_LOG(load(subnetOptionsTreeKeaDhcp4));
275     EXPECT_TRUE(verify(subnetOptionsJson4));
276 
277     resetSession();
278 
279     ASSERT_NO_THROW_LOG(load(subnetOptionsJson4));
280     EXPECT_TRUE(verify(subnetOptionsTreeKeaDhcp4));
281 
282     EXPECT_TRUE(validate());
283 }
284 
285 // Check subnet with a pool and option data lists with
286 // kea-dhcp6-server:config model.
TEST_F(ConfigTestKeaV6,subnetOptionsKeaDhcp6)287 TEST_F(ConfigTestKeaV6, subnetOptionsKeaDhcp6) {
288     ASSERT_NO_THROW_LOG(load(subnetOptionsTreeKeaDhcp6));
289     EXPECT_TRUE(verify(subnetOptionsJson6));
290 
291     resetSession();
292 
293     ASSERT_NO_THROW_LOG(load(subnetOptionsJson6));
294     EXPECT_TRUE(verify(subnetOptionsTreeKeaDhcp6));
295 
296     EXPECT_TRUE(validate());
297 }
298 
299 // Check with timers.
TEST_F(ConfigTestIetfV6,subnetTimersIetf6)300 TEST_F(ConfigTestIetfV6, subnetTimersIetf6) {
301     ASSERT_NO_THROW_LOG(load(subnetTimersIetf6));
302     EXPECT_TRUE(verify(subnetTimersJson6));
303 
304     resetSession();
305 
306     ASSERT_NO_THROW_LOG(load(subnetTimersJson6));
307     EXPECT_TRUE(verify(subnetTimersIetf6));
308 }
309 
310 // Check a ietf-dhcpv6-server configuration which validates.
TEST_F(ConfigTestIetfV6,validateIetf6)311 TEST_F(ConfigTestIetfV6, validateIetf6) {
312     ASSERT_NO_THROW_LOG(load(validTreeIetf6));
313     EXPECT_TRUE(verify(validTreeIetf6));
314 
315     // If this validation fails, make sure you have the model *and its
316     // dependencies* are installed. Note when you install ietf-dhcpv6-server
317     // module, its dependencies are semi-installed, which is not sufficient.
318     // This can be detected in output of sysrepoctl -l.
319     // Note the ietf-interfaces module. The conformance status must be
320     // "installed". "implemented" (installed as dependency) is not enough.
321     EXPECT_TRUE(validate());
322 }
323 
324 // Check Kea4 example files.
TEST_F(ConfigTestKeaV4,examples4)325 TEST_F(ConfigTestKeaV4, examples4) {
326     vector<string> examples = {
327         "advanced.json",
328         "all-keys-netconf.json",
329         "backends.json",
330         "cassandra.json",
331         "classify.json",
332         "classify2.json",
333         "comments.json",
334         "config-backend.json",
335         "dhcpv4-over-dhcpv6.json",
336         // "global-reservations.json" removed for #1405
337         "ha-load-balancing-primary.json",
338         "hooks.json",
339         "hooks-radius.json",
340         "leases-expiration.json",
341         "multiple-options.json",
342         "mysql-reservations.json",
343         "pgsql-reservations.json",
344         // "reservations.json" removed for #1405
345         "several-subnets.json",
346         // "shared-network.json" removed for #1405
347         "single-subnet.json",
348         // "with-ddns.json" removed for #35
349     };
350     for (string file : examples) {
351         resetSession();
352         string path = string(CFG_EXAMPLES) + "/kea4/" + file;
353         cout << "Testing file " << path << endl;
354         ConstElementPtr json;
355         ASSERT_NO_THROW_LOG(json = loadFile(path));
356         json = isc::test::moveComments(json);
357         EXPECT_TRUE(verify(json));
358         EXPECT_TRUE(validate());
359     }
360 }
361 
362 // Check Kea6 example files.
TEST_F(ConfigTestKeaV6,examples6)363 TEST_F(ConfigTestKeaV6, examples6) {
364     vector<string> examples = {
365         "advanced.json",
366         "all-keys-netconf.json",
367         "backends.json",
368         "cassandra.json",
369         "classify.json",
370         "classify2.json",
371         "comments.json",
372         "config-backend.json",
373         "dhcpv4-over-dhcpv6.json",
374         "duid.json",
375         // "global-reservations.json" removed for #1405
376         "ha-hot-standby.json",
377         "hooks.json",
378         "iPXE.json",
379         "leases-expiration.json",
380         "multiple-options.json",
381         "mysql-reservations.json",
382         "pgsql-reservations.json",
383         // "reservations.json" removed for #1405
384         "several-subnets.json",
385         // "shared-network.json" removed for #1405
386         "simple.json",
387         "softwire46.json",
388         "stateless.json",
389         "tee-times.json",
390         // "with-ddns.json" removed for #35
391     };
392     for (string file : examples) {
393         resetSession();
394         string path = string(CFG_EXAMPLES) + "/kea6/" + file;
395         cout << "Testing file " << path << endl;
396         ConstElementPtr json;
397         ASSERT_NO_THROW_LOG(json = loadFile(path));
398         json = isc::test::moveComments(json);
399         EXPECT_TRUE(verify(json));
400         EXPECT_TRUE(validate());
401     }
402 }
403 
404 // Check the example in the design document.
TEST_F(ConfigTestIetfV6,designExample)405 TEST_F(ConfigTestIetfV6, designExample) {
406     ASSERT_NO_THROW_LOG(load(designExampleTree));
407     EXPECT_TRUE(verify(designExampleJson));
408 
409     resetSession();
410 
411     ASSERT_NO_THROW_LOG(load(designExampleJson));
412     EXPECT_TRUE(verify(designExampleTree));
413 }
414 
415 }  // namespace
416