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