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