diff --git a/cmake/CliFboss2.cmake b/cmake/CliFboss2.cmake index 6a5872c93f967..caea3ddb0ce67 100644 --- a/cmake/CliFboss2.cmake +++ b/cmake/CliFboss2.cmake @@ -575,6 +575,8 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/config/CmdConfigAppliedInfo.cpp fboss/cli/fboss2/commands/config/CmdConfigReload.h fboss/cli/fboss2/commands/config/CmdConfigReload.cpp + fboss/cli/fboss2/commands/config/rollback/CmdConfigRollback.h + fboss/cli/fboss2/commands/config/rollback/CmdConfigRollback.cpp fboss/cli/fboss2/commands/config/session/CmdConfigSessionCommit.h fboss/cli/fboss2/commands/config/session/CmdConfigSessionCommit.cpp fboss/cli/fboss2/commands/config/session/CmdConfigSessionDiff.h diff --git a/fboss/cli/fboss2/BUCK b/fboss/cli/fboss2/BUCK index 4c08a6a7311e2..fddd9ae247522 100644 --- a/fboss/cli/fboss2/BUCK +++ b/fboss/cli/fboss2/BUCK @@ -771,6 +771,7 @@ cpp_library( "CmdListConfig.cpp", "commands/config/CmdConfigAppliedInfo.cpp", "commands/config/CmdConfigReload.cpp", + "commands/config/rollback/CmdConfigRollback.cpp", "commands/config/session/CmdConfigSessionCommit.cpp", "commands/config/session/CmdConfigSessionDiff.cpp", "session/ConfigSession.cpp", @@ -778,6 +779,7 @@ cpp_library( headers = [ "commands/config/CmdConfigAppliedInfo.h", "commands/config/CmdConfigReload.h", + "commands/config/rollback/CmdConfigRollback.h", "commands/config/session/CmdConfigSessionCommit.h", "commands/config/session/CmdConfigSessionDiff.h", "session/ConfigSession.h", diff --git a/fboss/cli/fboss2/CmdHandlerImplConfig.cpp b/fboss/cli/fboss2/CmdHandlerImplConfig.cpp index 6c93601afdc7a..6c394fcaafb35 100644 --- a/fboss/cli/fboss2/CmdHandlerImplConfig.cpp +++ b/fboss/cli/fboss2/CmdHandlerImplConfig.cpp @@ -12,6 +12,7 @@ #include "fboss/cli/fboss2/commands/config/CmdConfigAppliedInfo.h" #include "fboss/cli/fboss2/commands/config/CmdConfigReload.h" +#include "fboss/cli/fboss2/commands/config/rollback/CmdConfigRollback.h" #include "fboss/cli/fboss2/commands/config/session/CmdConfigSessionCommit.h" #include "fboss/cli/fboss2/commands/config/session/CmdConfigSessionDiff.h" @@ -20,6 +21,7 @@ namespace facebook::fboss { template void CmdHandler::run(); template void CmdHandler::run(); +template void CmdHandler::run(); template void CmdHandler::run(); template void diff --git a/fboss/cli/fboss2/CmdListConfig.cpp b/fboss/cli/fboss2/CmdListConfig.cpp index 231cef7c5990c..acdbc61d78c6e 100644 --- a/fboss/cli/fboss2/CmdListConfig.cpp +++ b/fboss/cli/fboss2/CmdListConfig.cpp @@ -13,6 +13,7 @@ #include "fboss/cli/fboss2/CmdHandler.h" #include "fboss/cli/fboss2/commands/config/CmdConfigAppliedInfo.h" #include "fboss/cli/fboss2/commands/config/CmdConfigReload.h" +#include "fboss/cli/fboss2/commands/config/rollback/CmdConfigRollback.h" #include "fboss/cli/fboss2/commands/config/session/CmdConfigSessionCommit.h" #include "fboss/cli/fboss2/commands/config/session/CmdConfigSessionDiff.h" @@ -49,6 +50,12 @@ const CommandTree& kConfigCommandTree() { "Reload agent configuration", commandHandler, argTypeHandler}, + + {"config", + "rollback", + "Rollback to a previous config revision", + commandHandler, + argTypeHandler}, }; sort(root.begin(), root.end()); return root; diff --git a/fboss/cli/fboss2/commands/config/rollback/CmdConfigRollback.cpp b/fboss/cli/fboss2/commands/config/rollback/CmdConfigRollback.cpp new file mode 100644 index 0000000000000..2b3b04e07a0ff --- /dev/null +++ b/fboss/cli/fboss2/commands/config/rollback/CmdConfigRollback.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#include "fboss/cli/fboss2/commands/config/rollback/CmdConfigRollback.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" + +namespace facebook::fboss { + +CmdConfigRollbackTraits::RetType CmdConfigRollback::queryClient( + const HostInfo& hostInfo, + const utils::RevisionList& revisions) { + auto& session = ConfigSession::getInstance(); + + // Validate arguments + if (revisions.size() > 1) { + throw std::invalid_argument( + "Too many arguments. Expected 0 or 1 revision specifier."); + } + + if (!revisions.empty() && revisions[0] == "current") { + throw std::invalid_argument( + "Cannot rollback to 'current'. Please specify a revision number like 'r42'."); + } + + try { + int newRevision; + if (revisions.empty()) { + // No revision specified - rollback to previous revision + newRevision = session.rollback(hostInfo); + } else { + // Specific revision specified + newRevision = session.rollback(hostInfo, revisions[0]); + } + if (newRevision) { + return "Successfully rolled back to r" + std::to_string(newRevision) + + " and config reloaded."; + } else { + return "Failed to create a new revision after rollback."; + } + } catch (const std::exception& ex) { + throw std::runtime_error( + "Failed to rollback config: " + std::string(ex.what())); + } +} + +void CmdConfigRollback::printOutput(const RetType& logMsg) { + std::cout << logMsg << std::endl; +} + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/rollback/CmdConfigRollback.h b/fboss/cli/fboss2/commands/config/rollback/CmdConfigRollback.h new file mode 100644 index 0000000000000..e03baab404dff --- /dev/null +++ b/fboss/cli/fboss2/commands/config/rollback/CmdConfigRollback.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#pragma once + +#include "fboss/cli/fboss2/CmdHandler.h" +#include "fboss/cli/fboss2/utils/CmdClientUtils.h" +#include "fboss/cli/fboss2/utils/CmdUtils.h" + +namespace facebook::fboss { + +struct CmdConfigRollbackTraits : public WriteCommandTraits { + static constexpr utils::ObjectArgTypeId ObjectArgTypeId = + utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_REVISION_LIST; + using ObjectArgType = utils::RevisionList; + using RetType = std::string; +}; + +class CmdConfigRollback + : public CmdHandler { + public: + using ObjectArgType = CmdConfigRollbackTraits::ObjectArgType; + using RetType = CmdConfigRollbackTraits::RetType; + + RetType queryClient( + const HostInfo& hostInfo, + const utils::RevisionList& revisions); + + void printOutput(const RetType& logMsg); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/session/ConfigSession.cpp b/fboss/cli/fboss2/session/ConfigSession.cpp index e9232a33b216f..a9a4d959fb124 100644 --- a/fboss/cli/fboss2/session/ConfigSession.cpp +++ b/fboss/cli/fboss2/session/ConfigSession.cpp @@ -167,6 +167,25 @@ void ensureDirectoryExists(const std::string& dirPath) { } } +/* + * Get the current revision number by reading the symlink target. + * Returns -1 if unable to determine the current revision. + */ +int getCurrentRevisionNumber(const std::string& systemConfigPath) { + std::error_code ec; + + if (!fs::is_symlink(systemConfigPath, ec)) { + return -1; + } + + std::string target = fs::read_symlink(systemConfigPath, ec); + if (ec) { + return -1; + } + + return ConfigSession::extractRevisionNumber(target); +} + } // anonymous namespace ConfigSession::ConfigSession() { @@ -456,4 +475,100 @@ int ConfigSession::commit(const HostInfo& hostInfo) { return revision; } +int ConfigSession::rollback(const HostInfo& hostInfo) { + // Get the current revision number + int currentRevision = getCurrentRevisionNumber(systemConfigPath_); + if (currentRevision <= 0) { + throw std::runtime_error( + "Cannot rollback: cannot determine the current revision from " + + systemConfigPath_); + } else if (currentRevision == 1) { + throw std::runtime_error( + "Cannot rollback: already at the first revision (r1)"); + } + + // Rollback to the previous revision + std::string targetRevision = "r" + std::to_string(currentRevision - 1); + return rollback(hostInfo, targetRevision); +} + +int ConfigSession::rollback( + const HostInfo& hostInfo, + const std::string& revision) { + ensureDirectoryExists(cliConfigDir_); + + // Build the path to the target revision + std::string targetConfigPath = cliConfigDir_ + "/agent-" + revision + ".conf"; + + // Check if the target revision exists + if (!fs::exists(targetConfigPath)) { + throw std::runtime_error( + "Revision " + revision + " does not exist at " + targetConfigPath); + } + + std::error_code ec; + + // Verify that the system config is a symlink + if (!fs::is_symlink(systemConfigPath_)) { + throw std::runtime_error( + systemConfigPath_ + " is not a symlink. Expected it to be a symlink."); + } + + // Read the old symlink target in case we need to undo the rollback + std::string oldSymlinkTarget = fs::read_symlink(systemConfigPath_, ec); + if (ec) { + throw std::runtime_error( + "Failed to read symlink " + systemConfigPath_ + ": " + ec.message()); + } + + // First, create a new revision with the same content as the target revision + auto [newRevisionPath, newRevision] = + createNextRevisionFile(fmt::format("{}/agent", cliConfigDir_)); + + // Copy the target config to the new revision file + fs::copy_file( + targetConfigPath, + newRevisionPath, + fs::copy_options::overwrite_existing, + ec); + if (ec) { + // Clean up the revision file we created + fs::remove(newRevisionPath); + throw std::runtime_error( + fmt::format( + "Failed to create new revision for rollback: {}", ec.message())); + } + + // Atomically update the symlink to point to the new revision + atomicSymlinkUpdate(systemConfigPath_, newRevisionPath); + + // Reload the config - if this fails, atomically undo the rollback + try { + auto client = + utils::createClient>( + hostInfo); + client->sync_reloadConfig(); + } catch (const std::exception& ex) { + // Rollback: atomically restore the old symlink + try { + atomicSymlinkUpdate(systemConfigPath_, oldSymlinkTarget); + } catch (const std::exception& rollbackEx) { + // If rollback also fails, include both errors in the message + throw std::runtime_error( + fmt::format( + "Failed to reload config: {}. Additionally, failed to rollback the symlink: {}", + ex.what(), + rollbackEx.what())); + } + throw std::runtime_error( + fmt::format( + "Failed to reload config, symlink was rolled back automatically: {}", + ex.what())); + } + + // Successfully rolled back + LOG(INFO) << "Rollback committed as revision r" << newRevision; + return newRevision; +} + } // namespace facebook::fboss diff --git a/fboss/cli/fboss2/session/ConfigSession.h b/fboss/cli/fboss2/session/ConfigSession.h index 5f962dad5c874..7838262eaae35 100644 --- a/fboss/cli/fboss2/session/ConfigSession.h +++ b/fboss/cli/fboss2/session/ConfigSession.h @@ -102,6 +102,11 @@ class ConfigSession { // successful. int commit(const HostInfo& hostInfo); + // Rollback to a specific revision or to the previous revision + // Returns the revision that was rolled back to + int rollback(const HostInfo& hostInfo); + int rollback(const HostInfo& hostInfo, const std::string& revision); + // Check if a session exists bool sessionExists() const; diff --git a/fboss/cli/fboss2/test/CmdConfigSessionTest.cpp b/fboss/cli/fboss2/test/CmdConfigSessionTest.cpp index 8b08a3ca1c950..1dfb390be1ef0 100644 --- a/fboss/cli/fboss2/test/CmdConfigSessionTest.cpp +++ b/fboss/cli/fboss2/test/CmdConfigSessionTest.cpp @@ -530,4 +530,111 @@ TEST_F(ConfigSessionTestFixture, revisionNumberExtraction) { 999); } +TEST_F(ConfigSessionTestFixture, rollbackCreatesNewRevision) { + // This test actually calls the rollback() method with a specific revision + fs::path cliConfigDir = testEtcDir_ / "coop" / "cli"; + fs::path symlinkPath = testEtcDir_ / "coop" / "agent.conf"; + fs::path sessionConfigPath = testHomeDir_ / ".fboss2" / "agent.conf"; + + // Remove the regular file created by SetUp + if (fs::exists(symlinkPath)) { + fs::remove(symlinkPath); + } + + // Create revision files (simulating previous commits) + createTestConfig(cliConfigDir / "agent-r1.conf", R"({"revision": 1})"); + createTestConfig(cliConfigDir / "agent-r2.conf", R"({"revision": 2})"); + createTestConfig(cliConfigDir / "agent-r3.conf", R"({"revision": 3})"); + + // Create symlink pointing to r3 (current revision) + fs::create_symlink(cliConfigDir / "agent-r3.conf", symlinkPath); + + // Verify initial state + EXPECT_TRUE(fs::is_symlink(symlinkPath)); + EXPECT_EQ(fs::read_symlink(symlinkPath), cliConfigDir / "agent-r3.conf"); + + // Setup mock agent server + setupMockedAgentServer(); + + // Expect reloadConfig to be called once + EXPECT_CALL(getMockAgent(), reloadConfig()).Times(1); + + // Create a testable ConfigSession with test paths + TestableConfigSession session( + sessionConfigPath.string(), symlinkPath.string(), cliConfigDir.string()); + + // Call the actual rollback method to rollback to r1 + int newRevision = session.rollback(localhost(), "r1"); + + // Verify rollback created a new revision (r4) + EXPECT_EQ(newRevision, 4); + EXPECT_TRUE(fs::is_symlink(symlinkPath)); + EXPECT_EQ(fs::read_symlink(symlinkPath), cliConfigDir / "agent-r4.conf"); + EXPECT_TRUE(fs::exists(cliConfigDir / "agent-r4.conf")); + + // Verify r4 has same content as r1 (the target revision) + EXPECT_EQ( + readFile(cliConfigDir / "agent-r1.conf"), + readFile(cliConfigDir / "agent-r4.conf")); + + // Verify old revisions still exist (rollback doesn't delete history) + EXPECT_TRUE(fs::exists(cliConfigDir / "agent-r1.conf")); + EXPECT_TRUE(fs::exists(cliConfigDir / "agent-r2.conf")); + EXPECT_TRUE(fs::exists(cliConfigDir / "agent-r3.conf")); +} + +TEST_F(ConfigSessionTestFixture, rollbackToPreviousRevision) { + // This test actually calls the rollback() method without a revision argument + // to rollback to the previous revision + fs::path cliConfigDir = testEtcDir_ / "coop" / "cli"; + fs::path symlinkPath = testEtcDir_ / "coop" / "agent.conf"; + fs::path sessionConfigPath = testHomeDir_ / ".fboss2" / "agent.conf"; + + // Remove the regular file created by SetUp + if (fs::exists(symlinkPath)) { + fs::remove(symlinkPath); + } + + // Create revision files (simulating previous commits) + createTestConfig(cliConfigDir / "agent-r1.conf", R"({"revision": 1})"); + createTestConfig(cliConfigDir / "agent-r2.conf", R"({"revision": 2})"); + createTestConfig(cliConfigDir / "agent-r3.conf", R"({"revision": 3})"); + + // Create symlink pointing to r3 (current revision) + fs::create_symlink(cliConfigDir / "agent-r3.conf", symlinkPath); + + // Verify initial state + EXPECT_TRUE(fs::is_symlink(symlinkPath)); + EXPECT_EQ(fs::read_symlink(symlinkPath), cliConfigDir / "agent-r3.conf"); + + // Setup mock agent server + setupMockedAgentServer(); + + // Expect reloadConfig to be called once + EXPECT_CALL(getMockAgent(), reloadConfig()).Times(1); + + // Create a testable ConfigSession with test paths + TestableConfigSession session( + sessionConfigPath.string(), symlinkPath.string(), cliConfigDir.string()); + + // Call the actual rollback method without a revision (should go to previous) + int newRevision = session.rollback(localhost()); + + // Verify rollback to previous revision created r4 with content from r2 + EXPECT_EQ(newRevision, 4); + EXPECT_TRUE(fs::is_symlink(symlinkPath)); + EXPECT_EQ(fs::read_symlink(symlinkPath), cliConfigDir / "agent-r4.conf"); + EXPECT_TRUE(fs::exists(cliConfigDir / "agent-r4.conf")); + + // Verify r4 has same content as r2 (the previous revision) + EXPECT_EQ( + readFile(cliConfigDir / "agent-r2.conf"), + readFile(cliConfigDir / "agent-r4.conf")); + + // Verify old revisions still exist (rollback doesn't delete history) + EXPECT_TRUE(fs::exists(cliConfigDir / "agent-r1.conf")); + EXPECT_TRUE(fs::exists(cliConfigDir / "agent-r2.conf")); + EXPECT_TRUE(fs::exists(cliConfigDir / "agent-r3.conf")); +} + } // namespace facebook::fboss