config_parser_unittest.cc 179 KB
Newer Older
1
// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
2
//
3 4 5
// 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/.
6 7

#include <config.h>
8

9
#include <cc/command_interpreter.h>
Tomek Mrugalski's avatar
Tomek Mrugalski committed
10
#include <config/module_spec.h>
11 12
#include <dhcp/libdhcp++.h>
#include <dhcp/option6_ia.h>
Tomek Mrugalski's avatar
Tomek Mrugalski committed
13
#include <dhcp/iface_mgr.h>
14 15
#include <dhcp/option_custom.h>
#include <dhcp/option_int.h>
16
#include <dhcp/option6_addrlst.h>
17
#include <dhcp/tests/iface_mgr_test_config.h>
18
#include <dhcp6/json_config_parser.h>
19
#include <dhcp6/dhcp6_srv.h>
20
#include <dhcpsrv/addr_utilities.h>
21
#include <dhcpsrv/cfgmgr.h>
22
#include <dhcpsrv/cfg_expiration.h>
23
#include <dhcpsrv/cfg_hosts.h>
24
#include <dhcpsrv/subnet.h>
25
#include <dhcpsrv/subnet_selector.h>
26
#include <dhcpsrv/testutils/config_result_check.h>
27
#include <hooks/hooks_manager.h>
28
#include <defaults.h>
29

30
#include "test_data_files_config.h"
31 32
#include "test_libraries.h"
#include "marker_file.h"
33
#include "dhcp6_test_utils.h"
34

35
#include <boost/foreach.hpp>
36
#include <gtest/gtest.h>
37

38 39
#include <fstream>
#include <iostream>
40
#include <fstream>
41
#include <sstream>
42 43
#include <string>
#include <vector>
44

45
#include <arpa/inet.h>
46
#include <unistd.h>
47 48 49 50

using namespace isc;
using namespace isc::asiolink;
using namespace isc::config;
51 52
using namespace isc::data;
using namespace isc::dhcp;
53
using namespace isc::dhcp::test;
54
using namespace isc::hooks;
55
using namespace std;
56 57 58

namespace {

59
const char* PARSER_CONFIGS[] = {
60
    // CONFIGURATION 0: one subnet with one pool, no user contexts
61
    "{"
62
    "    \"interfaces-config\": {"
63 64 65
    "        \"interfaces\": [\"*\" ]"
    "    },"
    "    \"valid-lifetime\": 4000,"
66
    "    \"preferred-lifetime\": 3000,"
67 68 69 70 71 72 73 74 75
    "    \"rebind-timer\": 2000,"
    "    \"renew-timer\": 1000,"
    "    \"subnet6\": [ {"
    "        \"pools\": [ "
    "            { \"pool\":  \"2001:db8::/64\" }"
    "        ],"
    "        \"subnet\": \"2001:db8::/32\""
    "     } ]"
    "}",
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97

    // Configuration 1: one pool with empty user context
    "{"
    "    \"interfaces-config\": {"
    "        \"interfaces\": [\"*\" ]"
    "    },"
    "    \"valid-lifetime\": 4000,"
    "    \"preferred-lifetime\": 3000,"
    "    \"rebind-timer\": 2000,"
    "    \"renew-timer\": 1000,"
    "    \"subnet6\": [ {"
    "        \"pools\": [ "
    "            { \"pool\":  \"2001:db8::/64\","
    "                \"user-context\": {"
    "                }"
    "            }"
    "        ],"
    "        \"subnet\": \"2001:db8::/32\""
    "     } ]"
    "}",

    // Configuration 2: one pool with user context containing lw4over6 parameters
98
    "{"
99
    "    \"interfaces-config\": {"
100 101 102
    "        \"interfaces\": [\"*\" ]"
    "    },"
    "    \"valid-lifetime\": 4000,"
103
    "    \"preferred-lifetime\": 3000,"
104 105 106 107 108 109
    "    \"rebind-timer\": 2000,"
    "    \"renew-timer\": 1000,"
    "    \"subnet6\": [ {"
    "        \"pools\": [ "
    "            { \"pool\":  \"2001:db8::/64\","
    "                \"user-context\": {"
110 111 112 113
    "                    \"lw4over6-sharing-ratio\": 64,"
    "                    \"lw4over6-v4-pool\": \"192.0.2.0/24\","
    "                    \"lw4over6-sysports-exclude\": true,"
    "                    \"lw4over6-bind-prefix-len\": 56"
114 115 116 117 118
    "                }"
    "            }"
    "        ],"
    "        \"subnet\": \"2001:db8::/32\""
    "     } ]"
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
    "}",

    // Configuration 3: pd-pool without any user-context
    "{"
    "    \"interfaces-config\": {"
    "        \"interfaces\": [\"*\" ]"
    "    },"
    "    \"valid-lifetime\": 4000,"
    "    \"preferred-lifetime\": 3000,"
    "    \"rebind-timer\": 2000,"
    "    \"renew-timer\": 1000,"
    "    \"subnet6\": [ {"
    "        \"pd-pools\": [ "
    "            { \"prefix\": \"2001:db8::\","
    "              \"prefix-len\": 56,"
    "              \"delegated-len\": 64 }"
    "        ],"
    "        \"subnet\": \"2001:db8::/32\""
    "     } ]"
    "}",

    // Configuration 4: pd-pool with empty user-context
    "{"
    "    \"interfaces-config\": {"
    "        \"interfaces\": [\"*\" ]"
    "    },"
    "    \"valid-lifetime\": 4000,"
    "    \"preferred-lifetime\": 3000,"
    "    \"rebind-timer\": 2000,"
    "    \"renew-timer\": 1000,"
    "    \"subnet6\": [ {"
    "        \"pd-pools\": [ "
    "            { \"prefix\":  \"2001:db8::\","
    "              \"prefix-len\": 56,"
    "              \"delegated-len\": 64,"
    "              \"user-context\": { }"
    "            }"
    "        ],"
    "        \"subnet\": \"2001:db8::/32\""
    "     } ]"
    "}",

    // Configuration 5: pd-pool with user-context with lw4over6 parameters
    "{"
    "    \"interfaces-config\": {"
    "        \"interfaces\": [\"*\" ]"
    "    },"
    "    \"valid-lifetime\": 4000,"
    "    \"preferred-lifetime\": 3000,"
    "    \"rebind-timer\": 2000,"
    "    \"renew-timer\": 1000,"
    "    \"subnet6\": [ {"
    "        \"pd-pools\": [ "
    "            { \"prefix\":  \"2001:db8::\","
    "              \"prefix-len\": 56,"
    "              \"delegated-len\": 64,"
    "              \"user-context\": {"
    "                  \"lw4over6-sharing-ratio\": 64,"
    "                  \"lw4over6-v4-pool\": \"192.0.2.0/24\","
    "                  \"lw4over6-sysports-exclude\": true,"
    "                  \"lw4over6-bind-prefix-len\": 56"
    "              }"
    "            }"
    "        ],"
    "        \"subnet\": \"2001:db8::/32\""
    "     } ]"
185 186 187
    "}"
};

188 189 190 191 192 193 194 195 196 197 198
std::string specfile(const std::string& name) {
    return (std::string(DHCP6_SRC_DIR) + "/" + name);
}

/// @brief Tests that the spec file is valid.
/// Verifies that the DHCP6 configuration specification file is valid.
TEST(Dhcp6SpecTest, basicSpec) {
    ASSERT_NO_THROW(isc::config::
                    moduleSpecFromFile(specfile("dhcp6.spec")));
}

199
class Dhcp6ParserTest : public ::testing::Test {
200 201 202 203 204 205 206 207 208 209
protected:
    // Check that no hooks libraries are loaded.  This is a pre-condition for
    // a number of tests, so is checked in one place.  As this uses an
    // ASSERT call - and it is not clear from the documentation that Gtest
    // predicates can be used in a constructor - the check is placed in SetUp.
    virtual void SetUp() {
        std::vector<std::string> libraries = HooksManager::getLibraryNames();
        ASSERT_TRUE(libraries.empty());
    }

210
public:
Stephen Morris's avatar
Stephen Morris committed
211 212
    Dhcp6ParserTest() :rcode_(-1), srv_(0) {
        // srv_(0) means to not open any sockets. We don't want to
213 214
        // deal with sockets here, just check if configuration handling
        // is sane.
Tomek Mrugalski's avatar
Tomek Mrugalski committed
215

216 217
        const IfaceMgr::IfaceCollection& ifaces =
            IfaceMgr::instance().getIfaces();
Tomek Mrugalski's avatar
Tomek Mrugalski committed
218 219 220

        // There must be some interface detected
        if (ifaces.empty()) {
221
            // We can't use ASSERT in constructor
Tomek Mrugalski's avatar
Tomek Mrugalski committed
222 223 224
            ADD_FAILURE() << "No interfaces detected.";
        }

225
        valid_iface_ = (*ifaces.begin())->getName();
Tomek Mrugalski's avatar
Tomek Mrugalski committed
226
        bogus_iface_ = "nonexisting0";
227 228 229

        if (IfaceMgr::instance().getIface(bogus_iface_)) {
            ADD_FAILURE() << "The '" << bogus_iface_ << "' exists on this system"
230 231
                          << " while the test assumes that it doesn't, to execute"
                          << " some negative scenarios. Can't continue this test.";
232
        }
233 234 235

        // Reset configuration for each test.
        resetConfiguration();
236 237 238
    }

    ~Dhcp6ParserTest() {
239 240
        // Reset configuration database after each test.
        resetConfiguration();
241 242

        // ... and delete the hooks library marker files if present
243 244
        static_cast<void>(remove(LOAD_MARKER_FILE));
        static_cast<void>(remove(UNLOAD_MARKER_FILE));
245 246
    };

247 248 249 250 251 252 253 254 255
    // Checks if config_result (result of DHCP server configuration) has
    // expected code (0 for success, other for failures).
    // Also stores result in rcode_ and comment_.
    void checkResult(ConstElementPtr status, int expected_code) {
        ASSERT_TRUE(status);
        comment_ = parseAnswer(rcode_, status);
        EXPECT_EQ(expected_code, rcode_);
    }

256 257 258
    /// @brief Returns an interface configuration used by the most of the
    /// unit tests.
    std::string genIfaceConfig() const {
259
        return ("\"interfaces-config\": {"
260 261 262 263
                "  \"interfaces\": [ \"*\" ]"
                "}");
    }

264 265 266
    /// @brief Create the simple configuration with single option.
    ///
    /// This function allows to set one of the parameters that configure
267 268
    /// option value. These parameters are: "name", "code", "data" and
    /// "csv-format".
269
    ///
270
    /// @param param_value string holding option parameter value to be
271 272 273 274 275
    /// injected into the configuration string.
    /// @param parameter name of the parameter to be configured with
    /// param value.
    std::string createConfigWithOption(const std::string& param_value,
                                       const std::string& parameter) {
276 277 278
        std::map<std::string, std::string> params;
        if (parameter == "name") {
            params["name"] = param_value;
279
            params["space"] = DHCP6_OPTION_SPACE;
280
            params["code"] = "38";
281
            params["data"] = "ABCDEF0105";
282
            params["csv-format"] = "false";
283
        } else if (parameter == "space") {
284 285 286
            params["name"] = "subscriber-id";
            params["space"] = param_value;
            params["code"] = "38";
287
            params["data"] = "ABCDEF0105";
288
            params["csv-format"] = "false";
289
        } else if (parameter == "code") {
290
            params["name"] = "subscriber-id";
291
            params["space"] = DHCP6_OPTION_SPACE;
292
            params["code"] = param_value;
293
            params["data"] = "ABCDEF0105";
294
            params["csv-format"] = "false";
295
        } else if (parameter == "data") {
296
            params["name"] = "subscriber-id";
297
            params["space"] = DHCP6_OPTION_SPACE;
298
            params["code"] = "38";
299
            params["data"] = param_value;
300
            params["csv-format"] = "false";
301
        } else if (parameter == "csv-format") {
302
            params["name"] = "subscriber-id";
303
            params["space"] = DHCP6_OPTION_SPACE;
304
            params["code"] = "38";
305
            params["data"] = "ABCDEF0105";
306
            params["csv-format"] = param_value;
307 308 309 310
        }
        return (createConfigWithOption(params));
    }

311 312 313 314 315 316 317 318
    /// @brief Create simple configuration with single option.
    ///
    /// This function creates a configuration for a single option with
    /// custom values for all parameters that describe the option.
    ///
    /// @params params map holding parameters and their values.
    /// @return configuration string containing custom values of parameters
    /// describing an option.
Mukund Sivaraman's avatar
Mukund Sivaraman committed
319 320 321
    std::string createConfigWithOption(const std::map<std::string,
                                       std::string>& params)
    {
322
        std::ostringstream stream;
323
        stream << "{ " << genIfaceConfig() << ","
324 325 326
            "\"preferred-lifetime\": 3000,"
            "\"rebind-timer\": 2000, "
            "\"renew-timer\": 1000, "
327 328 329 330
            "\"option-def\": [ {"
            "  \"name\": \"bool-option\","
            "  \"code\": 1000,"
            "  \"type\": \"boolean\","
331
            "  \"space\": \"dhcp6\""
332
            "} ],"
333
            "\"subnet6\": [ { "
334
            "    \"pools\": [ { \"pool\": \"2001:db8:1::/80\" } ],"
335 336
            "    \"subnet\": \"2001:db8:1::/64\", "
            "    \"option-data\": [ {";
337 338 339 340 341 342
        bool first = true;
        typedef std::pair<std::string, std::string> ParamPair;
        BOOST_FOREACH(ParamPair param, params) {
            if (!first) {
                stream << ", ";
            } else {
343
                // cppcheck-suppress unreadVariable
344 345 346 347
                first = false;
            }
            if (param.first == "name") {
                stream << "\"name\": \"" << param.second << "\"";
348 349
            } else if (param.first == "space") {
                stream << "\"space\": \"" << param.second << "\"";
350
            } else if (param.first == "code") {
351
                stream << "\"code\": " << param.second;;
352 353
            } else if (param.first == "data") {
                stream << "\"data\": \"" << param.second << "\"";
354 355
            } else if (param.first == "csv-format") {
                stream << "\"csv-format\": " << param.second;
356
            }
357 358 359 360 361 362 363 364
        }
        stream <<
            "        } ]"
            " } ],"
            "\"valid-lifetime\": 4000 }";
        return (stream.str());
    }

365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
    /// @brief Returns an option from the subnet.
    ///
    /// This function returns an option from a subnet to which the
    /// specified subnet address belongs. The option is identified
    /// by its code.
    ///
    /// @param subnet_address Address which belongs to the subnet from
    /// which the option is to be returned.
    /// @param option_code Code of the option to be returned.
    /// @param expected_options_count Expected number of options in
    /// the particular subnet.
    ///
    /// @return Descriptor of the option. If the descriptor holds a
    /// NULL option pointer, it means that there was no such option
    /// in the subnet.
380
    OptionDescriptor
381 382 383
    getOptionFromSubnet(const IOAddress& subnet_address,
                        const uint16_t option_code,
                        const uint16_t expected_options_count = 1) {
384 385
        Subnet6Ptr subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->
            selectSubnet(subnet_address, classify_);
386 387 388 389
        if (!subnet) {
            /// @todo replace toText() with the use of operator <<.
            ADD_FAILURE() << "A subnet for the specified address "
                          << subnet_address.toText()
390
                          << " does not exist in Config Manager";
391
        }
392
        OptionContainerPtr options =
393
            subnet->getCfgOption()->getAll(DHCP6_OPTION_SPACE);
394 395 396 397 398 399 400 401
        if (expected_options_count != options->size()) {
            ADD_FAILURE() << "The number of options in the subnet '"
                          << subnet_address.toText() << "' is different "
                " than expected number of options '"
                          << expected_options_count << "'";
        }

        // Get the search index. Index #1 is to search using option code.
402
        const OptionContainerTypeIndex& idx = options->get<1>();
403 404 405 406

        // Get the options for specified index. Expecting one option to be
        // returned but in theory we may have multiple options with the same
        // code so we get the range.
407 408
        std::pair<OptionContainerTypeIndex::const_iterator,
                  OptionContainerTypeIndex::const_iterator> range =
409 410 411 412 413 414 415
            idx.equal_range(option_code);
        if (std::distance(range.first, range.second) > 1) {
            ADD_FAILURE() << "There is more than one option having the"
                " option code '" << option_code << "' in a subnet '"
                          << subnet_address.toText() << "'. Expected "
                " at most one option";
        } else if (std::distance(range.first, range.second) == 0) {
416
            return (OptionDescriptor(OptionPtr(), false));
417 418 419 420 421
        }

        return (*range.first);
    }

422
    /// @brief Parse and Execute configuration
423
    ///
424 425 426 427 428 429 430 431 432 433 434
    /// Parses a configuration and executes a configuration of the server.
    /// If the operation fails, the current test will register a failure.
    ///
    /// @param config Configuration to parse
    /// @param operation Operation being performed.  In the case of an error,
    ///        the error text will include the string "unable to <operation>.".
    ///
    /// @return true if the configuration succeeded, false if not.  In the
    ///         latter case, a failure will have been added to the current test.
    bool
    executeConfiguration(const std::string& config, const char* operation) {
435
        ConstElementPtr json;
436 437
        ConstElementPtr status;
        try {
438
            json = parseJSON(config);
439
            status = configureDhcp6Server(srv_, json);
440

441
        } catch (const std::exception& ex) {
442 443
            ADD_FAILURE() << "Unable to " << operation << ". "
                   << "The following configuration was used: " << std::endl
444 445 446
                   << config << std::endl
                   << " and the following error message was returned:"
                   << ex.what() << std::endl;
447
            return (false);
448 449
        }

450
        // The status object must not be NULL
451
        if (!status) {
452 453 454
            ADD_FAILURE() << "Unable to " << operation << ". "
                   << "The configuration function returned a null pointer.";
            return (false);
455
        }
456 457 458 459

        // Store the answer if we need it.

        // Returned value should be 0 (configuration success)
460 461
        comment_ = parseAnswer(rcode_, status);
        if (rcode_ != 0) {
462 463 464 465 466 467 468 469
            string reason = "";
            if (comment_) {
                reason = string(" (") + comment_->stringValue() + string(")");
            }
            ADD_FAILURE() << "Unable to " << operation << ". "
                   << "The configuration function returned error code "
                   << rcode_ << reason;
            return (false);
470
        }
471 472 473 474 475 476 477 478 479 480 481

        return (true);
    }

    /// @brief Reset configuration database.
    ///
    /// This function resets configuration data base by removing all subnets
    /// option-data, and hooks libraries. The reset must be performed after each
    /// test to make sure that contents of the database do not affect the
    /// results of subsequent tests.
    void resetConfiguration() {
482 483 484
        string config = "{ \"interfaces-config\": {"
            "    \"interfaces\": [ ]"
            "},"
485 486 487 488 489 490
            "\"hooks-libraries\": [ ],"
            "\"preferred-lifetime\": 3000,"
            "\"rebind-timer\": 2000, "
            "\"renew-timer\": 1000, "
            "\"valid-lifetime\": 4000, "
            "\"subnet6\": [ ], "
491
            "\"dhcp-ddns\": { \"enable-updates\" : false }, "
492 493 494 495
            "\"option-def\": [ ], "
            "\"option-data\": [ ] }";
        static_cast<void>(executeConfiguration(config,
                                               "reset configuration database"));
496 497 498
        // The default setting is to listen on all interfaces. In order to
        // properly test interface configuration we disable listening on
        // all interfaces before each test and later check that this setting
Francis Dupont's avatar
Francis Dupont committed
499
        // has been overridden by the configuration used in the test.
500
        CfgMgr::instance().clear();
501 502
        // Create fresh context.
        globalContext()->copyContext(ParserContext(Option::V6));
503 504
    }

505 506 507 508 509 510 511 512
    /// @brief Retrieve an option associated with a host.
    ///
    /// The option is retrieved from the "dhcp6" option space.
    ///
    /// @param host Reference to a host for which an option should be retrieved.
    /// @param option_code Option code.
    /// @tparam ReturnType Type of the pointer object returned.
    ///
513
    /// @return Pointer to an option or NULL pointer if not found.
514 515 516
    template<typename ReturnType>
    ReturnType
    retrieveOption(const Host& host, const uint16_t option_code) const {
517
        return (retrieveOption<ReturnType>(host, DHCP6_OPTION_SPACE, option_code));
518 519 520 521 522 523 524 525 526
    }

    /// @brief Retrieve an option associated with a host.
    ///
    /// @param host Reference to a host for which an option should be retrieved.
    /// @param space Option space from which option should be retrieved.
    /// @param option_code Option code.
    /// @tparam ReturnType Type of the pointer object returned.
    ///
527
    /// @return Pointer to an option or NULL pointer if not found.
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543
    template<typename ReturnType>
    ReturnType
    retrieveOption(const Host& host, const std::string& space,
                   const uint16_t option_code) const {
        ConstCfgOptionPtr cfg_option = host.getCfgOption6();
        if (cfg_option) {
            OptionDescriptor opt_desc = cfg_option->get(space, option_code);
            if (opt_desc.option_) {
                return (boost::dynamic_pointer_cast<
                        typename ReturnType::element_type>(opt_desc.option_));
            }
        }
        return (ReturnType());
    }


544 545 546 547 548 549 550 551 552 553 554 555 556 557
    /// @brief Test invalid option parameter value.
    ///
    /// This test function constructs the simple configuration
    /// string and injects invalid option configuration into it.
    /// It expects that parser will fail with provided option code.
    ///
    /// @param param_value string holding invalid option parameter value
    /// to be injected into configuration string.
    /// @param parameter name of the parameter to be configured with
    /// param_value (can be any of "name", "code", "data")
    void testInvalidOptionParam(const std::string& param_value,
                                const std::string& parameter) {
        ConstElementPtr x;
        std::string config = createConfigWithOption(param_value, parameter);
558
        ConstElementPtr json = parseJSON(config);
Stephen Morris's avatar
Stephen Morris committed
559
        EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
560 561
        checkResult(x, 1);
        EXPECT_TRUE(errorContainsPosition(x, "<string>"));
562
        CfgMgr::instance().clear();
563 564
    }

565 566 567 568 569 570 571 572 573 574 575
    /// @brief Test invalid option paramater value.
    ///
    /// This test function constructs the simple configuration
    /// string and injects invalid option configuration into it.
    /// It expects that parser will fail with provided option code.
    ///
    /// @param params Map of parameters defining an option.
    void
    testInvalidOptionParam(const std::map<std::string, std::string>& params) {
        ConstElementPtr x;
        std::string config = createConfigWithOption(params);
576
        ConstElementPtr json = parseJSON(config);
577
        EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
578 579
        checkResult(x, 1);
        EXPECT_TRUE(errorContainsPosition(x, "<string>"));
580
        CfgMgr::instance().clear();
581 582
    }

583 584 585 586 587 588 589
    /// @brief Test option against given code and data.
    ///
    /// @param option_desc option descriptor that carries the option to
    /// be tested.
    /// @param expected_code expected code of the option.
    /// @param expected_data expected data in the option.
    /// @param expected_data_len length of the reference data.
590 591
    /// @param extra_data if true extra data is allowed in an option
    /// after tested data.
592
    void testOption(const OptionDescriptor& option_desc,
593
                    uint16_t expected_code, const uint8_t* expected_data,
594 595
                    size_t expected_data_len,
                    bool extra_data = false) {
596
        // Check if option descriptor contains valid option pointer.
597
        ASSERT_TRUE(option_desc.option_);
598
        // Verify option type.
599
        EXPECT_EQ(expected_code, option_desc.option_->getType());
600 601 602 603 604
        // We may have many different option types being created. Some of them
        // have dedicated classes derived from Option class. In such case if
        // we want to verify the option contents against expected_data we have
        // to prepare raw buffer with the contents of the option. The easiest
        // way is to call pack() which will prepare on-wire data.
605 606
        util::OutputBuffer buf(option_desc.option_->getData().size());
        option_desc.option_->pack(buf);
607 608 609 610 611
        if (extra_data) {
            // The length of the buffer must be at least equal to size of the
            // reference data but it can sometimes be greater than that. This is
            // because some options carry suboptions that increase the overall
            // length.
612
            ASSERT_GE(buf.getLength() - option_desc.option_->getHeaderLen(),
613 614
                      expected_data_len);
        } else {
615
            ASSERT_EQ(buf.getLength() - option_desc.option_->getHeaderLen(),
616 617
                      expected_data_len);
        }
618
        // Verify that the data is correct. Do not verify suboptions and a header.
619
        const uint8_t* data = static_cast<const uint8_t*>(buf.getData());
620
        EXPECT_EQ(0, memcmp(expected_data, data + option_desc.option_->getHeaderLen(),
621
                            expected_data_len));
622 623
    }

624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647
    /// @brief Test option configuration.
    ///
    /// This function creates a configuration for a specified option using
    /// a map of parameters specified as the argument. The map holds
    /// name/value pairs which identifies option's configuration parameters:
    /// - name
    /// - space
    /// - code
    /// - data
    /// - csv-format.
    /// This function applies a new server configuration and checks that the
    /// option being configured is inserted into CfgMgr. The raw contents of
    /// this option are compared with the binary data specified as expected
    /// data passed to this function.
    ///
    /// @param params Map of parameters defining an option.
    /// @param option_code Option code.
    /// @param expected_data Array containing binary data expected to be stored
    /// in the configured option.
    /// @param expected_data_len Length of the array holding reference data.
    void testConfiguration(const std::map<std::string, std::string>& params,
                           const uint16_t option_code,
                           const uint8_t* expected_data,
                           const size_t expected_data_len) {
648 649
        CfgMgr::instance().clear();

650 651
        std::string config = createConfigWithOption(params);
        ASSERT_TRUE(executeConfiguration(config, "parse option configuration"));
652

653
        // The subnet should now hold one option with the specified code.
654
        OptionDescriptor desc =
655
            getOptionFromSubnet(IOAddress("2001:db8:1::5"), option_code);
656
        ASSERT_TRUE(desc.option_);
657
        testOption(desc, option_code, expected_data, expected_data_len);
658
        CfgMgr::instance().clear();
659 660
    }

661
    /// @brief Tests the Rapid Commit configuration for a subnet.
662 663 664 665 666 667 668 669 670 671 672 673 674 675 676
    ///
    /// This test configures the server with a given configuration and
    /// verifies if the Rapid Commit has been configured successfully
    /// for a subnet.
    ///
    /// @param config Server configuration, possibly including the
    /// 'rapid-commit' parameter.
    /// @param exp_rapid_commit Expected value of the Rapid Commit flag
    /// within a subnet.
    void testRapidCommit(const std::string& config,
                         const bool exp_rapid_commit) {
        // Clear any existing configuration.
        CfgMgr::instance().clear();

        // Configure the server.
677
        ConstElementPtr json = parseJSON(config);
678 679 680 681 682 683 684 685 686 687 688 689 690

        // Make sure that the configuration was successful.
        ConstElementPtr status;
        EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
        checkResult(status, 0);

        // Get the subnet.
        Subnet6Ptr subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->
            selectSubnet(IOAddress("2001:db8:1::5"), classify_);
        ASSERT_TRUE(subnet);

        // Check the Rapid Commit flag for the subnet.
        EXPECT_EQ(exp_rapid_commit, subnet->getRapidCommit());
691 692 693

        // Clear any existing configuration.
        CfgMgr::instance().clear();
694 695
    }

696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726
    /// @brief This utility method attempts to configure using specified
    ///        config and then returns requested pool from requested subnet
    ///
    /// @param config configuration to be applied
    /// @param subnet_index index of the subnet to be returned (0 - the first subnet)
    /// @param pool_index index of the pool within a subnet (0 - the first pool)
    /// @param type Pool type (TYPE_NA or TYPE_PD)
    /// @param pool [out] Pool pointer will be stored here (if found)
    void getPool(const std::string& config, size_t subnet_index,
                 size_t pool_index, Lease::Type type, PoolPtr& pool) {
        ConstElementPtr status;
        ElementPtr json = Element::fromJSON(config);

        EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
        ASSERT_TRUE(status);
        checkResult(status, 0);

        ConstCfgSubnets6Ptr subnets6 = CfgMgr::instance().getStagingCfg()->getCfgSubnets6();
        ASSERT_TRUE(subnets6);

        const Subnet6Collection* subnets = subnets6->getAll();
        ASSERT_TRUE(subnets);
        ASSERT_GE(subnets->size(), subnet_index + 1);

        const PoolCollection pools = subnets->at(subnet_index)->getPools(type);
        ASSERT_GE(pools.size(), pool_index + 1);

        pool = pools.at(pool_index);
        EXPECT_TRUE(pool);
    }

727 728 729 730 731
    int rcode_;          ///< Return code (see @ref isc::config::parseAnswer)
    Dhcpv6Srv srv_;      ///< Instance of the Dhcp6Srv used during tests
    ConstElementPtr comment_; ///< Comment (see @ref isc::config::parseAnswer)
    string valid_iface_; ///< Valid network interface name (present in system)
    string bogus_iface_; ///< invalid network interface name (not in system)
732
    isc::dhcp::ClientClasses classify_; ///< used in client classification
733 734
};

735 736 737
// Goal of this test is a verification if a very simple config update
// with just a bumped version number. That's the simplest possible
// config update.
738 739 740 741
TEST_F(Dhcp6ParserTest, version) {

    ConstElementPtr x;

Stephen Morris's avatar
Stephen Morris committed
742
    EXPECT_NO_THROW(x = configureDhcp6Server(srv_,
743
                    parseJSON("{\"version\": 0}")));
744 745

    // returned value must be 0 (configuration accepted)
746
    checkResult(x, 0);
747 748
}

749 750
/// The goal of this test is to verify that the code accepts only
/// valid commands and malformed or unsupported parameters are rejected.
751
TEST_F(Dhcp6ParserTest, bogusCommand) {
752 753 754

    ConstElementPtr x;

Stephen Morris's avatar
Stephen Morris committed
755
    EXPECT_NO_THROW(x = configureDhcp6Server(srv_,
756
                    parseJSON("{\"bogus\": 5}")));
757 758

    // returned value must be 1 (configuration parse error)
759
    checkResult(x, 1);
760 761
}

762 763
/// The goal of this test is to verify if configuration without any
/// subnets defined can be accepted.
764
TEST_F(Dhcp6ParserTest, emptySubnet) {
765

766 767
    ConstElementPtr json;
    ASSERT_NO_THROW(json = parseDHCP6("{ " + genIfaceConfig() + ","
768 769 770 771
                                      "\"preferred-lifetime\": 3000,"
                                      "\"rebind-timer\": 2000, "
                                      "\"renew-timer\": 1000, "
                                      "\"subnet6\": [  ], "
772
                                      "\"valid-lifetime\": 4000 }"));
773

774 775 776
    ConstElementPtr status;
    EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
                    
777
    // returned value should be 0 (success)
778
    checkResult(status, 0);
779 780
}

781 782
/// The goal of this test is to verify if defined subnet uses global
/// parameter timer definitions.
783
TEST_F(Dhcp6ParserTest, subnetGlobalDefaults) {
784

785
    string config = "{ " + genIfaceConfig() + ","
786 787 788 789
        "\"preferred-lifetime\": 3000,"
        "\"rebind-timer\": 2000, "
        "\"renew-timer\": 1000, "
        "\"subnet6\": [ { "
790
        "    \"pools\": [ { \"pool\": \"2001:db8:1::1 - 2001:db8:1::ffff\" } ],"
791 792 793
        "    \"subnet\": \"2001:db8:1::/64\" } ],"
        "\"valid-lifetime\": 4000 }";

794 795
    ConstElementPtr json;
    ASSERT_NO_THROW(json = parseDHCP6(config));
796

797
    ConstElementPtr status;
Stephen Morris's avatar
Stephen Morris committed
798
    EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
799

800
    // check if returned status is OK
801
    checkResult(status, 0);
802

803 804
    // Now check if the configuration was indeed handled and we have
    // expected pool configured.
805 806
    Subnet6Ptr subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->
        selectSubnet(IOAddress("2001:db8:1::5"), classify_);
807 808 809 810 811
    ASSERT_TRUE(subnet);
    EXPECT_EQ(1000, subnet->getT1());
    EXPECT_EQ(2000, subnet->getT2());
    EXPECT_EQ(3000, subnet->getPreferred());
    EXPECT_EQ(4000, subnet->getValid());
812 813 814 815 816

    // Check that subnet-id is 1
    EXPECT_EQ(1, subnet->getID());
}

817
// This test checks that multiple subnets can be defined and handled properly.
818 819
TEST_F(Dhcp6ParserTest, multipleSubnets) {
    ConstElementPtr x;
820 821
    // Collection of four subnets for which ids should be autogenerated
    // - ids are unspecified or set to 0.
822
    string config = "{ " + genIfaceConfig() + ","
823 824 825 826
        "\"preferred-lifetime\": 3000,"
        "\"rebind-timer\": 2000, "
        "\"renew-timer\": 1000, "
        "\"subnet6\": [ { "
827
        "    \"pools\": [ { \"pool\": \"2001:db8:1::/80\" } ],"
828 829 830
        "    \"subnet\": \"2001:db8:1::/64\" "
        " },"
        " {"
831
        "    \"pools\": [ { \"pool\": \"2001:db8:2::/80\" } ],"
832 833
        "    \"subnet\": \"2001:db8:2::/64\", "
        "    \"id\": 0"
834 835
        " },"
        " {"
836
        "    \"pools\": [ { \"pool\": \"2001:db8:3::/80\" } ],"
837 838 839
        "    \"subnet\": \"2001:db8:3::/64\" "
        " },"
        " {"
840
        "    \"pools\": [ { \"pool\": \"2001:db8:4::/80\" } ],"
841 842 843 844 845 846
        "    \"subnet\": \"2001:db8:4::/64\" "
        " } ],"
        "\"valid-lifetime\": 4000 }";

    int cnt = 0; // Number of reconfigurations

847 848
    ConstElementPtr json;
    ASSERT_NO_THROW(json = parseDHCP6(config));
849

850
    do {
851
        EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
852
        checkResult(x, 0);
853

854 855 856 857
        CfgMgr::instance().commit();

        const Subnet6Collection* subnets =
            CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();
858 859 860 861 862 863 864 865 866 867 868 869 870 871
        ASSERT_TRUE(subnets);
        ASSERT_EQ(4, subnets->size()); // We expect 4 subnets

        // Check subnet-ids of each subnet (it should be monotonously increasing)
        EXPECT_EQ(1, subnets->at(0)->getID());
        EXPECT_EQ(2, subnets->at(1)->getID());
        EXPECT_EQ(3, subnets->at(2)->getID());
        EXPECT_EQ(4, subnets->at(3)->getID());

        // Repeat reconfiguration process 10 times and check that the subnet-id
        // is set to the same value. Technically, just two iterations would be
        // sufficient, but it's nice to have a test that exercises reconfiguration
        // a bit.
    } while (++cnt < 10);
872 873
}

874 875 876
// This checks that it is possible to assign arbitrary ids for subnets.
TEST_F(Dhcp6ParserTest, multipleSubnetsExplicitIDs) {
    ConstElementPtr x;
877
    // Four subnets with arbitrary subnet ids.
878
    string config = "{ " + genIfaceConfig() + ","
879 880 881 882
        "\"preferred-lifetime\": 3000,"
        "\"rebind-timer\": 2000, "
        "\"renew-timer\": 1000, "
        "\"subnet6\": [ { "
883
        "    \"pools\": [ { \"pool\": \"2001:db8:1::/80\" } ],"
884 885 886 887
        "    \"subnet\": \"2001:db8:1::/64\", "
        "    \"id\": 1024"
        " },"
        " {"
888
        "    \"pools\": [ { \"pool\": \"2001:db8:2::/80\" } ],"
889 890 891 892
        "    \"subnet\": \"2001:db8:2::/64\", "
        "    \"id\": 100"
        " },"
        " {"
893
        "    \"pools\": [ { \"pool\": \"2001:db8:3::/80\" } ],"
894 895 896 897
        "    \"subnet\": \"2001:db8:3::/64\", "
        "    \"id\": 1"
        " },"
        " {"
898
        "    \"pools\": [ { \"pool\": \"2001:db8:4::/80\" } ],"
899 900 901 902 903 904 905
        "    \"subnet\": \"2001:db8:4::/64\", "
        "    \"id\": 34"
        " } ],"
        "\"valid-lifetime\": 4000 }";

    int cnt = 0; // Number of reconfigurations

906 907
    ConstElementPtr json;
    ASSERT_NO_THROW(json = parseDHCP6(config));
908

909
    do {
910
        EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
911
        checkResult(x, 0);
912

913 914 915 916
        CfgMgr::instance().commit();

        const Subnet6Collection* subnets =
            CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();
917 918 919 920 921 922 923 924 925 926 927
        ASSERT_TRUE(subnets);
        ASSERT_EQ(4, subnets->size()); // We expect 4 subnets

        // Check that subnet ids are as expected.
        EXPECT_EQ(1024, subnets->at(0)->getID());
        EXPECT_EQ(100, subnets->at(1)->getID());
        EXPECT_EQ(1, subnets->at(2)->getID());
        EXPECT_EQ(34, subnets->at(3)->getID());

        // Repeat reconfiguration process 10 times and check that the subnet-id
        // is set to the same value.
928
    } while (++cnt < 3);
929 930 931 932 933 934
}

// CHeck that the configuration with two subnets having the same id is rejected.
TEST_F(Dhcp6ParserTest, multipleSubnetsOverlapingIDs) {
    ConstElementPtr x;
    // Four subnets, two of them have the same id.
935
    string config = "{ " + genIfaceConfig() + ","
936 937 938 939
        "\"preferred-lifetime\": 3000,"
        "\"rebind-timer\": 2000, "
        "\"renew-timer\": 1000, "
        "\"subnet6\": [ { "
940
        "    \"pools\": [ { \"pool\": \"2001:db8:1::/80\" } ],"
941 942 943 944
        "    \"subnet\": \"2001:db8:1::/64\", "
        "    \"id\": 1024"
        " },"
        " {"
945
        "    \"pools\": [ { \"pool\": \"2001:db8:2::/80\" } ],"
946 947 948 949
        "    \"subnet\": \"2001:db8:2::/64\", "
        "    \"id\": 100"
        " },"
        " {"
950
        "    \"pools\": [ { \"pool\": \"2001:db8:3::/80\" } ],"
951 952 953 954
        "    \"subnet\": \"2001:db8:3::/64\", "
        "    \"id\": 1024"
        " },"
        " {"
955
        "    \"pools\": [ { \"pool\": \"2001:db8:4::/80\" } ],"
956 957 958 959 960
        "    \"subnet\": \"2001:db8:4::/64\", "
        "    \"id\": 34"
        " } ],"
        "\"valid-lifetime\": 4000 }";

961 962
    ConstElementPtr json;
    ASSERT_NO_THROW(json = parseDHCP6(config));
963 964

    EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
965
    checkResult(x, 1);
966
    EXPECT_TRUE(errorContainsPosition(x, "<string>"));
967 968 969
}


Tomek Mrugalski's avatar
Tomek Mrugalski committed
970 971 972 973 974 975
// Goal of this test is to verify that a previously configured subnet can be
// deleted in subsequent reconfiguration.
TEST_F(Dhcp6ParserTest, reconfigureRemoveSubnet) {
    ConstElementPtr x;

    // All four subnets
976
    string config4 = "{ " + genIfaceConfig() + ","
Tomek Mrugalski's avatar
Tomek Mrugalski committed
977 978 979 980
        "\"preferred-lifetime\": 3000,"
        "\"rebind-timer\": 2000, "
        "\"renew-timer\": 1000, "
        "\"subnet6\": [ { "
981
        "    \"pools\": [ { \"pool\": \"2001:db8:1::/80\" } ],"
982 983
        "    \"subnet\": \"2001:db8:1::/64\", "
        "    \"id\": 1"
Tomek Mrugalski's avatar
Tomek Mrugalski committed
984 985
        " },"
        " {"
986
        "    \"pools\": [ { \"pool\": \"2001:db8:2::/80\" } ],"
987 988
        "    \"subnet\": \"2001:db8:2::/64\", "
        "    \"id\": 2"
Tomek Mrugalski's avatar
Tomek Mrugalski committed
989 990
        " },"
        " {"
991
        "    \"pools\": [ { \"pool\": \"2001:db8:3::/80\" } ],"
992 993
        "    \"subnet\": \"2001:db8:3::/64\", "
        "    \"id\": 3"
Tomek Mrugalski's avatar
Tomek Mrugalski committed
994 995
        " },"
        " {"
996
        "    \"pools\": [ { \"pool\": \"2001:db8:4::/80\" } ],"
997 998
        "    \"subnet\": \"2001:db8:4::/64\", "
        "    \"id\": 4"
Tomek Mrugalski's avatar
Tomek Mrugalski committed
999 1000 1001 1002
        " } ],"
        "\"valid-lifetime\": 4000 }";

    // Three subnets (the last one removed)
1003
    string config_first3 = "{ " + genIfaceConfig() + ","
Tomek Mrugalski's avatar
Tomek Mrugalski committed
1004 1005 1006 1007
        "\"preferred-lifetime\": 3000,"
        "\"rebind-timer\": 2000, "
        "\"renew-timer\": 1000, "
        "\"subnet6\": [ { "
1008
        "    \"pools\": [ { \"pool\": \"2001:db8:1::/80\" } ],"
1009 1010
        "    \"subnet\": \"2001:db8:1::/64\", "
        "    \"id\": 1"
Tomek Mrugalski's avatar
Tomek Mrugalski committed
1011 1012
        " },"
        " {"
1013
        "    \"pools\": [ { \"pool\": \"2001:db8:2::/80\" } ],"
1014 1015
        "    \"subnet\": \"2001:db8:2::/64\", "
        "    \"id\": 2"
Tomek Mrugalski's avatar
Tomek Mrugalski committed
1016 1017
        " },"
        " {"
1018
        "    \"pools\": [ { \"pool\": \"2001:db8:3::/80\" } ],"
1019 1020
        "    \"subnet\": \"2001:db8:3::/64\", "
        "    \"id\": 3"
Tomek Mrugalski's avatar
Tomek Mrugalski committed
1021 1022 1023 1024
        " } ],"
        "\"valid-lifetime\": 4000 }";

    // Second subnet removed
1025
    string config_second_removed = "{ " + genIfaceConfig() + ","
Tomek Mrugalski's avatar
Tomek Mrugalski committed
1026 1027 1028 1029
        "\"preferred-lifetime\": 3000,"
        "\"rebind-timer\": 2000, "
        "\"renew-timer\": 1000, "
        "\"subnet6\": [ { "
1030
        "    \"pools\": [ { \"pool\": \"2001:db8:1::/80\" } ],"