1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
// Copyright (C) 2017-2024 Internet Systems Consortium, Inc. ("ISC")
//
// 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/.

#include <config.h>
#include <agent/ca_cfg_mgr.h>
#include <agent/ca_command_mgr.h>
#include <agent/ca_controller.h>
#include <agent/ca_process.h>
#include <asiolink/asio_wrapper.h>
#include <asiolink/interval_timer.h>
#include <asiolink/io_service.h>
#include <asiolink/testutils/test_server_unix_socket.h>
#include <cc/command_interpreter.h>
#include <cc/data.h>
#include <process/testutils/d_test_stubs.h>
#include <boost/pointer_cast.hpp><--- Include file:  not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <gtest/gtest.h><--- Include file:  not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <testutils/sandbox.h>
#include <cstdlib><--- Include file:  not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <functional><--- Include file:  not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <vector><--- Include file:  not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <thread><--- Include file:  not found. Please note: Cppcheck does not need standard library headers to get proper results.

using namespace isc::agent;
using namespace isc::asiolink;
using namespace isc::data;
using namespace isc::process;

namespace {

/// @brief Test timeout in ms.
const long TEST_TIMEOUT = 10000;

/// @brief Test fixture class for @ref CtrlAgentCommandMgr.
///
/// @todo Add tests for various commands, including the cases when the
/// commands are forwarded to other servers via unix sockets.
/// Meanwhile, this is just a placeholder for the tests.
class CtrlAgentCommandMgrTest : public DControllerTest {
public:
    isc::test::Sandbox sandbox;

    /// @brief Constructor.
    ///
    /// Deregisters all commands except 'list-commands'.
    CtrlAgentCommandMgrTest()
        : DControllerTest(CtrlAgentController::instance),
          mgr_(CtrlAgentCommandMgr::instance()) {
        mgr_.deregisterAll();
        removeUnixSocketFile();
        initProcess();
    }

    /// @brief Destructor.
    ///
    /// Deregisters all commands except 'list-commands'.
    virtual ~CtrlAgentCommandMgrTest() {
        mgr_.deregisterAll();
        removeUnixSocketFile();
    }

    /// @brief Verifies received answer
    ///
    /// @todo Add better checks for failure cases and for
    /// verification of the response parameters.
    ///
    /// @param answer answer to be verified
    /// @param expected_code0 code expected to be returned in first result within
    /// the answer.
    /// @param expected_code1 code expected to be returned in second result within
    /// the answer.
    /// @param expected_code2 code expected to be returned in third result within
    /// the answer.
    void checkAnswer(const ConstElementPtr& answer, const int expected_code0 = 0,
                     const int expected_code1 = -1, const int expected_code2 = -1) {
        std::vector<int> expected_codes;
        if (expected_code0 >= 0) {
            expected_codes.push_back(expected_code0);
        }

        if (expected_code1 >= 0) {
            expected_codes.push_back(expected_code1);
        }

        if (expected_code2 >= 0) {
            expected_codes.push_back(expected_code2);
        }

        int status_code;
        // There may be multiple answers returned within a list.
        std::vector<ElementPtr> answer_list = answer->listValue();

        ASSERT_EQ(expected_codes.size(), answer_list.size());
        size_t count = 0;
        // Check all answers.
        for (auto const& ans : answer_list) {
            ConstElementPtr text;
            ASSERT_NO_THROW(text = isc::config::parseAnswer(status_code, ans));
            EXPECT_EQ(expected_codes[count], status_code)
                << "answer contains text: " << text->stringValue();
            count++;
        }
    }

    /// @brief Returns socket file path.
    ///
    /// If the KEA_SOCKET_TEST_DIR environment variable is specified, the
    /// socket file is created in the location pointed to by this variable.
    /// Otherwise, it is created in the build directory.
    std::string unixSocketFilePath() {
        std::string socket_path;
        const char* env = getenv("KEA_SOCKET_TEST_DIR");
        if (env) {
            socket_path = std::string(env) + "/test-socket";
        } else {
            socket_path = sandbox.join("test-socket");
        }
        return (socket_path);
    }

    /// @brief Removes unix socket descriptor.
    void removeUnixSocketFile() {
        static_cast<void>(remove(unixSocketFilePath().c_str()));
    }

    /// @brief Returns pointer to CtrlAgentProcess instance.
    CtrlAgentProcessPtr getCtrlAgentProcess() {
        return (boost::dynamic_pointer_cast<CtrlAgentProcess>(getProcess()));
    }

    /// @brief Returns pointer to CtrlAgentCfgMgr instance for a process.
    CtrlAgentCfgMgrPtr getCtrlAgentCfgMgr() {
        CtrlAgentCfgMgrPtr p;
        if (getCtrlAgentProcess()) {
            p = getCtrlAgentProcess()->getCtrlAgentCfgMgr();
        }
        return (p);
    }

    /// @brief Returns a pointer to the configuration context.
    CtrlAgentCfgContextPtr getCtrlAgentCfgContext() {
        CtrlAgentCfgContextPtr p;
        if (getCtrlAgentCfgMgr()) {
            p = getCtrlAgentCfgMgr()->getCtrlAgentCfgContext();
        }
        return (p);
    }

    /// @brief Adds configuration of the control socket.
    ///
    /// @param service Service for which socket configuration is to be added.
    void
    configureControlSocket(const std::string& service) {
        CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext();
        ASSERT_TRUE(ctx);

        ElementPtr control_socket = Element::createMap();
        control_socket->set("socket-name",
                            Element::create(unixSocketFilePath()));
        ctx->setControlSocketInfo(control_socket, service);
    }

    /// @brief Create and bind server side socket.
    ///
    /// @param response Stub response to be sent from the server socket to the
    /// client.
    /// @param use_thread Indicates if the IO service will be ran in thread.
    void bindServerSocket(const std::string& response,
                          const bool use_thread = false) {
        server_socket_.reset(new test::TestServerUnixSocket(getIOService(),
                                                            unixSocketFilePath(),
                                                            response));
        server_socket_->startTimer(TEST_TIMEOUT);
        server_socket_->bindServerSocket(use_thread);
    }

    /// @brief Creates command with no arguments.
    ///
    /// @param command_name Command name.
    /// @param service Service value to be added to the command. This value is
    /// specified as a list of comma separated values, e.g. "dhcp4, dhcp6".
    ///
    /// @return Pointer to the instance of the created command.
    ConstElementPtr createCommand(const std::string& command_name,
                                  const std::string& service) {
        ElementPtr command = Element::createMap();
        command->set("command", Element::create(command_name));

        // Only add the 'service' parameter if non-empty.
        if (!service.empty()) {
            std::string s = boost::replace_all_copy(service, ",", "\",\"");
            s = std::string("[ \"") + s + std::string("\" ]");
            command->set("service", Element::fromJSON(s));
        }

        command->set("arguments", Element::createMap());

        return (command);
    }

    /// @brief Test forwarding the command.
    ///
    /// @param server_type Server for which the client socket should be
    /// configured.
    /// @param service Service to be included in the command.
    /// @param expected_result0 Expected first result in response from the server.
    /// @param expected_result1 Expected second result in response from the server.
    /// @param expected_result2 Expected third result in response from the server.
    /// server socket after which the IO service should be stopped.
    /// @param expected_responses Number of responses after which the test finishes.
    /// @param server_response Stub response to be sent by the server.
    void testForward(const std::string& configured_service,
                     const std::string& service,
                     const int expected_result0,
                     const int expected_result1 = -1,
                     const int expected_result2 = -1,
                     const size_t expected_responses = 1,
                     const std::string& server_response = "{ \"result\": 0 }") {
        // Configure client side socket.
        configureControlSocket(configured_service);
        // Create server side socket.
        bindServerSocket(server_response, true);

        // The client side communication is synchronous. To be able to respond
        // to this we need to run the server side socket at the same time as the
        // client. Running IO service in a thread guarantees that the server
        //responds as soon as it receives the control command.
        std::thread th(std::bind(&IOService::run, getIOService().get()));


        // Wait for the IO service in thread to actually run.
        server_socket_->waitForRunning();

        ConstElementPtr command = createCommand("foo", service);
        ConstElementPtr answer = mgr_.processCommand(command);

        // Stop IO service immediately and let the thread die.
        getIOService()->stop();

        // Wait for the thread to finish.
        th.join();

        // Cancel all asynchronous operations on the server.
        server_socket_->stopServer();

        // We have some cancelled operations for which we need to invoke the
        // handlers with the operation_aborted error code.
        getIOService()->stopAndPoll(false);

        EXPECT_EQ(expected_responses, server_socket_->getResponseNum());
        checkAnswer(answer, expected_result0, expected_result1, expected_result2);
    }

    /// @brief a convenience reference to control agent command manager
    CtrlAgentCommandMgr& mgr_;

    /// @brief Pointer to the test server unix socket.
    test::TestServerUnixSocketPtr server_socket_;
};

/// Just a basic test checking that non-existent command is handled
/// properly.
TEST_F(CtrlAgentCommandMgrTest, bogus) {<--- syntax error
    ConstElementPtr answer;
    EXPECT_NO_THROW(answer = mgr_.processCommand(createCommand("fish-and-chips-please", "")));
    checkAnswer(answer, isc::config::CONTROL_RESULT_COMMAND_UNSUPPORTED);
};

// Test verifying that parameter other than command, arguments and service is
// rejected and that the correct error is returned.
TEST_F(CtrlAgentCommandMgrTest, extraParameter) {
    ElementPtr command = Element::createMap();
    command->set("command", Element::create("list-commands"));
    command->set("arguments", Element::createMap());
    command->set("extra-arg", Element::createMap());

    ConstElementPtr answer;
    EXPECT_NO_THROW(answer = mgr_.processCommand(command));
    checkAnswer(answer, isc::config::CONTROL_RESULT_ERROR);
}

/// Just a basic test checking that 'list-commands' is supported.
TEST_F(CtrlAgentCommandMgrTest, listCommands) {
    ConstElementPtr answer;
    EXPECT_NO_THROW(answer = mgr_.processCommand(createCommand("list-commands", "")));

    checkAnswer(answer, isc::config::CONTROL_RESULT_SUCCESS);
};

/// Check that control command is successfully forwarded to the DHCPv4 server.
TEST_F(CtrlAgentCommandMgrTest, forwardToDHCPv4Server) {
    testForward("dhcp4", "dhcp4", isc::config::CONTROL_RESULT_SUCCESS);
}

/// Check that control command is successfully forwarded to the DHCPv6 server.
TEST_F(CtrlAgentCommandMgrTest, forwardToDHCPv6Server) {
    testForward("dhcp6", "dhcp6", isc::config::CONTROL_RESULT_SUCCESS);
}

/// Check that control command is successfully forwarded to the D2 server.
TEST_F(CtrlAgentCommandMgrTest, forwardToD2Server) {
    testForward("d2", "d2", isc::config::CONTROL_RESULT_SUCCESS);
}

/// Check that the same command is forwarded to multiple servers.
TEST_F(CtrlAgentCommandMgrTest, forwardToBothDHCPServers) {
    configureControlSocket("dhcp6");

    testForward("dhcp4", "dhcp4,dhcp6", isc::config::CONTROL_RESULT_SUCCESS,
                isc::config::CONTROL_RESULT_SUCCESS, -1, 2);
}

/// Check that the same command is forwarded to all servers.
TEST_F(CtrlAgentCommandMgrTest, forwardToAllServers) {
    configureControlSocket("dhcp6");
    configureControlSocket("d2");

    testForward("dhcp4", "dhcp4,dhcp6,d2", isc::config::CONTROL_RESULT_SUCCESS,
                isc::config::CONTROL_RESULT_SUCCESS,
                isc::config::CONTROL_RESULT_SUCCESS, 3);
}

/// Check that the command may forwarded to the second server even if
/// forwarding to a first server fails.
TEST_F(CtrlAgentCommandMgrTest, failForwardToServer) {
    testForward("dhcp6", "dhcp4,dhcp6",
                isc::config::CONTROL_RESULT_ERROR,
                isc::config::CONTROL_RESULT_SUCCESS);
}

/// Check that control command is not forwarded if the service is not specified.
TEST_F(CtrlAgentCommandMgrTest, noService) {
    testForward("dhcp6", "",
                isc::config::CONTROL_RESULT_COMMAND_UNSUPPORTED,
                -1, -1, 0);
}

/// Check that error is returned to the client when the server to which the
/// command was forwarded sent an invalid message.
TEST_F(CtrlAgentCommandMgrTest, invalidAnswer) {
    testForward("dhcp6", "dhcp6",
                isc::config::CONTROL_RESULT_ERROR, -1, -1, 1,
                "{ \"result\": }");
}

/// Check that connection is dropped if it takes too long. The test checks
/// client's behavior when partial JSON is returned. Client will be waiting
/// for the '}' and will timeout because it is never received.
/// @todo Currently this test is disabled because we don't have configurable
/// timeout value. It is hardcoded to 5 sec, which is too long for the
/// unit test to run.
TEST_F(CtrlAgentCommandMgrTest, DISABLED_connectionTimeout) {
    testForward("dhcp6", "dhcp6",
                isc::config::CONTROL_RESULT_ERROR, -1, -1, 1,
                "{ \"result\": 0");
}

/// Check that error is returned to the client if the forwarding socket is
/// not configured for the given service.
TEST_F(CtrlAgentCommandMgrTest, noClientSocket) {
    ConstElementPtr command = createCommand("foo", "dhcp4");
    ConstElementPtr answer = mgr_.handleCommand("foo", ConstElementPtr(),
                                                command);

    checkAnswer(answer, isc::config::CONTROL_RESULT_ERROR);
}

/// Check that error is returned to the client if the remote server to
/// which the control command is to be forwarded is not available.
TEST_F(CtrlAgentCommandMgrTest, noServerSocket) {
    configureControlSocket("dhcp6");

    ConstElementPtr command = createCommand("foo", "dhcp6");
    ConstElementPtr answer = mgr_.handleCommand("foo", ConstElementPtr(),
                                                command);

    checkAnswer(answer, isc::config::CONTROL_RESULT_ERROR);
}

// Check that list-commands command is forwarded when the service
// value is specified.
TEST_F(CtrlAgentCommandMgrTest, forwardListCommands) {
    // Configure client side socket.
    configureControlSocket("dhcp4");
    // Create server side socket.
    bindServerSocket("{ \"result\" : 3 }", true);

    // The client side communication is synchronous. To be able to respond
    // to this we need to run the server side socket at the same time.
    // Running IO service in a thread guarantees that the server responds
    // as soon as it receives the control command.
    std::thread th(std::bind(&IOService::run, getIOService().get()));

    // Wait for the IO service in thread to actually run.
    server_socket_->waitForRunning();

    ConstElementPtr command = createCommand("list-commands", "dhcp4");
    ConstElementPtr answer = mgr_.handleCommand("list-commands", ConstElementPtr(),
                                                command);

    // Stop IO service immediately and let the thread die.
    getIOService()->stop();

    // Wait for the thread to finish.
    th.join();

    // Cancel all asynchronous operations on the server.
    server_socket_->stopServer();

    // We have some cancelled operations for which we need to invoke the
    // handlers with the operation_aborted error code.
    getIOService()->stopAndPoll(false);

    // Answer of 3 is specific to the stub response we send when the
    // command is forwarded. So having this value returned means that
    // the command was forwarded as expected.
    checkAnswer(answer, 3);
}

}