Browse Source

[3880] Unit-test for UNIX control socket written.

Tomek Mrugalski 10 years ago
parent
commit
fd66d0643b
3 changed files with 128 additions and 25 deletions
  1. 120 24
      src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc
  2. 5 0
      src/lib/dhcp/iface_mgr.cc
  3. 3 1
      src/lib/dhcp/iface_mgr.h

+ 120 - 24
src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc

@@ -72,6 +72,70 @@ public:
         static_cast<void>(unlink(LOAD_MARKER_FILE));
         static_cast<void>(unlink(LOAD_MARKER_FILE));
         static_cast<void>(unlink(UNLOAD_MARKER_FILE));
         static_cast<void>(unlink(UNLOAD_MARKER_FILE));
     }
     }
+
+    /// @brief sends commands over specified UNIX socket
+    ///
+    /// @param command command to be sent (should be valid JSON)
+    /// @param response response received (expected to be a valid JSON)
+    /// @param socket_path UNIX socket path
+    ///
+    /// @return true if send/response exchange was successful, false otherwise
+    bool sendCommandUnixSocket(const std::string& command,
+                               std::string& response,
+                               const std::string& socket_path) {
+
+        // Create UNIX socket
+        int socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
+        if (socket_fd < 0) {
+            ADD_FAILURE() << "Failed to open unix stream socket.";
+            return (false);
+        }
+
+        // Prepare socket address
+        struct sockaddr_un srv_addr;
+        memset(&srv_addr, 0, sizeof(struct sockaddr_un));
+        srv_addr.sun_family = AF_UNIX;
+        strncpy(srv_addr.sun_path, socket_path.c_str(), sizeof(srv_addr.sun_path));
+        socklen_t len = sizeof(srv_addr);
+
+        // Connect to the specified UNIX socket
+        int status = connect(socket_fd, (struct sockaddr*)&srv_addr, len);
+        if (status == -1) {
+            ADD_FAILURE() << "Failed to connect socket";
+            close(socket_fd);
+            return (false);
+        }
+
+
+        // Send command
+        cout << "Sending command: " << command << endl;
+        int bytes_sent = send(socket_fd, command.c_str(), command.length(), 0);
+        if (bytes_sent < command.length()) {
+            ADD_FAILURE() << "Failed to send " << command.length()
+                      << " bytes, send() returned " << bytes_sent;
+            close(socket_fd);
+            return (false);
+        }
+
+        // Receive response
+        /// @todo: this may block if server fails to respond. Some sort of
+        /// of a timer is needed.
+        char buf[65536];
+        memset(buf, 0, sizeof(buf));
+        int bytes_rcvd = recv(socket_fd, buf, sizeof(buf), 0);
+        if (bytes_rcvd < 0) {
+            ADD_FAILURE() << "Failed to receive a response. recv() returned "
+                      << bytes_rcvd;
+            close(socket_fd);
+            return (false);
+        }
+
+        // Convert the response to a string, close the socket and return
+        response = string(buf, bytes_rcvd);
+        cout << "Received response: " << response << endl;
+        close(socket_fd);
+        return (true);
+    }
 };
 };
 
 
 TEST_F(CtrlDhcpv4SrvTest, commands) {
 TEST_F(CtrlDhcpv4SrvTest, commands) {
@@ -193,7 +257,14 @@ TEST_F(CtrlDhcpv4SrvTest, commandsRegistration) {
 
 
 // Checks if the server is able to parse control socket configuration and
 // Checks if the server is able to parse control socket configuration and
 // configures the command socket properly.
 // configures the command socket properly.
-TEST_F(CtrlDhcpv4SrvTest, commandSocketBasic) {
+
+/// @todo: This unit-test is disabled, because it causes weird issues, when
+/// IfaceMgr::receive4() is called in a separate process. That's a side effect
+/// of how we run the test. We should either investigate why IfaceMgr doesn't
+/// work correctly after fork or develop a small tool that will send data
+/// from stdin to specified UNIX socket, print out the responses on stdout
+/// and develop shell tests for this.
+TEST_F(CtrlDhcpv4SrvTest, DISABLED_commandSocketBasic) {
 
 
     string socket_path = string(TEST_DATA_DIR) + "/kea4.sock";
     string socket_path = string(TEST_DATA_DIR) + "/kea4.sock";
 
 
@@ -211,35 +282,60 @@ TEST_F(CtrlDhcpv4SrvTest, commandSocketBasic) {
         "    \"control-socket\": {"
         "    \"control-socket\": {"
         "        \"socket-type\": \"unix\","
         "        \"socket-type\": \"unix\","
         "        \"socket-name\": \"" + socket_path + "\""
         "        \"socket-name\": \"" + socket_path + "\""
-        "    }"
+        "    },"
+        "    \"lease-database\": { \"type\": \"memfile\", \"persist\": false }"
         "}";
         "}";
 
 
-    boost::scoped_ptr<ControlledDhcpv4Srv> srv;
-    ASSERT_NO_THROW(
-        srv.reset(new ControlledDhcpv4Srv(0));
-    );
+    pid_t pid = fork();
+    if (pid > 0) {
+        cout << "Created child process: " << pid << endl;
 
 
-    ConstElementPtr config = Element::fromJSON(config_txt);
+        string command("{ \"command\": \"shutdown\" }");
+        string response;
 
 
-    ConstElementPtr answer = srv->processConfig(config);
-    ASSERT_TRUE(answer);
+        sleep(3);
 
 
-    int status = 0;
-    isc::config::parseAnswer(status, answer);
-    EXPECT_EQ(0, status);
+        EXPECT_TRUE(sendCommandUnixSocket(command, response, socket_path));
 
 
-    // Now check that the socket was indeed open.
-    EXPECT_TRUE(isc::config::CommandMgr::instance().getControlSocketFD() > 0);
-}
+        kill(pid, SIGTERM);
+        int status;
+        waitpid(pid, &status, 0);
 
 
-/// @todo: Implement system tests for the control socket.
-/// It is tricky in unit-tests, as it would require running two processes
-/// (one for the server itself and a second one for the test that sends
-/// command and receives an aswer).
-///
-/// Alternatively, we could use shell tests. It would be much simpler,
-/// but that requires using socat, a tool that is typically not installed.
-/// So we'd need a check in configure to check if it's available and
-/// fail configure process if missing (or disable the tests).
+        ASSERT_NE(0, response.length());
+
+        ConstElementPtr rsp;
+        EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+        ASSERT_TRUE(rsp);
+
+        int status_code;
+        ConstElementPtr comment = parseAnswer(status_code, rsp);
+        EXPECT_EQ(0, status_code);
+    } else {
+
+        IfaceMgr::instance().deleteAllExternalSockets();
+
+        boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+        ASSERT_NO_THROW(
+            srv.reset(new ControlledDhcpv4Srv(0));
+            );
+
+        ConstElementPtr config = Element::fromJSON(config_txt);
+
+        ConstElementPtr answer = srv->processConfig(config);
+        ASSERT_TRUE(answer);
+
+        int status = 0;
+        isc::config::parseAnswer(status, answer);
+        EXPECT_EQ(0, status);
+
+        // Now check that the socket was indeed open.
+        ASSERT_TRUE(isc::config::CommandMgr::instance().getControlSocketFD() > -1);
+
+        cout << "Child process: pid=" << pid << ", running server." << endl;
+        srv->run();
+
+        exit(EXIT_SUCCESS);
+    }
+}
 
 
 } // End of anonymous namespace
 } // End of anonymous namespace

+ 5 - 0
src/lib/dhcp/iface_mgr.cc

@@ -320,6 +320,11 @@ IfaceMgr::deleteExternalSocket(int socketfd) {
 }
 }
 
 
 void
 void
+IfaceMgr::deleteAllExternalSockets() {
+    callbacks_.clear();
+}
+
+void
 IfaceMgr::setPacketFilter(const PktFilterPtr& packet_filter) {
 IfaceMgr::setPacketFilter(const PktFilterPtr& packet_filter) {
     // Do not allow NULL pointer.
     // Do not allow NULL pointer.
     if (!packet_filter) {
     if (!packet_filter) {

+ 3 - 1
src/lib/dhcp/iface_mgr.h

@@ -973,9 +973,11 @@ public:
     void addExternalSocket(int socketfd, SocketCallback callback);
     void addExternalSocket(int socketfd, SocketCallback callback);
 
 
     /// @brief Deletes external socket
     /// @brief Deletes external socket
-
     void deleteExternalSocket(int socketfd);
     void deleteExternalSocket(int socketfd);
 
 
+    /// @brief Deletes all external sockets.
+    void deleteAllExternalSockets();
+
     /// @brief Set packet filter object to handle sending and receiving DHCPv4
     /// @brief Set packet filter object to handle sending and receiving DHCPv4
     /// messages.
     /// messages.
     ///
     ///