ctrl_dhcp6_srv_unittest.cc 40.3 KB
Newer Older
1
// Copyright (C) 2012-2017 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

Francis Dupont's avatar
Francis Dupont committed
9
#include <asiolink/io_address.h>
10
#include <cc/command_interpreter.h>
11
#include <config/command_mgr.h>
Tomek Mrugalski's avatar
Tomek Mrugalski committed
12
#include <dhcpsrv/cfgmgr.h>
Francis Dupont's avatar
Francis Dupont committed
13
14
#include <dhcpsrv/lease.h>
#include <dhcpsrv/lease_mgr_factory.h>
15
#include <dhcp6/ctrl_dhcp6_srv.h>
16
#include <dhcp6/tests/dhcp6_test_utils.h>
17
#include <hooks/hooks_manager.h>
18
#include <log/logger_support.h>
Francis Dupont's avatar
Francis Dupont committed
19
#include <stats/stats_mgr.h>
20
#include <testutils/unix_control_client.h>
21
#include <testutils/io_utils.h>
22
23
24

#include "marker_file.h"
#include "test_libraries.h"
25

26
#include <boost/scoped_ptr.hpp>
27
28
#include <gtest/gtest.h>

29
#include <sys/select.h>
30
#include <sys/stat.h>
31
#include <sys/ioctl.h>
32
#include <cstdlib>
33

34
using namespace std;
Francis Dupont's avatar
Francis Dupont committed
35
using namespace isc::asiolink;
36
using namespace isc::config;
37
38
39
40
using namespace isc::data;
using namespace isc::dhcp;
using namespace isc::dhcp::test;
using namespace isc::hooks;
Francis Dupont's avatar
Francis Dupont committed
41
using namespace isc::stats;
42
using namespace isc::test;
43
44
45

namespace {

46
47


48
class NakedControlledDhcpv6Srv: public ControlledDhcpv6Srv {
49
    // "Naked" DHCPv6 server, exposes internal fields
50
public:
51
52
53
    NakedControlledDhcpv6Srv():ControlledDhcpv6Srv(DHCP6_SERVER_PORT + 10000) {
    }

54
55
    /// Expose internal methods for the sake of testing
    using Dhcpv6Srv::receivePacket;
56
57
};

58
class CtrlDhcpv6SrvTest : public BaseServerTest {
59
public:
60
61
    CtrlDhcpv6SrvTest()
        : BaseServerTest() {
62
        reset();
63
64
    }

65
    virtual ~CtrlDhcpv6SrvTest() {
Francis Dupont's avatar
Francis Dupont committed
66
67
        LeaseMgrFactory::destroy();
        StatsMgr::instance().removeAll();
68
        reset();
69
    };
70

71

72
73
74
75
    /// @brief Reset hooks data
    ///
    /// Resets the data for the hooks-related portion of the test by ensuring
    /// that no libraries are loaded and that any marker files are deleted.
76
    virtual void reset() {
77
78
79
80
        // Unload any previously-loaded libraries.
        HooksManager::unloadLibraries();

        // Get rid of any marker files.
81
82
        static_cast<void>(remove(LOAD_MARKER_FILE));
        static_cast<void>(remove(UNLOAD_MARKER_FILE));
83
84
85
86
87
88
89
90
        IfaceMgr::instance().deleteAllExternalSockets();
        CfgMgr::instance().clear();
    }

};

class CtrlChannelDhcpv6SrvTest : public CtrlDhcpv6SrvTest {
public:
91
92

    /// @brief Path to the UNIX socket being used to communicate with the server
93
    std::string socket_path_;
94
95

    /// @brief Pointer to the tested server object
96
97
    boost::shared_ptr<NakedControlledDhcpv6Srv> server_;

98
99
100
    /// @brief Default constructor
    ///
    /// Sets socket path to its default value.
101
    CtrlChannelDhcpv6SrvTest() {
102
103
104
105
106
107
        const char* env = getenv("KEA_SOCKET_TEST_DIR");
        if (env) {
            socket_path_ = string(env) + "/kea6.sock";
        } else {
            socket_path_ = string(TEST_DATA_BUILDDIR) + "/kea6.sock";
        }
108
109
110
        reset();
    }

111
    /// @brief Destructor
112
113
114
115
116
    ~CtrlChannelDhcpv6SrvTest() {
        server_.reset();
        reset();
    };

117
118
119
120
121
122
123
124
    /// @brief Returns pointer to the server's IO service.
    ///
    /// @return Pointer to the server's IO service or null pointer if the server
    /// hasn't been created.
    IOServicePtr getIOService() {
        return (server_ ? server_->getIOService() : IOServicePtr());
    }

125
    void createUnixChannelServer() {
126
        static_cast<void>(::remove(socket_path_.c_str()));
127
128
129

        // Just a simple config. The important part here is the socket
        // location information.
130
        std::string header =
131
132
133
134
            "{"
            "    \"interfaces-config\": {"
            "        \"interfaces\": [ \"*\" ]"
            "    },"
Francis Dupont's avatar
Francis Dupont committed
135
136
137
138
139
            "    \"expired-leases-processing\": {"
            "         \"reclaim-timer-wait-time\": 60,"
            "         \"hold-reclaimed-time\": 500,"
            "         \"flush-reclaimed-timer-wait-time\": 60"
            "    },"
140
141
142
143
144
145
            "    \"rebind-timer\": 2000, "
            "    \"renew-timer\": 1000, "
            "    \"subnet6\": [ ],"
            "    \"valid-lifetime\": 4000,"
            "    \"control-socket\": {"
            "        \"socket-type\": \"unix\","
146
147
148
149
            "        \"socket-name\": \"";

        std::string footer =
            "\"    },"
150
151
152
153
            "    \"lease-database\": {"
            "       \"type\": \"memfile\", \"persist\": false }"
            "}";

154
155
156
157
        // Fill in the socket-name value with socket_path_  to
        // make the actual configuration text.
        std::string config_txt = header + socket_path_  + footer;

158
159
        ASSERT_NO_THROW(server_.reset(new NakedControlledDhcpv6Srv()));

160
        ConstElementPtr config;
161
        ASSERT_NO_THROW(config = parseDHCP6(config_txt));
162
        ConstElementPtr answer = server_->processConfig(config);
163
164
165
166
167
168

        // Commit the configuration so any subsequent reconfigurations
        // will only close the command channel if its configuration has
        // changed.
        CfgMgr::instance().commit();

169
170
171
        ASSERT_TRUE(answer);

        int status = 0;
172
173
174
        ConstElementPtr txt = isc::config::parseAnswer(status, answer);
        // This should succeed. If not, print the error message.
        ASSERT_EQ(0, status) << txt->str();
175
176
177
178
179
180
181
182

        // Now check that the socket was indeed open.
        ASSERT_GT(isc::config::CommandMgr::instance().getControlSocketFD(), -1);
    }

    /// @brief Reset
    void reset() {
        CtrlDhcpv6SrvTest::reset();
183
        static_cast<void>(::remove(socket_path_.c_str()));
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
    }

    /// @brief Conducts a command/response exchange via UnixCommandSocket
    ///
    /// This method connects to the given server over the given socket path.
    /// If successful, it then sends the given command and retrieves the
    /// server's response.  Note that it calls the server's receivePacket()
    /// method where needed to cause the server to process IO events on
    /// control channel the control channel sockets.
    ///
    /// @param command the command text to execute in JSON form
    /// @param response variable into which the received response should be
    /// placed.
    void sendUnixCommand(const std::string& command, std::string& response) {
        response = "";
        boost::scoped_ptr<UnixControlClient> client;
        client.reset(new UnixControlClient());
        ASSERT_TRUE(client);

        // Connect and then call server's receivePacket() so it can
        // detect the control socket connect and call the  accept handler
        ASSERT_TRUE(client->connectToServer(socket_path_));
        ASSERT_NO_THROW(server_->receivePacket(0));
207
        ASSERT_NO_THROW(getIOService()->run_one());
208
209
210
211
212

        // Send the command and then call server's receivePacket() so it can
        // detect the inbound data and call the read handler
        ASSERT_TRUE(client->sendCommand(command));
        ASSERT_NO_THROW(server_->receivePacket(0));
213
        ASSERT_NO_THROW(getIOService()->run_one());
214
215
216
217
218
219
220
221
222

        // Read the response generated by the server. Note that getResponse
        // only fails if there an IO error or no response data was present.
        // It is not based on the response content.
        ASSERT_TRUE(client->getResponse(response));

        // Now disconnect and process the close event
        client->disconnectFromServer();
        ASSERT_NO_THROW(server_->receivePacket(0));
223
224

        ASSERT_NO_THROW(getIOService()->poll());
225
    }
226
227
228
229
230
231
232
233
234
235

    /// @brief Checks response for list-commands
    ///
    /// This method checks if the list-commands response is generally sane
    /// and whether specified command is mentioned in the response.
    ///
    /// @param rsp response sent back by the server
    /// @param command command expected to be on the list.
    void checkListCommands(const ConstElementPtr& rsp, const std::string& command) {
        ConstElementPtr params;
Francis Dupont's avatar
Francis Dupont committed
236
        int status_code = -1;
237
238
239
240
241
242
        EXPECT_NO_THROW(params = parseAnswer(status_code, rsp));
        EXPECT_EQ(CONTROL_RESULT_SUCCESS, status_code);
        ASSERT_TRUE(params);
        ASSERT_EQ(Element::list, params->getType());

        int cnt = 0;
Francis Dupont's avatar
Francis Dupont committed
243
        for (size_t i = 0; i < params->size(); ++i) {
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
            string tmp = params->get(i)->stringValue();
            if (tmp == command) {
                // Command found, but that's not enough. Need to continue working
                // through the list to see if there are no duplicates.
                cnt++;
            }
        }

        // Exactly one command on the list is expected.
        EXPECT_EQ(1, cnt) << "Command " << command << " not found";
    }

    /// @brief Check if the answer for write-config command is correct
    ///
    /// @param response_txt response in text form (as read from the control socket)
    /// @param exp_status expected status (0 success, 1 failure)
    /// @param exp_txt for success cases this defines the expected filename,
    ///                for failure cases this defines the expected error message
    void checkConfigWrite(const std::string& response_txt, int exp_status,
                          const std::string& exp_txt = "") {

        ConstElementPtr rsp;
        EXPECT_NO_THROW(rsp = Element::fromJSON(response_txt));
        ASSERT_TRUE(rsp);

        int status;
        ConstElementPtr params = parseAnswer(status, rsp);
        EXPECT_EQ(exp_status, status);

        if (exp_status == CONTROL_RESULT_SUCCESS) {
            // Let's check couple things...

            // The parameters must include filename
            ASSERT_TRUE(params);
            ASSERT_TRUE(params->get("filename"));
Francis Dupont's avatar
Francis Dupont committed
279
            ASSERT_EQ(Element::string, params->get("filename")->getType());
280
281
282
283
284
            EXPECT_EQ(exp_txt, params->get("filename")->stringValue());

            // The parameters must include size. And the size
            // must indicate some content.
            ASSERT_TRUE(params->get("size"));
Francis Dupont's avatar
Francis Dupont committed
285
            ASSERT_EQ(Element::integer, params->get("size")->getType());
286
287
288
289
290
291
292
293
294
295
296
            int64_t size = params->get("size")->intValue();
            EXPECT_LE(1, size);

            // Now check if the file is really there and suitable for
            // opening.
            ifstream f(exp_txt, ios::binary | ios::ate);
            ASSERT_TRUE(f.good());

            // Now check that it is the correct size as reported.
            EXPECT_EQ(size, static_cast<int64_t>(f.tellg()));

297
298
299
            // Finally, check that it's really a JSON.
            ElementPtr from_file = Element::fromJSONFile(exp_txt);
            ASSERT_TRUE(from_file);
300
301
302
303
304
305
306
307
308
309
310
        } else if (exp_status == CONTROL_RESULT_ERROR) {

            // Let's check if the reason for failure was given.
            ConstElementPtr text = rsp->get("text");
            ASSERT_TRUE(text);
            ASSERT_EQ(Element::string, text->getType());
            EXPECT_EQ(exp_txt, text->stringValue());
        } else {
            ADD_FAILURE() << "Invalid expected status: " << exp_status;
        }
    }
311
312
};

313

314
315
TEST_F(CtrlDhcpv6SrvTest, commands) {

316
317
318
319
    boost::scoped_ptr<ControlledDhcpv6Srv> srv;
    ASSERT_NO_THROW(
        srv.reset(new ControlledDhcpv6Srv(DHCP6_SERVER_PORT + 10000))
    );
320

321
    // Use empty parameters list
322
323
324
    ElementPtr params(new isc::data::MapElement());
    int rcode = -1;

325
    // Case 1: send bogus command
Tomek Mrugalski's avatar
Tomek Mrugalski committed
326
327
    ConstElementPtr result = ControlledDhcpv6Srv::processCommand("blah", params);
    ConstElementPtr comment = isc::config::parseAnswer(rcode, result);
328
329
    EXPECT_EQ(1, rcode); // expect failure (no such command as blah)

330
    // Case 2: send shutdown command without any parameters
Tomek Mrugalski's avatar
Tomek Mrugalski committed
331
332
    result = ControlledDhcpv6Srv::processCommand("shutdown", params);
    comment = isc::config::parseAnswer(rcode, result);
333
334
335
336
337
338
    EXPECT_EQ(0, rcode); // expect success

    const pid_t pid(getpid());
    ConstElementPtr x(new isc::data::IntElement(pid));
    params->set("pid", x);

339
    // Case 3: send shutdown command with 1 parameter: pid
Tomek Mrugalski's avatar
Tomek Mrugalski committed
340
341
    result = ControlledDhcpv6Srv::processCommand("shutdown", params);
    comment = isc::config::parseAnswer(rcode, result);
342
    EXPECT_EQ(0, rcode); // Expect success
343
344
}

345
// Check that the "libreload" command will reload libraries
346
347
TEST_F(CtrlChannelDhcpv6SrvTest, libreload) {
    createUnixChannelServer();
Tomek Mrugalski's avatar
Tomek Mrugalski committed
348

349
350
351
352
353
    // Ensure no marker files to start with.
    ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
    ASSERT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));

    // Load two libraries
354
355
356
    HookLibsCollection libraries;
    libraries.push_back(make_pair(CALLOUT_LIBRARY_1, ConstElementPtr()));
    libraries.push_back(make_pair(CALLOUT_LIBRARY_2, ConstElementPtr()));
357
358
359
    HooksManager::loadLibraries(libraries);

    // Check they are loaded.
360
361
    HookLibsCollection loaded_libraries =
        HooksManager::getLibraryInfo();
362
363
364
    ASSERT_TRUE(libraries == loaded_libraries);

    // ... which also included checking that the marker file created by the
365
366
367
    // load functions exists and holds the correct value (of "12" - the
    // first library appends "1" to the file, the second appends "2"). Also
    // check that the unload marker file does not yet exist.
368
369
370
371
372
    EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "12"));
    EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));

    // Now execute the "libreload" command.  This should cause the libraries
    // to unload and to reload.
373
374
375
376
377
    std::string response;
    sendUnixCommand("{ \"command\": \"libreload\" }", response);
    EXPECT_EQ("{ \"result\": 0, "
              "\"text\": \"Hooks libraries successfully reloaded.\" }"
              , response);
378
379
380
381
382
383
384
385

    // Check that the libraries have unloaded and reloaded.  The libraries are
    // unloaded in the reverse order to which they are loaded.  When they load,
    // they should append information to the loading marker file.
    EXPECT_TRUE(checkMarkerFile(UNLOAD_MARKER_FILE, "21"));
    EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "1212"));
}

386
// Check that the "config-set" command will replace current configuration
387
TEST_F(CtrlChannelDhcpv6SrvTest, configSet) {
388
389
390
391
    createUnixChannelServer();

    // Define strings to permutate the config arguments
    // (Note the line feeds makes errors easy to find)
392
    string set_config_txt = "{ \"command\": \"config-set\" \n";
393
394
395
396
397
398
399
400
401
402
    string args_txt = " \"arguments\": { \n";
    string dhcp6_cfg_txt =
        "    \"Dhcp6\": { \n"
        "        \"interfaces-config\": { \n"
        "            \"interfaces\": [\"*\"] \n"
        "        },   \n"
        "        \"preferred-lifetime\": 3000, \n"
        "        \"valid-lifetime\": 4000, \n"
        "        \"renew-timer\": 1000, \n"
        "        \"rebind-timer\": 2000, \n"
403
404
405
406
407
408
        "        \"lease-database\": { \n"
        "           \"type\": \"memfile\", \n"
        "           \"persist\":false, \n"
        "           \"lfc-interval\": 0  \n"
        "        }, \n"
        "        \"expired-leases-processing\": { \n"
409
410
411
412
        "            \"reclaim-timer-wait-time\": 0, \n"
        "            \"hold-reclaimed-time\": 0, \n"
        "            \"flush-reclaimed-timer-wait-time\": 0 \n"
        "        },"
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
        "        \"subnet6\": [ \n";
    string subnet1 =
        "               {\"subnet\": \"3002::/64\", \n"
        "                \"pools\": [{ \"pool\": \"3002::100-3002::200\" }]}\n";
    string subnet2 =
        "               {\"subnet\": \"3003::/64\", \n"
        "                \"pools\": [{ \"pool\": \"3003::100-3003::200\" }]}\n";
    string bad_subnet =
        "               {\"BOGUS\": \"3005::/64\", \n"
        "                \"pools\": [{ \"pool\": \"3005::100-3005::200\" }]}\n";
    string subnet_footer =
        "          ] \n";
    string control_socket_header =
        "       ,\"control-socket\": { \n"
        "       \"socket-type\": \"unix\", \n"
        "       \"socket-name\": \"";
    string control_socket_footer =
        "\"   \n} \n";
    string logger_txt =
        "    \"Logging\": { \n"
        "        \"loggers\": [ { \n"
434
435
        "            \"name\": \"kea\", \n"
        "            \"severity\": \"FATAL\", \n"
436
        "            \"output_options\": [{ \n"
437
        "                \"output\": \"/dev/null\" \n"
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
        "            }] \n"
        "        }] \n"
        "    } \n";

    std::ostringstream os;

    // Create a valid config with all the parts should parse
    os << set_config_txt << ","
        << args_txt
        << dhcp6_cfg_txt
        << subnet1
        << subnet_footer
        << control_socket_header
        << socket_path_
        << control_socket_footer
        << "}\n"                      // close dhcp6
        << ","
455
456
        << logger_txt
        << "}}";
457

458
    // Send the config-set command
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
    std::string response;
    sendUnixCommand(os.str(), response);

    // Verify the configuration was successful.
    EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration successful.\" }",
              response);

    // Check that the config was indeed applied.
    const Subnet6Collection* subnets =
        CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();
    EXPECT_EQ(1, subnets->size());

    // Create a config with malformed subnet that should fail to parse.
    os.str("");
    os << set_config_txt << ","
        << args_txt
        << dhcp6_cfg_txt
        << bad_subnet
        << subnet_footer
        << control_socket_header
        << socket_path_
        << control_socket_footer
        << "}\n"                      // close dhcp6
482
        "}}";
483

484
    // Send the config-set command
485
486
487
488
    sendUnixCommand(os.str(), response);

    // Should fail with a syntax error
    EXPECT_EQ("{ \"result\": 1, "
489
              "\"text\": \"subnet configuration failed: mandatory 'subnet' parameter is missing for a subnet being configured (<string>:21:17)\" }",
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
              response);

    // Check that the config was not lost
    subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();
    EXPECT_EQ(1, subnets->size());

    // Create a valid config with two subnets and no command channel.
    // It should succeed but client will not receive a the response
    os.str("");
    os << set_config_txt << ","
        << args_txt
        << dhcp6_cfg_txt
        << subnet1
        << ",\n"
        << subnet2
        << subnet_footer
        << "}\n"                      // close dhcp6
507
        << "}}";
508

509
    // Verify the control channel socket exists.
510
511
    ASSERT_TRUE(fileExists(socket_path_));

512
    // Send the config-set command.
513
514
    sendUnixCommand(os.str(), response);

515
    // Verify the control channel socket no longer exists.
516
517
518
519
520
    EXPECT_FALSE(fileExists(socket_path_));

    // With no command channel, should still receive the response.
    EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration successful.\" }",
              response);
521
522
523
524
525
526
527
528
529

    // Check that the config was not lost
    subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();
    EXPECT_EQ(2, subnets->size());

    // Clean up after the test.
    CfgMgr::instance().clear();
}

Francis Dupont's avatar
Francis Dupont committed
530
  // Verify that the "config-test" command will do what we expect.
531
TEST_F(CtrlChannelDhcpv6SrvTest, configTest) {
Francis Dupont's avatar
Francis Dupont committed
532
533
534
535
    createUnixChannelServer();

    // Define strings to permutate the config arguments
    // (Note the line feeds makes errors easy to find)
536
    string set_config_txt = "{ \"command\": \"config-set\" \n";
Francis Dupont's avatar
Francis Dupont committed
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
    string config_test_txt = "{ \"command\": \"config-test\" \n";
    string args_txt = " \"arguments\": { \n";
    string dhcp6_cfg_txt =
        "    \"Dhcp6\": { \n"
        "        \"interfaces-config\": { \n"
        "            \"interfaces\": [\"*\"] \n"
        "        },   \n"
        "        \"preferred-lifetime\": 3000, \n"
        "        \"valid-lifetime\": 4000, \n"
        "        \"renew-timer\": 1000, \n"
        "        \"rebind-timer\": 2000, \n"
        "        \"lease-database\": { \n"
        "           \"type\": \"memfile\", \n"
        "           \"persist\":false, \n"
        "           \"lfc-interval\": 0  \n"
        "        }, \n"
        "        \"expired-leases-processing\": { \n"
        "            \"reclaim-timer-wait-time\": 0, \n"
        "            \"hold-reclaimed-time\": 0, \n"
        "            \"flush-reclaimed-timer-wait-time\": 0 \n"
        "        },"
        "        \"subnet6\": [ \n";
    string subnet1 =
        "               {\"subnet\": \"3002::/64\", \n"
        "                \"pools\": [{ \"pool\": \"3002::100-3002::200\" }]}\n";
    string subnet2 =
        "               {\"subnet\": \"3003::/64\", \n"
        "                \"pools\": [{ \"pool\": \"3003::100-3003::200\" }]}\n";
    string bad_subnet =
        "               {\"BOGUS\": \"3005::/64\", \n"
        "                \"pools\": [{ \"pool\": \"3005::100-3005::200\" }]}\n";
    string subnet_footer =
        "          ] \n";
    string control_socket_header =
        "       ,\"control-socket\": { \n"
        "       \"socket-type\": \"unix\", \n"
        "       \"socket-name\": \"";
    string control_socket_footer =
        "\"   \n} \n";
    string logger_txt =
        "    \"Logging\": { \n"
        "        \"loggers\": [ { \n"
        "            \"name\": \"kea\", \n"
        "            \"severity\": \"FATAL\", \n"
        "            \"output_options\": [{ \n"
        "                \"output\": \"/dev/null\" \n"
        "            }] \n"
        "        }] \n"
        "    } \n";

    std::ostringstream os;

    // Create a valid config with all the parts should parse
    os << set_config_txt << ","
        << args_txt
        << dhcp6_cfg_txt
        << subnet1
        << subnet_footer
        << control_socket_header
        << socket_path_
        << control_socket_footer
        << "}\n"                      // close dhcp6
        << ","
        << logger_txt
        << "}}";

603
    // Send the config-set command
Francis Dupont's avatar
Francis Dupont committed
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
    std::string response;
    sendUnixCommand(os.str(), response);

    // Verify the configuration was successful.
    EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration successful.\" }",
              response);

    // Check that the config was indeed applied.
    const Subnet6Collection* subnets =
        CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();
    EXPECT_EQ(1, subnets->size());

    // Create a config with malformed subnet that should fail to parse.
    os.str("");
    os << config_test_txt << ","
        << args_txt
        << dhcp6_cfg_txt
        << bad_subnet
        << subnet_footer
        << control_socket_header
        << socket_path_
        << control_socket_footer
        << "}\n"                      // close dhcp6
        "}}";

    // Send the config-test command
    sendUnixCommand(os.str(), response);

    // Should fail with a syntax error
    EXPECT_EQ("{ \"result\": 1, "
634
635
              "\"text\": \"subnet configuration failed: mandatory 'subnet' parameter "
              "is missing for a subnet being configured (<string>:21:17)\" }",
Francis Dupont's avatar
Francis Dupont committed
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
              response);

    // Check that the config was not lost
    subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();
    EXPECT_EQ(1, subnets->size());

    // Create a valid config with two subnets and no command channel.
    os.str("");
    os << config_test_txt << ","
        << args_txt
        << dhcp6_cfg_txt
        << subnet1
        << ",\n"
        << subnet2
        << subnet_footer
        << "}\n"                      // close dhcp6
        << "}}";

654
    // Verify the control channel socket exists.
Francis Dupont's avatar
Francis Dupont committed
655
656
    ASSERT_TRUE(fileExists(socket_path_));

657
    // Send the config-test command.
Francis Dupont's avatar
Francis Dupont committed
658
659
    sendUnixCommand(os.str(), response);

660
    // Verify the control channel socket still exists.
Francis Dupont's avatar
Francis Dupont committed
661
662
663
    EXPECT_TRUE(fileExists(socket_path_));

    // Verify the configuration was successful.
664
    EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration seems sane. "
Francis Dupont's avatar
Francis Dupont committed
665
666
              "Control-socket, hook-libraries, and D2 configuration were "
              "sanity checked, but not applied.\" }",
Francis Dupont's avatar
Francis Dupont committed
667
668
669
670
671
672
673
674
675
676
              response);

    // Check that the config was not applied.
    subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();
    EXPECT_EQ(1, subnets->size());

    // Clean up after the test.
    CfgMgr::instance().clear();
}

677
678
typedef std::map<std::string, isc::data::ConstElementPtr> ElementMap;

679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
// This test checks which commands are registered by the DHCPv4 server.
TEST_F(CtrlDhcpv6SrvTest, commandsRegistration) {

    ConstElementPtr list_cmds = createCommand("list-commands");
    ConstElementPtr answer;

    // By default the list should be empty (except the standard list-commands
    // supported by the CommandMgr itself)
    EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_cmds));
    ASSERT_TRUE(answer);
    ASSERT_TRUE(answer->get("arguments"));
    EXPECT_EQ("[ \"list-commands\" ]", answer->get("arguments")->str());

    // Created server should register several additional commands.
    boost::scoped_ptr<ControlledDhcpv6Srv> srv;
    ASSERT_NO_THROW(
        srv.reset(new ControlledDhcpv6Srv(0));
    );

    EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_cmds));
    ASSERT_TRUE(answer);
700

701
    ASSERT_TRUE(answer->get("arguments"));
702
703
704
    std::string command_list = answer->get("arguments")->str();

    EXPECT_TRUE(command_list.find("\"list-commands\"") != string::npos);
Francis Dupont's avatar
Francis Dupont committed
705
706
707
708
709
    EXPECT_TRUE(command_list.find("\"build-report\"") != string::npos);
    EXPECT_TRUE(command_list.find("\"config-get\"") != string::npos);
    EXPECT_TRUE(command_list.find("\"config-write\"") != string::npos);
    EXPECT_TRUE(command_list.find("\"leases-reclaim\"") != string::npos);
    EXPECT_TRUE(command_list.find("\"libreload\"") != string::npos);
710
    EXPECT_TRUE(command_list.find("\"config-set\"") != string::npos);
Francis Dupont's avatar
Francis Dupont committed
711
    EXPECT_TRUE(command_list.find("\"shutdown\"") != string::npos);
712
713
714
715
716
717
    EXPECT_TRUE(command_list.find("\"statistic-get\"") != string::npos);
    EXPECT_TRUE(command_list.find("\"statistic-get-all\"") != string::npos);
    EXPECT_TRUE(command_list.find("\"statistic-remove\"") != string::npos);
    EXPECT_TRUE(command_list.find("\"statistic-remove-all\"") != string::npos);
    EXPECT_TRUE(command_list.find("\"statistic-reset\"") != string::npos);
    EXPECT_TRUE(command_list.find("\"statistic-reset-all\"") != string::npos);
Francis Dupont's avatar
Francis Dupont committed
718
    EXPECT_TRUE(command_list.find("\"version-get\"") != string::npos);
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736

    // Ok, and now delete the server. It should deregister its commands.
    srv.reset();

    // The list should be (almost) empty again.
    EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_cmds));
    ASSERT_TRUE(answer);
    ASSERT_TRUE(answer->get("arguments"));
    EXPECT_EQ("[ \"list-commands\" ]", answer->get("arguments")->str());
}

// Tests that the server properly responds to invalid commands sent
// via ControlChannel
TEST_F(CtrlChannelDhcpv6SrvTest, controlChannelNegative) {
    createUnixChannelServer();
    std::string response;

    sendUnixCommand("{ \"command\": \"bogus\" }", response);
737
    EXPECT_EQ("{ \"result\": 2,"
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
              " \"text\": \"'bogus' command not supported.\" }", response);

    sendUnixCommand("utter nonsense", response);
    EXPECT_EQ("{ \"result\": 1, "
              "\"text\": \"error: unexpected character u in <string>:1:2\" }",
              response);
}

// Tests that the server properly responds to shtudown command sent
// via ControlChannel
TEST_F(CtrlChannelDhcpv6SrvTest, controlChannelShutdown) {
    createUnixChannelServer();
    std::string response;

    sendUnixCommand("{ \"command\": \"shutdown\" }", response);
    EXPECT_EQ("{ \"result\": 0, \"text\": \"Shutting down.\" }",response);
}

Francis Dupont's avatar
Francis Dupont committed
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
// This test verifies that the DHCP server handles version-get commands
TEST_F(CtrlChannelDhcpv6SrvTest, getversion) {
    createUnixChannelServer();

    std::string response;

    // Send the version-get command
    sendUnixCommand("{ \"command\": \"version-get\" }", response);
    EXPECT_TRUE(response.find("\"result\": 0") != string::npos);
    EXPECT_TRUE(response.find("log4cplus") != string::npos);
    EXPECT_FALSE(response.find("GTEST_VERSION") != string::npos);

    // Send the build-report command
    sendUnixCommand("{ \"command\": \"build-report\" }", response);
    EXPECT_TRUE(response.find("\"result\": 0") != string::npos);
    EXPECT_TRUE(response.find("GTEST_VERSION") != string::npos);
}

Francis Dupont's avatar
Francis Dupont committed
774
// This test verifies that the DHCP server immediately reclaims expired
Francis Dupont's avatar
Francis Dupont committed
775
776
777
778
// leases on leases-reclaim command
TEST_F(CtrlChannelDhcpv6SrvTest, controlLeasesReclaim) {
    createUnixChannelServer();

Francis Dupont's avatar
Francis Dupont committed
779
    // Create expired leases. Leases are expired by 40 seconds ago
Francis Dupont's avatar
Francis Dupont committed
780
    // (valid lifetime = 60, cltt = now - 100).
Francis Dupont's avatar
Francis Dupont committed
781
782
783
784
785
786
787
788
789
790
    DuidPtr duid0(new DUID(DUID::fromText("00:01:02:03:04:05:06").getDuid()));
    Lease6Ptr lease0(new Lease6(Lease::TYPE_NA, IOAddress("3000::1"),
                                duid0, 1, 50, 60, 10, 20, SubnetID(1)));
    lease0->cltt_ = time(NULL) - 100;
    DuidPtr duid1(new DUID(DUID::fromText("01:02:03:04:05:06:07").getDuid()));
    Lease6Ptr lease1(new Lease6(Lease::TYPE_NA, IOAddress("3000::2"),
                                duid1, 1, 50, 60, 10, 20, SubnetID(1)));
    lease1->cltt_ = time(NULL) - 100;

    // Add leases to the database.
791
    LeaseMgr& lease_mgr = LeaseMgrFactory::instance();
Francis Dupont's avatar
Francis Dupont committed
792
793
    ASSERT_NO_THROW(lease_mgr.addLease(lease0));
    ASSERT_NO_THROW(lease_mgr.addLease(lease1));
Francis Dupont's avatar
Francis Dupont committed
794

Francis Dupont's avatar
Francis Dupont committed
795
    // Make sure they have been added.
Francis Dupont's avatar
Francis Dupont committed
796
    ASSERT_TRUE(lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::1")));
Francis Dupont's avatar
Francis Dupont committed
797
    ASSERT_TRUE(lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::2")));
Francis Dupont's avatar
Francis Dupont committed
798

Francis Dupont's avatar
Francis Dupont committed
799
    // No arguments
Francis Dupont's avatar
Francis Dupont committed
800
801
    std::string response;
    sendUnixCommand("{ \"command\": \"leases-reclaim\" }", response);
Francis Dupont's avatar
Francis Dupont committed
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
    EXPECT_EQ("{ \"result\": 1, \"text\": "
              "\"Missing mandatory 'remove' parameter.\" }", response);

    // Bad argument name
    sendUnixCommand("{ \"command\": \"leases-reclaim\", "
                    "\"arguments\": { \"reclaim\": true } }", response);
    EXPECT_EQ("{ \"result\": 1, \"text\": "
              "\"Missing mandatory 'remove' parameter.\" }", response);

    // Bad remove argument type
    sendUnixCommand("{ \"command\": \"leases-reclaim\", "
                    "\"arguments\": { \"remove\": \"bogus\" } }", response);
    EXPECT_EQ("{ \"result\": 1, \"text\": "
              "\"'remove' parameter expected to be a boolean.\" }", response);

    // Send the command
    sendUnixCommand("{ \"command\": \"leases-reclaim\", "
                    "\"arguments\": { \"remove\": false } }", response);
    EXPECT_EQ("{ \"result\": 0, \"text\": "
              "\"Reclamation of expired leases is complete.\" }", response);

    // Leases should be reclaimed, but not removed
    ASSERT_NO_THROW(
        lease0 = lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::1"))
    );
    ASSERT_NO_THROW(
        lease1 = lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::2"))
    );
    ASSERT_TRUE(lease0);
    ASSERT_TRUE(lease1);
    EXPECT_TRUE(lease0->stateExpiredReclaimed());
    EXPECT_TRUE(lease1->stateExpiredReclaimed());
}

// This test verifies that the DHCP server immediately reclaims expired
// leases on leases-reclaim command with remove = true
TEST_F(CtrlChannelDhcpv6SrvTest, controlLeasesReclaimRemove) {
    createUnixChannelServer();
Francis Dupont's avatar
Francis Dupont committed
840

Francis Dupont's avatar
Francis Dupont committed
841
842
843
844
845
846
847
848
849
850
851
852
    // Create expired leases. Leases are expired by 40 seconds ago
    // (valid lifetime = 60, cltt = now - 100).
    DuidPtr duid0(new DUID(DUID::fromText("00:01:02:03:04:05:06").getDuid()));
    Lease6Ptr lease0(new Lease6(Lease::TYPE_NA, IOAddress("3000::1"),
                                duid0, 1, 50, 60, 10, 20, SubnetID(1)));
    lease0->cltt_ = time(NULL) - 100;
    DuidPtr duid1(new DUID(DUID::fromText("01:02:03:04:05:06:07").getDuid()));
    Lease6Ptr lease1(new Lease6(Lease::TYPE_NA, IOAddress("3000::2"),
                                duid1, 1, 50, 60, 10, 20, SubnetID(1)));
    lease1->cltt_ = time(NULL) - 100;

    // Add leases to the database.
853
    LeaseMgr& lease_mgr = LeaseMgrFactory::instance();
Francis Dupont's avatar
Francis Dupont committed
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
    ASSERT_NO_THROW(lease_mgr.addLease(lease0));
    ASSERT_NO_THROW(lease_mgr.addLease(lease1));

    // Make sure they have been added.
    ASSERT_TRUE(lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::1")));
    ASSERT_TRUE(lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::2")));

    // Send the command
    std::string response;
    sendUnixCommand("{ \"command\": \"leases-reclaim\", "
                    "\"arguments\": { \"remove\": true } }", response);
    EXPECT_EQ("{ \"result\": 0, \"text\": "
              "\"Reclamation of expired leases is complete.\" }", response);

    // Leases should have been removed.
    ASSERT_NO_THROW(
        lease0 = lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::1"))
    );
Francis Dupont's avatar
Francis Dupont committed
872
    ASSERT_NO_THROW(
Francis Dupont's avatar
Francis Dupont committed
873
        lease1 = lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::2"))
Francis Dupont's avatar
Francis Dupont committed
874
    );
Francis Dupont's avatar
Francis Dupont committed
875
876
    ASSERT_FALSE(lease0);
    ASSERT_FALSE(lease1);
Francis Dupont's avatar
Francis Dupont committed
877
878
}

879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
// Tests that the server properly responds to statistics commands.  Note this
// is really only intended to verify that the appropriate Statistics handler
// is called based on the command.  It is not intended to be an exhaustive
// test of Dhcpv6 statistics.
TEST_F(CtrlChannelDhcpv6SrvTest, controlChannelStats) {
    createUnixChannelServer();
    std::string response;

    // Check statistic-get
    sendUnixCommand("{ \"command\" : \"statistic-get\", "
                    "  \"arguments\": {"
                    "  \"name\":\"bogus\" }}", response);
    EXPECT_EQ("{ \"arguments\": {  }, \"result\": 0 }", response);

    // Check statistic-get-all
    sendUnixCommand("{ \"command\" : \"statistic-get-all\", "
                    "  \"arguments\": {}}", response);
    EXPECT_EQ("{ \"arguments\": {  }, \"result\": 0 }", response);

    // Check statistic-reset
    sendUnixCommand("{ \"command\" : \"statistic-reset\", "
                    "  \"arguments\": {"
                    "  \"name\":\"bogus\" }}", response);
    EXPECT_EQ("{ \"result\": 1, \"text\": \"No 'bogus' statistic found\" }",
              response);

    // Check statistic-reset-all
    sendUnixCommand("{ \"command\" : \"statistic-reset-all\", "
                    "  \"arguments\": {}}", response);
    EXPECT_EQ("{ \"result\": 0, \"text\": "
              "\"All statistics reset to neutral values.\" }", response);

    // Check statistic-remove
    sendUnixCommand("{ \"command\" : \"statistic-remove\", "
                    "  \"arguments\": {"
                    "  \"name\":\"bogus\" }}", response);
    EXPECT_EQ("{ \"result\": 1, \"text\": \"No 'bogus' statistic found\" }",
              response);

    // Check statistic-remove-all
    sendUnixCommand("{ \"command\" : \"statistic-remove-all\", "
                    "  \"arguments\": {}}", response);
    EXPECT_EQ("{ \"result\": 0, \"text\": \"All statistics removed.\" }",
              response);
}

925
926
927
928
929
930
931
932
933
934
935
936
// Tests that the server properly responds to shtudown command sent
// via ControlChannel
TEST_F(CtrlChannelDhcpv6SrvTest, commandsList) {
    createUnixChannelServer();
    std::string response;

    sendUnixCommand("{ \"command\": \"list-commands\" }", response);

    ConstElementPtr rsp;
    EXPECT_NO_THROW(rsp = Element::fromJSON(response));

    // We expect the server to report at least the following commands:
Francis Dupont's avatar
Francis Dupont committed
937
    checkListCommands(rsp, "build-report");
938
    checkListCommands(rsp, "config-get");
939
    checkListCommands(rsp, "config-set");
Francis Dupont's avatar
Francis Dupont committed
940
    checkListCommands(rsp, "config-test");
941
942
943
944
    checkListCommands(rsp, "config-write");
    checkListCommands(rsp, "list-commands");
    checkListCommands(rsp, "leases-reclaim");
    checkListCommands(rsp, "libreload");
Francis Dupont's avatar
Francis Dupont committed
945
    checkListCommands(rsp, "version-get");
946
947
948
949
950
951
952
953
954
    checkListCommands(rsp, "shutdown");
    checkListCommands(rsp, "statistic-get");
    checkListCommands(rsp, "statistic-get-all");
    checkListCommands(rsp, "statistic-remove");
    checkListCommands(rsp, "statistic-remove-all");
    checkListCommands(rsp, "statistic-reset");
    checkListCommands(rsp, "statistic-reset-all");
}

Francis Dupont's avatar
Francis Dupont committed
955
// Tests if the server returns its configuration using config-get.
956
// Note there are separate tests that verify if toElement() called by the
Francis Dupont's avatar
Francis Dupont committed
957
// get-config handler are actually converting the configuration correctly.
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
TEST_F(CtrlChannelDhcpv6SrvTest, configGet) {
    createUnixChannelServer();
    std::string response;

    sendUnixCommand("{ \"command\": \"config-get\" }", response);
    ConstElementPtr rsp;

    // The response should be a valid JSON.
    EXPECT_NO_THROW(rsp = Element::fromJSON(response));
    ASSERT_TRUE(rsp);

    int status;
    ConstElementPtr cfg = parseAnswer(status, rsp);
    EXPECT_EQ(CONTROL_RESULT_SUCCESS, status);

    // Ok, now roughly check if the response seems legit.
    ASSERT_TRUE(cfg);
    ASSERT_EQ(Element::map, cfg->getType());
    EXPECT_TRUE(cfg->get("Dhcp6"));
}

// Tests if config-write can be called without any parameters.
TEST_F(CtrlChannelDhcpv6SrvTest, configWriteNoFilename) {
    createUnixChannelServer();
    std::string response;

    // This is normally set by the command line -c parameter.
    server_->setConfigFile("test1.json");

    // If the filename is not explicitly specified, the name used
    // in -c command line switch is used.
    sendUnixCommand("{ \"command\": \"config-write\" }", response);

    checkConfigWrite(response, CONTROL_RESULT_SUCCESS, "test1.json");
    ::remove("test1.json");
}

// Tests if config-write can be called with a valid filename as parameter.
TEST_F(CtrlChannelDhcpv6SrvTest, configWriteFilename) {
    createUnixChannelServer();
    std::string response;

    sendUnixCommand("{ \"command\": \"config-write\", "