// Copyright (C) 2018-2023 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace isc::asiolink; using namespace isc::config; using namespace isc::data; using namespace isc::dhcp; using namespace isc::ha; using namespace isc::ha::test; using namespace isc::hooks; namespace { /// @brief Structure that holds registered hook indexes. /// /// This allows us to park packets. struct TestHooks { /// @brief Index of leases4_committed callout. int hook_index_leases4_committed_; /// @brief Index of leases6_committed callout. int hook_index_leases6_committed_; /// @brief Constructor /// /// The constructor registers hook points for callout tests. TestHooks() { hook_index_leases4_committed_ = HooksManager::registerHook("leases4_committed"); hook_index_leases6_committed_ = HooksManager::registerHook("leases6_committed"); } }; TestHooks test_hooks; /// @brief Derivation of the @c HAImpl which provides access to protected /// methods and members. class TestHAImpl : public HAImpl { public: using HAImpl::config_; using HAImpl::service_; }; /// @brief Test fixture class for @c HAImpl. class HAImplTest : public HATest { public: /// @brief Tests handler of a ha-sync command. /// /// It always expects that the error result is returned. The expected /// error text should be provided as function argument. /// /// @param ha_sync_command command provided as text. /// @param expected_response expected text response. void testSynchronizeHandler(const std::string& ha_sync_command, const std::string& expected_response) { HAImpl ha_impl; ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration())); // Starting the service is required prior to running any callouts. NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4)); ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state, HAServerType::DHCPv4)); ConstElementPtr command = Element::fromJSON(ha_sync_command); CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle(); callout_handle->setArgument("command", command); ASSERT_NO_THROW(ha_impl.synchronizeHandler(*callout_handle)); ConstElementPtr response; callout_handle->getArgument("response", response); ASSERT_TRUE(response); checkAnswer(response, CONTROL_RESULT_ERROR, expected_response); } }; // Tests that HAService object is created for DHCPv4 service. TEST_F(HAImplTest, startService) { // Valid configuration must be provided prior to starting the service. TestHAImpl ha_impl; ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration())); // Network state is also required. NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4)); // Start the service for DHCPv4 server. ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state, HAServerType::DHCPv4)); // Make sure that the HA service has been created for the requested // server type. ASSERT_TRUE(ha_impl.service_); EXPECT_EQ(HAServerType::DHCPv4, ha_impl.service_->getServerType()); } // Tests that HAService object is created for DHCPv6 service. TEST_F(HAImplTest, startService6) { // Valid configuration must be provided prior to starting the service. TestHAImpl ha_impl; ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration())); // Network state is also required. NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv6)); // Start the service for DHCPv4 server. ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state, HAServerType::DHCPv6)); // Make sure that the HA service has been created for the requested // server type. ASSERT_TRUE(ha_impl.service_); EXPECT_EQ(HAServerType::DHCPv6, ha_impl.service_->getServerType()); } // Tests for buffer4_receive callout implementation. TEST_F(HAImplTest, buffer4Receive) { // Use hot-standby mode to make sure that this server instance is selected // to process each received query. This is going to give predictable results. ConstElementPtr ha_config = createValidJsonConfiguration(HAConfig::HOT_STANDBY); // Create implementation object and configure it. TestHAImpl ha_impl; ASSERT_NO_THROW(ha_impl.configure(ha_config)); // Starting the service is required prior to running any callouts. NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4)); ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state, HAServerType::DHCPv4)); // Initially the HA service is in the waiting state and serves no scopes. // We need to explicitly enable the scope to be served. ha_impl.service_->serveDefaultScopes(); // Create callout handle to be used for passing arguments to the // callout. CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle(); ASSERT_TRUE(callout_handle); // Create the BOOTP message. We can use it for testing message parsing // failure case because BOOTP is not supported. We will later turn it // into the DHCP message to test successful parsing. std::vector msg = { 1, // BOOTREQUEST 1, // ethernet 6, // HW address length = 6 0, // hops = 0 1, 2, 3, 4, // xid 0, 0, // secs = 0 0, 0, // flags 0, 0, 0, 0, // ciaddr = 0 0, 0, 0, 0, // yiaddr = 0 0, 0, 0, 0, // siaddr = 0 0, 0, 0, 0, // giaddr = 0 1, 2, 3, 4, 5, 6, // chaddr }; // fill chaddr reminder, sname and file with zeros msg.insert(msg.end(), 10 + 128 + 64, 0); // Create DHCPv4 message object from the BOOTP message. This should be // successful because the message is not parsed yet. Pkt4Ptr query4(new Pkt4(&msg[0], msg.size())); // Set buffer4_receive callout arguments. callout_handle->setArgument("query4", query4); // Invoke the buffer4_receive callout. ASSERT_NO_THROW(ha_impl.buffer4Receive(*callout_handle)); // The BOOTP messages are not supported so trying to unpack the message // should trigger an error. The callout should set the next step to // DROP treating the message as malformed. EXPECT_EQ(CalloutHandle::NEXT_STEP_DROP, callout_handle->getStatus()); // Malformed message should not be classified. EXPECT_TRUE(query4->getClasses().empty()); // Turn this into the DHCP message by appending a magic cookie and the // options. std::vector magic_cookie = { 99, 130, 83, 99 }; // Provide DHCP message type option, truncated vendor option and domain name. // Parsing this message should be successful but vendor option should be // skipped. std::vector options = { 53, 1, 1, // Message type = DHCPDISCOVER 125, 6, // vendor options 1, 2, 3, 4, // enterprise id 8, 1, // data len 8 but the actual length is 1 (truncated options) 15, 3, 'a', 'b', 'c' // Domain name = abc }; // Append the magic cookie and the options to our BOOTP message. msg.insert(msg.end(), magic_cookie.begin(), magic_cookie.end()); msg.insert(msg.end(), options.begin(), options.end()); // Create new query and pass it to the callout. query4.reset(new Pkt4(&msg[0], msg.size())); callout_handle->setArgument("query4", query4); // Invoke the callout again. ASSERT_NO_THROW(ha_impl.buffer4Receive(*callout_handle)); // This time the callout should set the next step to SKIP to indicate to // the DHCP server that the message has been already parsed. EXPECT_EQ(CalloutHandle::NEXT_STEP_SKIP, callout_handle->getStatus()); // The client class should be assigned to the message to indicate that the // server1 should process this message. ASSERT_EQ(1, query4->getClasses().size()); EXPECT_TRUE(query4->inClass("HA_server1")); // Check that the message has been parsed. The DHCP message type should // be set in this case. EXPECT_EQ(DHCPDISCOVER, static_cast(query4->getType())); // Vendor option should be skipped because it was truncated. EXPECT_FALSE(query4->getOption(DHO_VIVSO_SUBOPTIONS)); // Domain name should not be skipped because the vendor option was truncated. EXPECT_TRUE(query4->getOption(DHO_DOMAIN_NAME)); } // Tests for buffer6_receive callout implementation. TEST_F(HAImplTest, buffer6Receive) { // Use hot-standby mode to make sure that this server instance is selected // to process each received query. This is going to give predictable results. ConstElementPtr ha_config = createValidJsonConfiguration(HAConfig::HOT_STANDBY); // Create implementation object and configure it. TestHAImpl ha_impl; ASSERT_NO_THROW(ha_impl.configure(ha_config)); // Starting the service is required prior to running any callouts. NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv6)); ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state, HAServerType::DHCPv6)); // Initially the HA service is in the waiting state and serves no scopes. // We need to explicitly enable the scope to be served. ha_impl.service_->serveDefaultScopes(); // Create callout handle to be used for passing arguments to the // callout. CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle(); ASSERT_TRUE(callout_handle); // Create DHCPv6 message. It initially has no transaction id so should be // considered malformed. std::vector msg = { 1, // Solicit }; // Create DHCPv4 message object from the BOOTP message. This should be // successful because the message is not parsed yet. Pkt6Ptr query6(new Pkt6(&msg[0], msg.size())); // Set buffer6_receive callout arguments. callout_handle->setArgument("query6", query6); // Invoke the buffer6_receive callout. ASSERT_NO_THROW(ha_impl.buffer6Receive(*callout_handle)); // Our DHCP messages contains no transaction id so it should cause // parsing error. The next step is set to DROP for malformed messages. EXPECT_EQ(CalloutHandle::NEXT_STEP_DROP, callout_handle->getStatus()); // Malformed message should not be classified. EXPECT_TRUE(query6->getClasses().empty()); // Append transaction id (3 bytes, each set to 1). msg.insert(msg.end(), 3, 1); // Include 3 options in the DHCPv6 message: ORO, truncated vendor option // and the NIS Domain Name option. This should be parsed correctly but the // last option should be skipped because of the preceding option being // truncated. std::vector options = { 0, 6, 0, 2, 0, 29, // option ORO requesting option 29 0, 17, // vendor options 0, 9, // option length = 10 1, 2, 3, 4, // enterprise id 0, 1, 0, 10, // code 1, (invalid) length = 10 1, // ONLY 1 byte of data (truncated) 0, 29, 0, 3, 'a', 'b', 'c' // NIS Domain Name = abc }; msg.insert(msg.end(), options.begin(), options.end()); // Create new query and pass it to the callout. query6.reset(new Pkt6(&msg[0], msg.size())); callout_handle->setArgument("query6", query6); // Invoke the callout again. ASSERT_NO_THROW(ha_impl.buffer6Receive(*callout_handle)); // This time the callout should set the next step to SKIP to indicate to // the DHCP server that the message has been already parsed. EXPECT_EQ(CalloutHandle::NEXT_STEP_SKIP, callout_handle->getStatus()); // The client class should be assigned to the message to indicate that the // server1 should process this message. ASSERT_EQ(1, query6->getClasses().size()); EXPECT_TRUE(query6->inClass("HA_server1")); // Check that the message has been parsed. The DHCP message type should // be set in this case. EXPECT_EQ(DHCPV6_SOLICIT, static_cast(query6->getType())); // Domain name should be skipped because the vendor option was truncated. EXPECT_FALSE(query6->getOption(D6O_NIS_DOMAIN_NAME)); } // Tests leases4_committed callout implementation. TEST_F(HAImplTest, leases4Committed) { // Create implementation object and configure it. TestHAImpl ha_impl; ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration())); // Starting the service is required prior to running any callouts. NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4)); ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state, HAServerType::DHCPv4)); // Make sure we wait for the acks from the backup server to be able to // test the case of sending lease updates even though the service is // in the state in which the lease updates are normally not sent. ha_impl.config_->setWaitBackupAck(true); // Create callout handle to be used for passing arguments to the // callout. CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle(); ASSERT_TRUE(callout_handle); // Set the hook index so we can park packets. callout_handle->setCurrentHook(test_hooks.hook_index_leases4_committed_); // query4 Pkt4Ptr query4 = createMessage4(DHCPREQUEST, 1, 0, 0); callout_handle->setArgument("query4", query4); // leases4 Lease4CollectionPtr leases4(new Lease4Collection()); callout_handle->setArgument("leases4", leases4); // deleted_leases4 Lease4CollectionPtr deleted_leases4(new Lease4Collection()); callout_handle->setArgument("deleted_leases4", deleted_leases4); // Set initial status. callout_handle->setStatus(CalloutHandle::NEXT_STEP_CONTINUE); // Park the packet. HooksManager::park("leases4_committed", query4, []{}); // There are no leases so the callout should return. ASSERT_NO_THROW(ha_impl.leases4Committed(*callout_handle)); // No updates are generated so the default status should not be modified. EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, callout_handle->getStatus()); EXPECT_TRUE(callout_handle->getParkingLotHandlePtr()->drop(query4)); // Create a lease and pass it to the callout, but temporarily disable lease // updates. HWAddrPtr hwaddr(new HWAddr(std::vector(6, 1), HTYPE_ETHER)); Lease4Ptr lease4(new Lease4(IOAddress("192.1.2.3"), hwaddr, static_cast(0), 0, 60, 0, 1)); leases4->push_back(lease4); callout_handle->setArgument("leases4", leases4); ha_impl.config_->setSendLeaseUpdates(false); // Park the packet. HooksManager::park("leases4_committed", query4, []{}); // Run the callout again. ASSERT_NO_THROW(ha_impl.leases4Committed(*callout_handle)); // No updates are generated so the default status should not be modified. EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, callout_handle->getStatus()); EXPECT_TRUE(callout_handle->getParkingLotHandlePtr()->drop(query4)); // Enable updates and retry. ha_impl.config_->setSendLeaseUpdates(true); callout_handle->setArgument("leases4", leases4); // Park the packet. HooksManager::park("leases4_committed", query4, []{}); // Run the callout again. ASSERT_NO_THROW(ha_impl.leases4Committed(*callout_handle)); // This time the lease update should be generated and the status should // be set to "park". EXPECT_EQ(CalloutHandle::NEXT_STEP_PARK, callout_handle->getStatus()); EXPECT_TRUE(callout_handle->getParkingLotHandlePtr()->drop(query4)); } // Tests leases6_committed callout implementation. TEST_F(HAImplTest, leases6Committed) { // Create implementation object and configure it. TestHAImpl ha_impl; ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration())); // Starting the service is required prior to running any callouts. NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv6)); ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state, HAServerType::DHCPv6)); // Make sure we wait for the acks from the backup server to be able to // test the case of sending lease updates even though the service is // in the state in which the lease updates are normally not sent. ha_impl.config_->setWaitBackupAck(true); // Create callout handle to be used for passing arguments to the // callout. CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle(); ASSERT_TRUE(callout_handle); // Set the hook index so we can park packets. callout_handle->setCurrentHook(test_hooks.hook_index_leases6_committed_); // query6 Pkt6Ptr query6 = createMessage6(DHCPV6_REQUEST, 1, 0); callout_handle->setArgument("query6", query6); // leases6 Lease6CollectionPtr leases6(new Lease6Collection()); callout_handle->setArgument("leases6", leases6); // deleted_leases6 Lease6CollectionPtr deleted_leases6(new Lease6Collection()); callout_handle->setArgument("deleted_leases6", deleted_leases6); // Set initial status. callout_handle->setStatus(CalloutHandle::NEXT_STEP_CONTINUE); // Park the packet. HooksManager::park("leases6_committed", query6, []{}); // There are no leases so the callout should return. ASSERT_NO_THROW(ha_impl.leases6Committed(*callout_handle)); // No updates are generated so the default status should not be modified. EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, callout_handle->getStatus()); EXPECT_TRUE(callout_handle->getParkingLotHandlePtr()->drop(query6)); // Create a lease and pass it to the callout, but temporarily disable lease // updates. DuidPtr duid(new DUID(std::vector(8, 2))); Lease6Ptr lease6(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::cafe"), duid, 1234, 50, 60, 1)); leases6->push_back(lease6); callout_handle->setArgument("leases6", leases6); ha_impl.config_->setSendLeaseUpdates(false); // Park the packet. HooksManager::park("leases6_committed", query6, []{}); // Run the callout again. ASSERT_NO_THROW(ha_impl.leases6Committed(*callout_handle)); // No updates are generated so the default status should not be modified. EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, callout_handle->getStatus()); EXPECT_TRUE(callout_handle->getParkingLotHandlePtr()->drop(query6)); // Enable updates and retry. ha_impl.config_->setSendLeaseUpdates(true); callout_handle->setArgument("leases6", leases6); // Park the packet. HooksManager::park("leases6_committed", query6, []{}); // Run the callout again. ASSERT_NO_THROW(ha_impl.leases6Committed(*callout_handle)); // This time the lease update should be generated and the status should // be set to "park". EXPECT_EQ(CalloutHandle::NEXT_STEP_PARK, callout_handle->getStatus()); EXPECT_TRUE(callout_handle->getParkingLotHandlePtr()->drop(query6)); } // Tests ha-sync command handler with correct and incorrect arguments. TEST_F(HAImplTest, synchronizeHandler) { { // This syntax is correct. The error returned is simply a result of // trying to connect to the server which is offline, which should // result in connection refused error. SCOPED_TRACE("Correct syntax"); testSynchronizeHandler("{" " \"command\": \"ha-sync\"," " \"arguments\": {" " \"server-name\": \"server2\"" " }" "}", "Connection refused"); } { SCOPED_TRACE("No arguments"); testSynchronizeHandler("{" " \"command\": \"ha-sync\"" "}", "arguments not found in the 'ha-sync' command"); } { SCOPED_TRACE("No server name"); testSynchronizeHandler("{" " \"command\": \"ha-sync\"," " \"arguments\": {" " \"max-period\": 20" " }" "}", "'server-name' is mandatory for the 'ha-sync' command"); } { SCOPED_TRACE("Server name is not a string"); testSynchronizeHandler("{" " \"command\": \"ha-sync\"," " \"arguments\": {" " \"server-name\": 20" " }" "}", "'server-name' must be a string in the 'ha-sync' command"); } { SCOPED_TRACE("Max period is not a number"); testSynchronizeHandler("{" " \"command\": \"ha-sync\"," " \"arguments\": {" " \"server-name\": \"server2\"," " \"max-period\": \"20\"" " }" "}", "'max-period' must be a positive integer in the 'ha-sync'" " command"); } { SCOPED_TRACE("Max period must be positive"); testSynchronizeHandler("{" " \"command\": \"ha-sync\"," " \"arguments\": {" " \"server-name\": \"server2\"," " \"max-period\": \"20\"" " }" "}", "'max-period' must be a positive integer in the 'ha-sync'" " command"); } } // Tests ha-continue command handler. TEST_F(HAImplTest, continueHandler) { HAImpl ha_impl; ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration())); // Starting the service is required prior to running any callouts. NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4)); ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state, HAServerType::DHCPv4)); ConstElementPtr command = Element::fromJSON("{ \"command\": \"ha-continue\" }"); CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle(); callout_handle->setArgument("command", command); ASSERT_NO_THROW(ha_impl.continueHandler(*callout_handle)); ConstElementPtr response; callout_handle->getArgument("response", response); ASSERT_TRUE(response); checkAnswer(response, CONTROL_RESULT_SUCCESS, "HA state machine is not paused."); } // Tests status-get command processed handler. TEST_F(HAImplTest, statusGet) { HAImpl ha_impl; ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration())); // Starting the service is required prior to running any callouts. NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4)); ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state, HAServerType::DHCPv4)); std::string name = "status-get"; ConstElementPtr response = Element::fromJSON("{ \"arguments\": { \"pid\": 1 }, \"result\": 0 }"); CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle(); callout_handle->setArgument("name", name); callout_handle->setArgument("response", response); ASSERT_NO_THROW(ha_impl.commandProcessed(*callout_handle)); ConstElementPtr got; callout_handle->getArgument("response", got); ASSERT_TRUE(got); std::string expected = "{" " \"arguments\": {" " \"high-availability\": [" " {" " \"ha-mode\": \"load-balancing\"," " \"ha-servers\": {" " \"local\": {" " \"role\": \"primary\"," " \"scopes\": [ ]," " \"state\": \"waiting\"" " }," " \"remote\": {" " \"age\": 0," " \"in-touch\": false," " \"last-scopes\": [ ]," " \"last-state\": \"\"," " \"role\": \"secondary\"," " \"communication-interrupted\": false," " \"connecting-clients\": 0," " \"unacked-clients\": 0," " \"unacked-clients-left\": 0," " \"analyzed-packets\": 0" " }" " }" " }" " ]," " \"pid\": 1" " }," " \"result\": 0" "}"; EXPECT_TRUE(isEquivalent(got, Element::fromJSON(expected))); } // Tests status-get command processed handler for backup server. TEST_F(HAImplTest, statusGetBackupServer) { TestHAImpl ha_impl; ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration())); ha_impl.config_->setThisServerName("server3"); // Starting the service is required prior to running any callouts. NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4)); ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state, HAServerType::DHCPv4)); std::string name = "status-get"; ConstElementPtr response = Element::fromJSON("{ \"arguments\": { \"pid\": 1 }, \"result\": 0 }"); CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle(); callout_handle->setArgument("name", name); callout_handle->setArgument("response", response); ASSERT_NO_THROW(ha_impl.commandProcessed(*callout_handle)); ConstElementPtr got; callout_handle->getArgument("response", got); ASSERT_TRUE(got); std::string expected = "{" " \"arguments\": {" " \"high-availability\": [" " {" " \"ha-mode\": \"load-balancing\"," " \"ha-servers\": {" " \"local\": {" " \"role\": \"backup\"," " \"scopes\": [ ]," " \"state\": \"backup\"" " }" " }" " }" " ]," " \"pid\": 1" " }," " \"result\": 0" "}"; EXPECT_TRUE(isEquivalent(got, Element::fromJSON(expected))); } // Tests status-get command processed handler for primary server being in the // passive-backup state. TEST_F(HAImplTest, statusGetPassiveBackup) { TestHAImpl ha_impl; ASSERT_NO_THROW(ha_impl.configure(createValidPassiveBackupJsonConfiguration())); // Starting the service is required prior to running any callouts. NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4)); ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state, HAServerType::DHCPv4)); std::string name = "status-get"; ConstElementPtr response = Element::fromJSON("{ \"arguments\": { \"pid\": 1 }, \"result\": 0 }"); CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle(); callout_handle->setArgument("name", name); callout_handle->setArgument("response", response); ASSERT_NO_THROW(ha_impl.commandProcessed(*callout_handle)); ConstElementPtr got; callout_handle->getArgument("response", got); ASSERT_TRUE(got); std::string expected = "{" " \"arguments\": {" " \"high-availability\": [" " {" " \"ha-mode\": \"passive-backup\"," " \"ha-servers\": {" " \"local\": {" " \"role\": \"primary\"," " \"scopes\": [ \"server1\" ]," " \"state\": \"passive-backup\"" " }" " }" " }" " ]," " \"pid\": 1" " }," " \"result\": 0" "}"; EXPECT_TRUE(isEquivalent(got, Element::fromJSON(expected))); } // Test ha-maintenance-notify command handler. TEST_F(HAImplTest, maintenanceNotify) { HAImpl ha_impl; ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration())); // Starting the service is required prior to running any callouts. NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4)); ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state, HAServerType::DHCPv4)); ConstElementPtr command = Element::fromJSON( "{" " \"command\": \"ha-maintenance-notify\"," " \"arguments\": {" " \"cancel\": false" " }" "}" ); CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle(); callout_handle->setArgument("command", command); ASSERT_NO_THROW(ha_impl.maintenanceNotifyHandler(*callout_handle)); ConstElementPtr response; callout_handle->getArgument("response", response); ASSERT_TRUE(response); checkAnswer(response, CONTROL_RESULT_SUCCESS, "Server is in-maintenance state."); } // Test ha-reset command handler. TEST_F(HAImplTest, haReset) { HAImpl ha_impl; ASSERT_NO_THROW(ha_impl.configure(createValidJsonConfiguration())); // Starting the service is required prior to running any callouts. NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4)); ASSERT_NO_THROW(ha_impl.startService(io_service_, network_state, HAServerType::DHCPv4)); ConstElementPtr command = Element::fromJSON( "{" " \"command\": \"ha-reset\"" "}" ); CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle(); callout_handle->setArgument("command", command); ASSERT_NO_THROW(ha_impl.haResetHandler(*callout_handle)); ConstElementPtr response; callout_handle->getArgument("response", response); ASSERT_TRUE(response); checkAnswer(response, CONTROL_RESULT_SUCCESS, "HA state machine already in WAITING state."); } }