1 // Copyright (C) 2019-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 <arpa/inet.h>
10 #include <gtest/gtest.h>
11
12 #include <database/backend_selector.h>
13 #include <dhcp/tests/iface_mgr_test_config.h>
14 #include <dhcp4/dhcp4_srv.h>
15 #include <dhcp4/ctrl_dhcp4_srv.h>
16 #include <dhcp4/json_config_parser.h>
17 #include <dhcpsrv/subnet.h>
18 #include <dhcpsrv/cfgmgr.h>
19 #include <dhcpsrv/cfg_subnets4.h>
20 #include <dhcpsrv/testutils/generic_backend_unittest.h>
21 #include <dhcpsrv/testutils/test_config_backend_dhcp4.h>
22
23 #include "dhcp4_test_utils.h"
24 #include "get_config_unittest.h"
25
26 #include <boost/foreach.hpp>
27 #include <boost/scoped_ptr.hpp>
28
29 #include <iostream>
30 #include <fstream>
31 #include <sstream>
32 #include <limits.h>
33
34 using namespace isc::asiolink;
35 using namespace isc::config;
36 using namespace isc::data;
37 using namespace isc::dhcp;
38 using namespace isc::dhcp::test;
39 using namespace isc::db;
40 using namespace std;
41
42 namespace {
43
44 /// @brief Test fixture for testing external configuration merging
45 class Dhcp4CBTest : public GenericBackendTest {
46 protected:
47 /// @brief Pre test set up
48 /// Called prior to each test. It creates two configuration backends
49 /// that differ by host name ("db1" and "db2"). It then registers
50 /// a backend factory that will return them rather than create
51 /// new instances. The backends need to pre-exist so they can be
52 /// populated prior to calling server configure. It uses
53 /// TestConfigBackend instances but with a type of "memfile" to pass
54 /// parsing. Doing it all here allows us to use ASSERTs if we feel like
55 /// it.
SetUp()56 virtual void SetUp() {
57 DatabaseConnection::ParameterMap params;
58 params[std::string("type")] = std::string("memfile");
59 params[std::string("host")] = std::string("db1");
60 db1_.reset(new TestConfigBackendDHCPv4(params));
61
62 params[std::string("host")] = std::string("db2");
63 db2_.reset(new TestConfigBackendDHCPv4(params));
64
65 ConfigBackendDHCPv4Mgr::instance().registerBackendFactory("memfile",
66 [this](const DatabaseConnection::ParameterMap& params)
67 -> ConfigBackendDHCPv4Ptr {
68 auto host = params.find("host");
69 if (host != params.end()) {
70 if (host->second == "db1") {
71 return (db1_);
72 } else if (host->second == "db2") {
73 return (db2_);
74 }
75 }
76
77 // Apparently we're looking for a new one.
78 return (TestConfigBackendDHCPv4Ptr(new TestConfigBackendDHCPv4(params)));
79 });
80 }
81
82 /// @brief Clean up after each test
TearDown()83 virtual void TearDown() {
84 // Unregister the factory to be tidy.
85 ConfigBackendDHCPv4Mgr::instance().unregisterBackendFactory("memfile");
86 }
87
88 public:
89
90 /// Constructor
Dhcp4CBTest()91 Dhcp4CBTest()
92 : rcode_(-1), db1_selector("db1"), db2_selector("db1") {
93 // Open port 0 means to not do anything at all. We don't want to
94 // deal with sockets here, just check if configuration handling
95 // is sane.
96 srv_.reset(new ControlledDhcpv4Srv(0));
97
98 // Create fresh context.
99 resetConfiguration();
100 }
101
102 /// Destructor
~Dhcp4CBTest()103 virtual ~Dhcp4CBTest() {
104 resetConfiguration();
105 };
106
107 /// @brief Reset configuration singletons.
resetConfiguration()108 void resetConfiguration() {
109 CfgMgr::instance().clear();
110 ConfigBackendDHCPv4Mgr::destroy();
111 }
112
113 /// @brief Convenience method for running configuration
114 ///
115 /// This method does not throw, but signals errors using gtest macros.
116 ///
117 /// @param config text to be parsed as JSON
118 /// @param expected_code expected code (see cc/command_interpreter.h)
119 /// @param exp_error expected text error (check skipped if empty)
configure(std::string config,int expected_code,std::string exp_error="")120 void configure(std::string config, int expected_code,
121 std::string exp_error = "") {
122 ConstElementPtr json;
123 try {
124 json = parseDHCP4(config, true);
125 } catch(const std::exception& ex) {
126 ADD_FAILURE() << "parseDHCP4 failed: " << ex.what();
127 }
128
129 ConstElementPtr status;
130 ASSERT_NO_THROW(status = configureDhcp4Server(*srv_, json));
131 ASSERT_TRUE(status);
132
133 int rcode;
134 ConstElementPtr comment = parseAnswer(rcode, status);
135 ASSERT_EQ(expected_code, rcode) << " comment: "
136 << comment->stringValue();
137
138 string text;
139 ASSERT_NO_THROW(text = comment->stringValue());
140
141 if (expected_code != rcode) {
142 std::cout << "Reported status: " << text << std::endl;
143 }
144
145 if ((rcode != 0)) {
146 if (!exp_error.empty()) {
147 ASSERT_EQ(exp_error, text);
148 }
149 }
150 }
151
152 boost::scoped_ptr<Dhcpv4Srv> srv_; ///< DHCP4 server under test
153 int rcode_; ///< Return code from element parsing
154 ConstElementPtr comment_; ///< Reason for parse fail
155
156 BackendSelector db1_selector; ///< BackendSelector by host for first config backend
157 BackendSelector db2_selector; ///< BackendSelector by host for second config backend
158
159 TestConfigBackendDHCPv4Ptr db1_; ///< First configuration backend instance
160 TestConfigBackendDHCPv4Ptr db2_; ///< Second configuration backend instance
161 };
162
163 // This test verifies that externally configured globals are
164 // merged correctly into staging configuration.
TEST_F(Dhcp4CBTest,mergeGlobals)165 TEST_F(Dhcp4CBTest, mergeGlobals) {
166 string base_config =
167 "{ \n"
168 " \"interfaces-config\": { \n"
169 " \"interfaces\": [\"*\" ] \n"
170 " }, \n"
171 " \"echo-client-id\": false, \n"
172 " \"decline-probation-period\": 7000, \n"
173 " \"valid-lifetime\": 1000, \n"
174 " \"rebind-timer\": 800, \n"
175 " \"server-hostname\": \"overwrite.me.com\", \n"
176 " \"config-control\": { \n"
177 " \"config-databases\": [ { \n"
178 " \"type\": \"memfile\", \n"
179 " \"host\": \"db1\" \n"
180 " },{ \n"
181 " \"type\": \"memfile\", \n"
182 " \"host\": \"db2\" \n"
183 " } \n"
184 " ] \n"
185 " } \n"
186 "} \n";
187
188 extractConfig(base_config);
189
190 // Make some globals:
191 StampedValuePtr server_hostname(new StampedValue("server-hostname", "isc.example.org"));
192 StampedValuePtr decline_period(new StampedValue("decline-probation-period", Element::create(86400)));
193 StampedValuePtr calc_tee_times(new StampedValue("calculate-tee-times", Element::create(bool(false))));
194 StampedValuePtr t2_percent(new StampedValue("t2-percent", Element::create(0.75)));
195 StampedValuePtr renew_timer(new StampedValue("renew-timer", Element::create(500)));
196
197 // Let's add all of the globals to the second backend. This will verify
198 // we find them there.
199 db2_->createUpdateGlobalParameter4(ServerSelector::ALL(), server_hostname);
200 db2_->createUpdateGlobalParameter4(ServerSelector::ALL(), decline_period);
201 db2_->createUpdateGlobalParameter4(ServerSelector::ALL(), calc_tee_times);
202 db2_->createUpdateGlobalParameter4(ServerSelector::ALL(), t2_percent);
203 db2_->createUpdateGlobalParameter4(ServerSelector::ALL(), renew_timer);
204
205 // Should parse and merge without error.
206 ASSERT_NO_FATAL_FAILURE(configure(base_config, CONTROL_RESULT_SUCCESS, ""));
207
208 // Verify the composite staging is correct. (Remember that
209 // CfgMgr::instance().commit() hasn't been called)
210 SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();
211
212 // echo-client-id is set explicitly in the original config, meanwhile
213 // the backend config does not set it, so the explicit value wins.
214 EXPECT_FALSE(staging_cfg->getEchoClientId());
215
216 // decline-probation-period is an explicit member that should come
217 // from the backend.
218 EXPECT_EQ(86400, staging_cfg->getDeclinePeriod());
219
220 // Verify that the implicit globals from JSON are there.
221 ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, "valid-lifetime",
222 Element::create(1000)));
223 ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, "rebind-timer",
224 Element::create(800)));
225
226 // Verify that the implicit globals from the backend are there.
227 ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, server_hostname));
228 ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, calc_tee_times));
229 ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, t2_percent));
230 ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, renew_timer));
231 }
232
233 // This test verifies that externally configured option definitions
234 // merged correctly into staging configuration.
TEST_F(Dhcp4CBTest,mergeOptionDefs)235 TEST_F(Dhcp4CBTest, mergeOptionDefs) {
236 string base_config =
237 "{ \n"
238 " \"option-def\": [ { \n"
239 " \"name\": \"one\", \n"
240 " \"code\": 1, \n"
241 " \"type\": \"ipv4-address\", \n"
242 " \"space\": \"isc\" \n"
243 " }, \n"
244 " { \n"
245 " \"name\": \"two\", \n"
246 " \"code\": 2, \n"
247 " \"type\": \"string\", \n"
248 " \"space\": \"isc\" \n"
249 " } \n"
250 " ], \n"
251 " \"config-control\": { \n"
252 " \"config-databases\": [ { \n"
253 " \"type\": \"memfile\", \n"
254 " \"host\": \"db1\" \n"
255 " },{ \n"
256 " \"type\": \"memfile\", \n"
257 " \"host\": \"db2\" \n"
258 " } \n"
259 " ] \n"
260 " } \n"
261 "} \n";
262
263 extractConfig(base_config);
264
265 // Create option one replacement and add it to first backend.
266 OptionDefinitionPtr def;
267 def.reset(new OptionDefinition("one", 101, "isc", "uint16"));
268 db1_->createUpdateOptionDef4(ServerSelector::ALL(), def);
269
270 // Create option three and add it to first backend.
271 def.reset(new OptionDefinition("three", 3, "isc", "string"));
272 db1_->createUpdateOptionDef4(ServerSelector::ALL(), def);
273
274 // Create option four and add it to second backend.
275 def.reset(new OptionDefinition("four", 4, "isc", "string"));
276 db2_->createUpdateOptionDef4(ServerSelector::ALL(), def);
277
278 // Should parse and merge without error.
279 ASSERT_NO_FATAL_FAILURE(configure(base_config, CONTROL_RESULT_SUCCESS, ""));
280
281 // Verify the composite staging is correct.
282 SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();
283 ConstCfgOptionDefPtr option_defs = staging_cfg->getCfgOptionDef();
284
285 // Definition "one" from first backend should be there.
286 OptionDefinitionPtr found_def = option_defs->get("isc", "one");
287 ASSERT_TRUE(found_def);
288 EXPECT_EQ(101, found_def->getCode());
289 EXPECT_EQ(OptionDataType::OPT_UINT16_TYPE, found_def->getType());
290
291 // Definition "two" from JSON config should be there.
292 found_def = option_defs->get("isc", "two");
293 ASSERT_TRUE(found_def);
294 EXPECT_EQ(2, found_def->getCode());
295
296 // Definition "three" from first backend should be there.
297 found_def = option_defs->get("isc", "three");
298 ASSERT_TRUE(found_def);
299 EXPECT_EQ(3, found_def->getCode());
300
301 // Definition "four" from first backend should not be there.
302 found_def = option_defs->get("isc", "four");
303 ASSERT_FALSE(found_def);
304 }
305
306 // This test verifies that externally configured options
307 // merged correctly into staging configuration.
TEST_F(Dhcp4CBTest,mergeOptions)308 TEST_F(Dhcp4CBTest, mergeOptions) {
309 string base_config =
310 "{ \n"
311 " \"option-data\": [ { \n"
312 " \"name\": \"dhcp-message\", \n"
313 " \"data\": \"0A0B0C0D\", \n"
314 " \"csv-format\": false \n"
315 " },{ \n"
316 " \"name\": \"host-name\", \n"
317 " \"data\": \"old.example.com\", \n"
318 " \"csv-format\": true \n"
319 " } \n"
320 " ], \n"
321 " \"config-control\": { \n"
322 " \"config-databases\": [ { \n"
323 " \"type\": \"memfile\", \n"
324 " \"host\": \"db1\" \n"
325 " },{ \n"
326 " \"type\": \"memfile\", \n"
327 " \"host\": \"db2\" \n"
328 " } \n"
329 " ] \n"
330 " } \n"
331 "} \n";
332
333 extractConfig(base_config);
334
335 OptionDescriptorPtr opt;
336
337 // Add host-name to the first backend.
338 opt.reset(new OptionDescriptor(
339 createOption<OptionString>(Option::V4, DHO_HOST_NAME,
340 true, false, "new.example.com")));
341 opt->space_name_ = DHCP4_OPTION_SPACE;
342 db1_->createUpdateOption4(ServerSelector::ALL(), opt);
343
344 // Add boot-file-name to the first backend.
345 opt.reset(new OptionDescriptor(
346 createOption<OptionString>(Option::V4, DHO_BOOT_FILE_NAME,
347 true, false, "my-boot-file")));
348 opt->space_name_ = DHCP4_OPTION_SPACE;
349 db1_->createUpdateOption4(ServerSelector::ALL(), opt);
350
351 // Add boot-file-name to the second backend.
352 opt.reset(new OptionDescriptor(
353 createOption<OptionString>(Option::V4, DHO_BOOT_FILE_NAME,
354 true, false, "your-boot-file")));
355 opt->space_name_ = DHCP4_OPTION_SPACE;
356 db2_->createUpdateOption4(ServerSelector::ALL(), opt);
357
358 // Should parse and merge without error.
359 ASSERT_NO_FATAL_FAILURE(configure(base_config, CONTROL_RESULT_SUCCESS, ""));
360
361 // Verify the composite staging is correct.
362 SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();
363
364 // Option definition from JSON should be there.
365 CfgOptionPtr options = staging_cfg->getCfgOption();
366
367 // dhcp-message should come from the original config.
368 OptionDescriptor found_opt =
369 options->get(DHCP4_OPTION_SPACE, DHO_DHCP_MESSAGE);
370 ASSERT_TRUE(found_opt.option_);
371 EXPECT_EQ("0x0A0B0C0D", found_opt.option_->toHexString());
372
373 // host-name should come from the first back end,
374 // (overwriting the original).
375 found_opt = options->get(DHCP4_OPTION_SPACE, DHO_HOST_NAME);
376 ASSERT_TRUE(found_opt.option_);
377 EXPECT_EQ("new.example.com", found_opt.option_->toString());
378
379 // booth-file-name should come from the first back end.
380 found_opt = options->get(DHCP4_OPTION_SPACE, DHO_BOOT_FILE_NAME);
381 ASSERT_TRUE(found_opt.option_);
382 EXPECT_EQ("my-boot-file", found_opt.option_->toString());
383 }
384
385 // This test verifies that externally configured shared-networks are
386 // merged correctly into staging configuration.
TEST_F(Dhcp4CBTest,mergeSharedNetworks)387 TEST_F(Dhcp4CBTest, mergeSharedNetworks) {
388 string base_config =
389 "{ \n"
390 " \"interfaces-config\": { \n"
391 " \"interfaces\": [\"*\" ] \n"
392 " }, \n"
393 " \"valid-lifetime\": 4000, \n"
394 " \"config-control\": { \n"
395 " \"config-databases\": [ { \n"
396 " \"type\": \"memfile\", \n"
397 " \"host\": \"db1\" \n"
398 " },{ \n"
399 " \"type\": \"memfile\", \n"
400 " \"host\": \"db2\" \n"
401 " } \n"
402 " ] \n"
403 " }, \n"
404 " \"shared-networks\": [ { \n"
405 " \"name\": \"two\" \n"
406 " }] \n"
407 "} \n";
408
409 extractConfig(base_config);
410
411 // Make a few networks
412 SharedNetwork4Ptr network1(new SharedNetwork4("one"));
413 SharedNetwork4Ptr network3(new SharedNetwork4("three"));
414
415 // Add network1 to db1 and network3 to db2
416 db1_->createUpdateSharedNetwork4(ServerSelector::ALL(), network1);
417 db2_->createUpdateSharedNetwork4(ServerSelector::ALL(), network3);
418
419 // Should parse and merge without error.
420 ASSERT_NO_FATAL_FAILURE(configure(base_config, CONTROL_RESULT_SUCCESS, ""));
421
422 // Verify the composite staging is correct. (Remember that
423 // CfgMgr::instance().commit() hasn't been called)
424 SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();
425
426 CfgSharedNetworks4Ptr networks = staging_cfg->getCfgSharedNetworks4();
427 SharedNetwork4Ptr staged_network;
428
429 // SharedNetwork One should have been added from db1 config
430 staged_network = networks->getByName("one");
431 ASSERT_TRUE(staged_network);
432
433 // Subnet2 should have come from the json config
434 staged_network = networks->getByName("two");
435 ASSERT_TRUE(staged_network);
436
437 // Subnet3, which is in db2 should not have been merged.
438 // We queried db1 first and the query returned data. In
439 // other words, we iterate over the backends, asking for
440 // data. We use the first data, we find.
441 staged_network = networks->getByName("three");
442 ASSERT_FALSE(staged_network);
443 }
444
445 // This test verifies that externally configured subnets are
446 // merged correctly into staging configuration.
TEST_F(Dhcp4CBTest,mergeSubnets)447 TEST_F(Dhcp4CBTest, mergeSubnets) {
448 string base_config =
449 "{ \n"
450 " \"interfaces-config\": { \n"
451 " \"interfaces\": [\"*\" ] \n"
452 " }, \n"
453 " \"valid-lifetime\": 4000, \n"
454 " \"config-control\": { \n"
455 " \"config-databases\": [ { \n"
456 " \"type\": \"memfile\", \n"
457 " \"host\": \"db1\" \n"
458 " },{ \n"
459 " \"type\": \"memfile\", \n"
460 " \"host\": \"db2\" \n"
461 " } \n"
462 " ] \n"
463 " }, \n"
464 " \"subnet4\": [ \n"
465 " { \n"
466 " \"id\": 2,\n"
467 " \"subnet\": \"192.0.3.0/24\" \n"
468 " } ]\n"
469 "} \n";
470
471 extractConfig(base_config);
472
473 // Make a few subnets
474 Subnet4Ptr subnet1(new Subnet4(IOAddress("192.0.2.0"), 26, 1, 2, 3, SubnetID(1)));
475 Subnet4Ptr subnet3(new Subnet4(IOAddress("192.0.4.0"), 26, 1, 2, 3, SubnetID(3)));
476
477 // Add subnet1 to db1 and subnet3 to db2
478 db1_->createUpdateSubnet4(ServerSelector::ALL(), subnet1);
479 db2_->createUpdateSubnet4(ServerSelector::ALL(), subnet3);
480
481 // Should parse and merge without error.
482 configure(base_config, CONTROL_RESULT_SUCCESS, "");
483
484 // Verify the composite staging is correct. (Remember that
485 // CfgMgr::instance().commit() hasn't been called)
486
487 SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();
488
489 CfgSubnets4Ptr subnets = staging_cfg->getCfgSubnets4();
490 Subnet4Ptr staged_subnet;
491
492 // Subnet1 should have been added from db1 config
493 staged_subnet = subnets->getSubnet(1);
494 ASSERT_TRUE(staged_subnet);
495
496 // Subnet2 should have come from the json config
497 staged_subnet = subnets->getSubnet(2);
498 ASSERT_TRUE(staged_subnet);
499
500 // Subnet3, which is in db2 should not have been merged, since it is
501 // first found, first used?
502 staged_subnet = subnets->getSubnet(3);
503 ASSERT_FALSE(staged_subnet);
504 }
505
506 }
507