Browse Source

[5100] Implemented HookedCommandMgr class.

Marcin Siodelski 8 years ago
parent
commit
4e2fb1e163

+ 2 - 0
src/lib/config/Makefile.am

@@ -20,12 +20,14 @@ libkea_cfgclient_la_SOURCES += command_mgr.cc command_mgr.h
 libkea_cfgclient_la_SOURCES += command_socket.cc command_socket.h
 libkea_cfgclient_la_SOURCES += command_socket_factory.cc command_socket_factory.h
 libkea_cfgclient_la_SOURCES += config_log.h config_log.cc
+libkea_cfgclient_la_SOURCES += hooked_command_mgr.cc hooked_command_mgr.h
 
 libkea_cfgclient_la_LIBADD = $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
+libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la

+ 15 - 9
src/lib/config/base_command_mgr.cc

@@ -76,15 +76,7 @@ BaseCommandMgr::processCommand(const isc::data::ConstElementPtr& cmd) {
 
         LOG_INFO(command_logger, COMMAND_RECEIVED).arg(name);
 
-        HandlerContainer::const_iterator it = handlers_.find(name);
-        if (it == handlers_.end()) {
-            // Ok, there's no such command.
-            return (createAnswer(CONTROL_RESULT_ERROR,
-                                 "'" + name + "' command not supported."));
-        }
-
-        // Call the actual handler and return whatever it returned
-        return (it->second(name, arg));
+        return (handleCommand(name, arg));
 
     } catch (const Exception& e) {
         LOG_WARN(command_logger, COMMAND_PROCESS_ERROR2).arg(e.what());
@@ -94,6 +86,20 @@ BaseCommandMgr::processCommand(const isc::data::ConstElementPtr& cmd) {
     }
 }
 
+ConstElementPtr
+BaseCommandMgr::handleCommand(const std::string& cmd_name,
+                              const ConstElementPtr& params) {
+    auto it = handlers_.find(cmd_name);
+    if (it == handlers_.end()) {
+        // Ok, there's no such command.
+        return (createAnswer(CONTROL_RESULT_ERROR,
+                             "'" + cmd_name + "' command not supported."));
+    }
+
+    // Call the actual handler and return whatever it returned
+    return (it->second(cmd_name, params));
+}
+
 isc::data::ConstElementPtr
 BaseCommandMgr::listCommandsHandler(const std::string& name,
                                     const isc::data::ConstElementPtr& params) {

+ 17 - 1
src/lib/config/base_command_mgr.h

@@ -96,7 +96,7 @@ public:
     ///
     /// @param cmd Pointer to the data element representing command in JSON
     /// format.
-    virtual isc::data::ConstElementPtr
+    isc::data::ConstElementPtr
     processCommand(const isc::data::ConstElementPtr& cmd);
 
     /// @brief Registers specified command handler for a given command
@@ -118,6 +118,22 @@ public:
 
 protected:
 
+    /// @brief Handles the command having a given name and arguments.
+    ///
+    /// This method can be overriden in the derived classes to provide
+    /// custom logic for processing commands. For example, the
+    /// @ref HookedCommandMgr extends this method to delegate commmands
+    /// processing to a hook library.
+    ///
+    /// @param cmd_name Command name.
+    /// @param params Command arguments.
+    ///
+    /// @return Pointer to the const data element representing response
+    /// to a command.
+    virtual isc::data::ConstElementPtr
+    handleCommand(const std::string& cmd_name,
+                  const isc::data::ConstElementPtr& params);
+
     /// @brief Type of the container for command handlers.
     typedef std::map<std::string, CommandHandler> HandlerContainer;
 

+ 1 - 1
src/lib/config/command_mgr.cc

@@ -18,7 +18,7 @@ namespace isc {
 namespace config {
 
 CommandMgr::CommandMgr()
-    : BaseCommandMgr() {
+    : HookedCommandMgr() {
 }
 
 CommandSocketPtr

+ 2 - 2
src/lib/config/command_mgr.h

@@ -8,7 +8,7 @@
 #define COMMAND_MGR_H
 
 #include <cc/data.h>
-#include <config/base_command_mgr.h>
+#include <config/hooked_command_mgr.h>
 #include <config/command_socket.h>
 #include <boost/noncopyable.hpp>
 #include <list>
@@ -20,7 +20,7 @@ namespace config {
 ///
 /// This class extends @ref BaseCommandMgr with the ability to receive and
 /// respond to commands over unix domain sockets.
-class CommandMgr : public BaseCommandMgr, public boost::noncopyable {
+class CommandMgr : public HookedCommandMgr, public boost::noncopyable {
 public:
 
     /// @brief CommandMgr is a singleton class. This method returns reference

+ 7 - 0
src/lib/config/config_messages.mes

@@ -11,6 +11,13 @@ This debug message indicates that the daemon stopped supporting specified
 command. This command can no longer be issued. If the command socket is
 open and this command is issued, the daemon will not be able to process it.
 
+% COMMAND_HOOK_RECEIVE_SKIP command %1 has been handled by the hook library which returned the skip state
+This debug message is issued when a hook library has processed the control
+command and returned the skip status. The callout should have set the
+'response' argument which contains the result of processing the command.
+The Command Manager skips processing of this command and simply returns
+the response generated by the hook library.
+
 % COMMAND_PROCESS_ERROR1 Error while processing command: %1
 This warning message indicates that the server encountered an error while
 processing received command. Additional information will be provided, if

+ 78 - 0
src/lib/config/hooked_command_mgr.cc

@@ -0,0 +1,78 @@
+// Copyright (C) 2017 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/hooked_command_mgr.h>
+#include <config/config_log.h>
+#include <hooks/hooks_manager.h>
+
+using namespace isc::data;
+using namespace isc::hooks;
+
+namespace {
+
+/// @brief Structure that holds registered hook indexes.
+struct CommandMgrHooks {
+    /// @brief Index for "control_command_receive" hook point.
+    int hook_index_control_command_receive_;
+
+    /// @brief Constructor that registers hook points for HookedCommandMgr
+    CommandMgrHooks() {
+        hook_index_control_command_receive_ =
+            HooksManager::registerHook("control_command_receive");
+    }
+};
+
+// Declare a hooks object. As this is outside any function or method, it
+// will be instantiated (and the constructor run) when the module is loaded.
+// As a result, the hook indexes will be defined before any method in this
+// module is called.
+CommandMgrHooks Hooks;
+
+} // end of anonymous namespace
+
+namespace isc {
+namespace config {
+
+HookedCommandMgr::HookedCommandMgr()
+    : BaseCommandMgr(), callout_handle_(HooksManager::createCalloutHandle()) {
+}
+
+ConstElementPtr
+HookedCommandMgr::handleCommand(const std::string& cmd_name,
+                                const ConstElementPtr& params) {
+    if (!callout_handle_) {
+        isc_throw(Unexpected, "callout handle not configured for the Command "
+                  "Manager: this is a programming error");
+    }
+
+    if (HooksManager::calloutsPresent(Hooks.hook_index_control_command_receive_)) {
+
+        // Delete previously set arguments.
+        callout_handle_->deleteAllArguments();
+
+        callout_handle_->setArgument("command", cmd_name);
+        callout_handle_->setArgument("arguments", params);
+
+        HooksManager::callCallouts(Hooks.hook_index_control_command_receive_,
+                                   *callout_handle_);
+
+        if (callout_handle_->getStatus() == CalloutHandle::NEXT_STEP_SKIP) {
+            ConstElementPtr response;
+            callout_handle_->getArgument("response", response);
+            return (response);
+
+        } else {
+            LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_HOOK_RECEIVE_SKIP)
+                .arg(cmd_name);
+        }
+    }
+
+    return (BaseCommandMgr::handleCommand(cmd_name, params));
+}
+
+
+} // end of namespace isc::config
+} // end of namespace isc

+ 70 - 0
src/lib/config/hooked_command_mgr.h

@@ -0,0 +1,70 @@
+// Copyright (C) 2017 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/.
+
+#ifndef HOOKED_COMMAND_MGR_H
+#define HOOKED_COMMAND_MGR_H
+
+#include <cc/data.h>
+#include <config/base_command_mgr.h>
+#include <hooks/callout_handle.h>
+
+namespace isc {
+namespace config {
+
+/// @brief Command Manager which can delegate commands to a hook library.
+///
+/// This class extends @ref BaseCommandMgr with the logic to delegate the
+/// commands to a hook library if the hook library is installed and provides
+/// callouts for the control API.
+class HookedCommandMgr : public BaseCommandMgr {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Initializes callout handle used by the Command Manager.
+    HookedCommandMgr();
+
+protected:
+
+    /// @brief Returns callout handle to the derived class.
+    ///
+    /// @return const pointer to the callout handle.
+    const isc::hooks::CalloutHandlePtr& getCalloutHandle() const {
+        return (callout_handle_);
+    }
+
+private:
+
+    /// @brief Handles the command having a given name and arguments.
+    ///
+    /// This method checks if the hook library is installed which implements
+    /// callouts for the 'control_command_receive' hook point, and calls them
+    /// if they exist. If the hook library supports the given command it creates
+    /// a response and returns it in the 'response' argument of the
+    /// @ref ConstElementPtr type. If the callout also sets the 'skip' status,
+    /// the response created by the callout is returned. Otherwise, the
+    /// @ref BaseCommandMgr::handleCommand is called.
+    ///
+    /// This method is private because it is its final implementation which
+    /// should not be overriden in the derived classes.
+    ///
+    /// @param cmd_name Command name.
+    /// @param params Command arguments.
+    ///
+    /// @return Pointer to the const data element representing response
+    /// to a command.
+    virtual isc::data::ConstElementPtr
+    handleCommand(const std::string& cmd_name,
+                  const isc::data::ConstElementPtr& params);
+
+    /// @brief Pointer to a callout handle used by this class.
+    isc::hooks::CalloutHandlePtr callout_handle_;
+};
+
+} // end of namespace isc::config
+} // end of namespace isc
+
+#endif

+ 1 - 0
src/lib/config/tests/Makefile.am

@@ -32,6 +32,7 @@ run_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
 run_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la
 run_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
 run_unittests_LDADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
+run_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
 run_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
 run_unittests_LDADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la
 run_unittests_LDADD += $(top_builddir)/src/lib/util/unittests/libutil_unittests.la

+ 136 - 3
src/lib/config/tests/command_mgr_unittests.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2015-2017 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
@@ -7,10 +7,17 @@
 #include <gtest/gtest.h>
 
 #include <config/command_mgr.h>
+#include <config/hooked_command_mgr.h>
 #include <cc/command_interpreter.h>
+#include <hooks/hooks_manager.h>
+#include <hooks/callout_handle.h>
+#include <hooks/library_handle.h>
+#include <string>
+#include <vector>
 
-using namespace isc::data;
 using namespace isc::config;
+using namespace isc::data;
+using namespace isc::hooks;
 using namespace std;
 
 // Test class for Command Manager
@@ -25,12 +32,23 @@ public:
 
         CommandMgr::instance().deregisterAll();
         CommandMgr::instance().closeCommandSocket();
+
+        resetCalloutIndicators();
     }
 
     /// Default destructor
-    ~CommandMgrTest() {
+    virtual ~CommandMgrTest() {
         CommandMgr::instance().deregisterAll();
         CommandMgr::instance().closeCommandSocket();
+        resetCalloutIndicators();
+        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+                "control_command_receive");
+    }
+
+    /// @brief Resets indicators related to callout invocation.
+    static void resetCalloutIndicators() {
+        callout_name = "";
+        callout_argument_names.clear();
     }
 
     /// @brief A simple command handler that always returns an eror
@@ -44,6 +62,53 @@ public:
         return (createAnswer(123, "test error message"));
     }
 
+    /// @brief Test callback which stores callout name and passed arguments.
+    ///
+    /// This callout doesn't indicate that the command has been processed,
+    /// allowing the Command Manager to process it.
+    ///
+    /// @param callout_handle Handle passed by the hooks framework.
+    /// @return Always 0.
+    static int
+    control_command_receive_callout(CalloutHandle& callout_handle) {
+        callout_name = "control_command_receive";
+
+        ConstElementPtr response;
+        callout_handle.setArgument("response", response);
+
+        callout_argument_names = callout_handle.getArgumentNames();
+        // Sort arguments alphabetically, so as we can access them on
+        // expected positions and verify.
+        std::sort(callout_argument_names.begin(), callout_argument_names.end());
+        return (0);
+    }
+
+    /// @brief Text callback which stores callout name and passed arguments and
+    /// which handles the command.
+    ///
+    /// This callout returns the skip status to indicate the the command has
+    /// been handled.
+    ///
+    /// @param callout_handle Handle passed by the hooks framework.
+    /// @return Always 0.
+    static int
+    control_command_receive_handle_callout(CalloutHandle& callout_handle) {
+        callout_name = "control_command_receive";
+
+        ConstElementPtr response = createAnswer(CONTROL_RESULT_SUCCESS,
+                                                "text produced by the callout");
+        callout_handle.setArgument("response", response);
+
+        // Set 'skip' status to indicate that the command has been handled.
+        callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+
+        callout_argument_names = callout_handle.getArgumentNames();
+        // Sort arguments alphabetically, so as we can access them on
+        // expected positions and verify.
+        std::sort(callout_argument_names.begin(), callout_argument_names.end());
+        return (0);
+    }
+
     /// @brief Name of the command (used in my_handler)
     static std::string handler_name;
 
@@ -52,6 +117,12 @@ public:
 
     /// @brief Indicates whether my_handler was called
     static bool handler_called;
+
+    /// @brief Holds invoked callout name.
+    static std::string callout_name;
+
+    /// @brief Holds a list of arguments passed to the callout.
+    static std::vector<std::string> callout_argument_names;
 };
 
 /// Name passed to the handler (used in my_handler)
@@ -63,6 +134,12 @@ ConstElementPtr CommandMgrTest::handler_params;
 /// Indicates whether my_handler was called
 bool CommandMgrTest::handler_called(false);
 
+/// Holds invoked callout name.
+std::string CommandMgrTest::callout_name("");
+
+/// Holds a list of arguments passed to the callout.
+std::vector<std::string> CommandMgrTest::callout_argument_names;
+
 // Test checks whether the internal command 'list-commands'
 // is working properly.
 TEST_F(CommandMgrTest, listCommandsEmpty) {
@@ -186,6 +263,11 @@ TEST_F(CommandMgrTest, deregisterAll) {
 // runs through processCommand to check that it's indeed called.
 TEST_F(CommandMgrTest, processCommand) {
 
+    // Register callout so as we can check that it is called before
+    // processing the command by the manager.
+    HooksManager::preCalloutsLibraryHandle().registerCallout(
+        "control_command_receive", control_command_receive_callout);
+
     // Install my handler
     EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command",
                                                            my_handler));
@@ -212,4 +294,55 @@ TEST_F(CommandMgrTest, processCommand) {
     EXPECT_EQ("my-command", handler_name);
     ASSERT_TRUE(handler_params);
     EXPECT_EQ("[ \"just\", \"some\", \"data\" ]", handler_params->str());
+
+    EXPECT_EQ("control_command_receive", callout_name);
+
+    // Check that the appropriate arguments have been set. Include the
+    // 'response' which should have been set by the callout.
+    ASSERT_EQ(3, callout_argument_names.size());
+    EXPECT_EQ("arguments", callout_argument_names[0]);
+    EXPECT_EQ("command", callout_argument_names[1]);
+    EXPECT_EQ("response", callout_argument_names[2]);
+}
+
+// Verify that processing a command can be delegated to a hook library.
+TEST_F(CommandMgrTest, delegateProcessCommand) {
+    // Register callout so as we can check that it is called before
+    // processing the command by the manager.
+    HooksManager::preCalloutsLibraryHandle().registerCallout(
+        "control_command_receive", control_command_receive_handle_callout);
+
+    // Install local handler
+    EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command",
+                                                           my_handler));
+
+    // Now tell CommandMgr to process a command 'my-command' with the
+    // specified parameter.
+    ElementPtr my_params = Element::fromJSON("[ \"just\", \"some\", \"data\" ]");
+    ConstElementPtr command = createCommand("my-command", my_params);
+    ConstElementPtr answer;
+    ASSERT_NO_THROW(answer = CommandMgr::instance().processCommand(command));
+
+    // There should be an answer.
+    ASSERT_TRUE(answer);
+
+    // Local handler shouldn't be called because the command is handled by the
+    // hook library.
+    ASSERT_FALSE(handler_called);
+
+    // Returned status should be unique for the hook library.
+    ConstElementPtr answer_arg;
+    int status_code;
+    ASSERT_NO_THROW(answer_arg = parseAnswer(status_code, answer));
+    EXPECT_EQ(0, status_code);
+    EXPECT_EQ("text produced by the callout", answer_arg->stringValue());
+
+    EXPECT_EQ("control_command_receive", callout_name);
+
+    // Check that the appropriate arguments have been set. Include the
+    // 'response' which should have been set by the callout.
+    ASSERT_EQ(3, callout_argument_names.size());
+    EXPECT_EQ("arguments", callout_argument_names[0]);
+    EXPECT_EQ("command", callout_argument_names[1]);
+    EXPECT_EQ("response", callout_argument_names[2]);
 }