Skip to content

Commit 699ef91

Browse files
BGP-aware fboss2 config session (ConfigSession infra)
Make the fboss2 ConfigSession and `config session` lifecycle BGP-aware. This is the base for the `config protocol bgp` commands, which stack on top. - ConfigSession owns the BGP config as a typed bgp::thrift::BgpConfig, exactly the way it owns cfg::AgentConfig for the agent. getBgpConfig()/saveBgpConfig() expose the WHOLE typed config and stage it to ~/.fboss2/bgp_config.json. ConfigSession is BGP-aware but agnostic about the config's internal structure: it has no notion of "global" vs "peer" vs "peer-group" -- a command mutates whichever typed fields it needs, mirroring how interface commands mutate getAgentConfig().sw()->ports(). Because the whole config round-trips through the typed struct, no surgical merge / preserve-list is needed. - commit() promotes bgp_config.json to /etc/coop/bgpcpp/bgpcpp.conf, restarts bgp_pp, and versions it in the same /etc/coop git repo as agent.conf. - config session clear/diff/rebase/rollback are BGP-aware: clear removes a staged BGP session, diff shows staged BGP changes, rebase 3-way-merges bgpcpp.conf, rollback restores bgpcpp.conf + restarts bgp_pp (using metadata history so BGP-only commits are reachable). - FbossServiceUtil + cli_metadata gain the bgp_pp (BGP_PP) service/action; the config lib gains the bgp_config cpp2 dependency. - Unit tests in CmdConfigSession{,Diff}Test cover the typed round-trip, commit, rebase, and rollback behavior. Test Plan: - bazel test //fboss/cli/fboss2/test/config:cmd_config_test //fboss/cli/fboss2/test:cmd_test - fboss2_integration_test on a DUT (ConfigConcurrentSessionsTest.ConflictAndRebase, ConfigSessionClearTest, ConfigInterfaceMtuTest, ConfigInterfaceDescriptionTest): 5/5 pass Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a867f6f commit 699ef91

12 files changed

Lines changed: 1110 additions & 174 deletions

‎cmake/CliFboss2.cmake‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,7 @@ target_link_libraries(fboss2_config_lib
10501050
agent_dir_util
10511051
common_file_utils
10521052
switch_config_cpp2
1053+
bgp_config_cpp2
10531054
switchinfo_utils
10541055
platform_mapping
10551056
Folly::folly

‎fboss/cli/fboss2/BUCK‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,6 +1292,7 @@ cpp_library(
12921292
":fboss2-lib",
12931293
":table-utils",
12941294
"//common/network/if:if-cpp2-types",
1295+
"//configerator/structs/neteng/fboss/bgp:bgp_config-cpp2-types",
12951296
"//fboss/agent:agent_config-cpp2-types",
12961297
"//fboss/agent:agent_dir_util",
12971298
"//fboss/agent:fboss-error",

‎fboss/cli/fboss2/cli_metadata.thrift‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ enum ConfigActionLevel {
1919
HITLESS = 0, // Can be applied with reloadConfig() - default
2020
AGENT_WARMBOOT = 1, // Requires agent warmboot restart
2121
AGENT_COLDBOOT = 2, // Requires agent coldboot restart (clears ASIC state)
22+
BGP_PP_RESTART = 3, // Requires a restart of the bgp_pp (BGP++) service
2223
}
2324

2425
// Identifier for different services that can be configured
2526
enum ServiceType {
2627
AGENT = 1,
28+
BGP_PP = 2, // The bgp_pp (BGP++) routing daemon
2729
}
2830

2931
// Metadata stored alongside the session configuration file.

‎fboss/cli/fboss2/commands/config/session/CmdConfigSessionClear.cpp‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ CmdConfigSessionClearTraits::RetType CmdConfigSessionClear::queryClient(
2828
// getInstance(), which would create a session if one doesn't exist
2929
std::string sessionConfigPath = ConfigSession::getSessionConfigPathStatic();
3030
std::string metadataPath = ConfigSession::getSessionMetadataPathStatic();
31+
std::string bgpConfigPath = ConfigSession::getBgpSessionConfigPathStatic();
3132

3233
std::error_code ec;
3334
bool removedConfig = false;
3435
bool removedMetadata = false;
36+
bool removedBgpConfig = false;
3537

3638
// Remove session config file (~/.fboss2/agent.conf)
3739
if (fs::exists(sessionConfigPath)) {
@@ -60,7 +62,23 @@ CmdConfigSessionClearTraits::RetType CmdConfigSessionClear::queryClient(
6062
removedMetadata = true;
6163
}
6264

63-
if (removedConfig || removedMetadata) {
65+
// Remove staged BGP config file (~/.fboss2/bgp_config.json). BGP global edits
66+
// are staged here (alongside any peer edits from BgpConfigSession), so a
67+
// BGP-only session must be cleared too.
68+
if (fs::exists(bgpConfigPath)) {
69+
ec.clear();
70+
fs::remove(bgpConfigPath, ec);
71+
if (ec) {
72+
throw std::runtime_error(
73+
fmt::format(
74+
"Failed to remove BGP session config file {}: {}",
75+
bgpConfigPath,
76+
ec.message()));
77+
}
78+
removedBgpConfig = true;
79+
}
80+
81+
if (removedConfig || removedMetadata || removedBgpConfig) {
6482
return "Config session cleared successfully.";
6583
}
6684
return "No config session exists. Nothing to clear.";

‎fboss/cli/fboss2/commands/config/session/CmdConfigSessionCommit.cpp‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ CmdConfigSessionCommitTraits::RetType CmdConfigSessionCommit::queryClient(
2323
const HostInfo& hostInfo) {
2424
auto& session = ConfigSession::getInstance();
2525

26-
if (!session.sessionExists()) {
26+
if (!session.hasActiveSession()) {
2727
return "No config session exists. Make a config change first.";
2828
}
2929

@@ -59,6 +59,11 @@ CmdConfigSessionCommitTraits::RetType CmdConfigSessionCommit::queryClient(
5959
fmt::format("{} (warmboot)", serviceName));
6060
}
6161
break;
62+
case cli::ConfigActionLevel::BGP_PP_RESTART:
63+
for (const auto& serviceName : serviceNamesList) {
64+
restartedServices.push_back(fmt::format("{} (restart)", serviceName));
65+
}
66+
break;
6267
case cli::ConfigActionLevel::HITLESS:
6368
for (const auto& serviceName : serviceNamesList) {
6469
reloadedServices.push_back(serviceName);

‎fboss/cli/fboss2/commands/config/session/CmdConfigSessionDiff.cpp‎

Lines changed: 144 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
#include "fboss/cli/fboss2/utils/CmdUtils.h"
1818
#include "fboss/cli/fboss2/utils/HostInfo.h"
1919

20+
#include <fmt/format.h>
2021
#include <folly/FileUtil.h>
2122
#include <folly/Subprocess.h>
2223
#include <unistd.h>
2324
#include <cstdlib>
2425
#include <exception>
26+
#include <functional>
2527
#include <iostream>
2628
#include <stdexcept>
2729
#include <string>
@@ -32,27 +34,68 @@ namespace facebook::fboss {
3234

3335
namespace {
3436

35-
// Helper function to get config content from a revision specifier
36-
// Returns the content and a label for the revision
37+
// Git-relative paths of the two config files tracked in the /etc/coop repo.
38+
constexpr auto kAgentGitRelPath = "cli/agent.conf";
39+
constexpr auto kBgpGitRelPath = "bgpcpp/bgpcpp.conf";
40+
41+
// A diffable config domain. The agent config and the BGP config are tracked in
42+
// the same /etc/coop git repo but live in different files; `config session
43+
// diff` shows whichever domain(s) are staged/relevant.
44+
struct DiffDomain {
45+
std::string name; // "Agent" / "BGP" (section header when >1 domain shown)
46+
std::string gitRelPath; // path in the git repo (e.g. cli/agent.conf)
47+
std::string systemPath; // current live file
48+
std::string sessionPath; // staged session file (~/.fboss2/...)
49+
bool staged; // a session edit is staged for this domain
50+
};
51+
52+
std::vector<DiffDomain> allDomains(ConfigSession& session) {
53+
return {
54+
DiffDomain{
55+
"Agent",
56+
kAgentGitRelPath,
57+
session.getSystemConfigPath(),
58+
session.getSessionConfigPath(),
59+
session.sessionExists()},
60+
DiffDomain{
61+
"BGP",
62+
kBgpGitRelPath,
63+
session.getBgpSystemConfigPath(),
64+
session.getBgpSessionConfigPath(),
65+
session.bgpSessionExists()},
66+
};
67+
}
68+
69+
// Read a file, returning empty content (not an error) when it doesn't exist.
70+
std::string readFileOrEmpty(const std::string& path) {
71+
std::string content;
72+
folly::readFile(path.c_str(), content);
73+
return content;
74+
}
75+
76+
// Get config content from a revision specifier for a specific domain file.
77+
// "current" reads the live system file. A path absent at the given revision
78+
// (e.g. a commit predating BGP config) is treated as empty content.
3779
std::pair<std::string, std::string> getRevisionContent(
3880
const std::string& revision,
39-
ConfigSession& session) {
40-
auto& git = session.getGit();
41-
std::string cliConfigPath = session.getCliConfigPath();
42-
81+
const DiffDomain& domain,
82+
Git& git) {
4383
if (revision == "current") {
44-
// Read the current live config (via the symlink or directly from cli path)
45-
std::string content;
46-
if (!folly::readFile(cliConfigPath.c_str(), content)) {
47-
throw std::runtime_error(
48-
"Failed to read current config from " + cliConfigPath);
49-
}
50-
return {content, "current live config"};
84+
return {readFileOrEmpty(domain.systemPath), "current live config"};
5185
}
52-
53-
// Resolve the commit SHA and get the content from Git
5486
std::string resolvedSha = git.resolveRef(revision);
55-
std::string content = git.fileAtRevision(resolvedSha, "cli/agent.conf");
87+
// Verify the revision is real before treating a missing domain path as empty.
88+
// cli/agent.conf is present in every commit (including the initial one), so a
89+
// genuinely invalid revision throws here and propagates; only a path absent
90+
// at an otherwise-valid revision (e.g. bgpcpp.conf before BGP existed) is
91+
// treated as empty.
92+
git.fileAtRevision(resolvedSha, kAgentGitRelPath);
93+
std::string content;
94+
try {
95+
content = git.fileAtRevision(resolvedSha, domain.gitRelPath);
96+
} catch (const std::exception&) {
97+
content = "";
98+
}
5699
return {content, Git::shortSha1(revision)};
57100
}
58101

@@ -116,64 +159,108 @@ std::string executeDiff(
116159
}
117160
}
118161

162+
// Append a (optionally headered) diff section to the combined output.
163+
void appendSection(
164+
std::string& out,
165+
const std::string& name,
166+
const std::string& body,
167+
bool withHeader) {
168+
if (withHeader) {
169+
if (!out.empty()) {
170+
out += "\n";
171+
}
172+
out += fmt::format("===== {} config =====\n", name);
173+
}
174+
out += body;
175+
if (!body.empty() && body.back() != '\n') {
176+
out += "\n";
177+
}
178+
}
179+
119180
} // namespace
120181

121182
CmdConfigSessionDiffTraits::RetType CmdConfigSessionDiff::queryClient(
122183
const HostInfo& /* hostInfo */,
123184
const utils::RevisionList& revisions) {
124185
auto& session = ConfigSession::getInstance();
125-
126-
std::string systemConfigPath = session.getSystemConfigPath();
127-
std::string sessionConfigPath = session.getSessionConfigPath();
128-
129-
// Mode 1: No arguments - diff session vs current live config
186+
auto& git = session.getGit();
187+
auto domains = allDomains(session);
188+
189+
// Modes 1 and 2 both diff each staged domain's session file against some
190+
// "base" (current live config for mode 1; a revision for mode 2). The only
191+
// difference is how the base content+label is obtained, so share the loop.
192+
auto diffStagedDomains =
193+
[&](const std::function<std::pair<std::string, std::string>(
194+
const DiffDomain&)>& getBase) {
195+
int stagedCount = 0;
196+
for (const auto& d : domains) {
197+
stagedCount += d.staged ? 1 : 0;
198+
}
199+
std::string out;
200+
for (const auto& d : domains) {
201+
if (!d.staged) {
202+
continue;
203+
}
204+
auto [baseContent, baseLabel] = getBase(d);
205+
std::string sessionContent;
206+
if (!folly::readFile(d.sessionPath.c_str(), sessionContent)) {
207+
throw std::runtime_error(
208+
"Failed to read session config from " + d.sessionPath);
209+
}
210+
appendSection(
211+
out,
212+
d.name,
213+
executeDiff(
214+
baseContent, sessionContent, baseLabel, "session config"),
215+
stagedCount > 1);
216+
}
217+
return out;
218+
};
219+
220+
// Mode 1: No arguments - diff each staged domain's session vs current live.
130221
if (revisions.empty()) {
131-
if (!session.sessionExists()) {
222+
if (!session.hasActiveSession()) {
132223
return "No config session exists. Make a config change first.";
133224
}
134-
135-
std::string currentContent;
136-
if (!folly::readFile(systemConfigPath.c_str(), currentContent)) {
137-
throw std::runtime_error(
138-
"Failed to read current config from " + systemConfigPath);
139-
}
140-
141-
std::string sessionContent;
142-
if (!folly::readFile(sessionConfigPath.c_str(), sessionContent)) {
143-
throw std::runtime_error(
144-
"Failed to read session config from " + sessionConfigPath);
145-
}
146-
147-
return executeDiff(
148-
currentContent,
149-
sessionContent,
150-
"current live config",
151-
"session config");
225+
return diffStagedDomains([&](const DiffDomain& d) {
226+
return std::make_pair(
227+
readFileOrEmpty(d.systemPath), std::string("current live config"));
228+
});
152229
}
153230

154-
// Mode 2: One argument - diff session vs specified revision
231+
// Mode 2: One argument - diff each staged domain's session vs a revision.
155232
if (revisions.size() == 1) {
156-
if (!session.sessionExists()) {
233+
if (!session.hasActiveSession()) {
157234
return "No config session exists. Make a config change first.";
158235
}
159-
160-
auto [revContent, revLabel] = getRevisionContent(revisions[0], session);
161-
162-
std::string sessionContent;
163-
if (!folly::readFile(sessionConfigPath.c_str(), sessionContent)) {
164-
throw std::runtime_error(
165-
"Failed to read session config from " + sessionConfigPath);
166-
}
167-
168-
return executeDiff(revContent, sessionContent, revLabel, "session config");
236+
return diffStagedDomains([&](const DiffDomain& d) {
237+
return getRevisionContent(revisions[0], d, git);
238+
});
169239
}
170240

171-
// Mode 3: Two arguments - diff between two revisions
241+
// Mode 3: Two arguments - diff between two revisions for each domain that
242+
// has content at either revision.
172243
if (revisions.size() == 2) {
173-
auto [content1, label1] = getRevisionContent(revisions[0], session);
174-
auto [content2, label2] = getRevisionContent(revisions[1], session);
175-
176-
return executeDiff(content1, content2, label1, label2);
244+
// Pre-compute each domain's rendered diff section so headers are only added
245+
// when more than one domain is shown.
246+
std::vector<std::pair<std::string, std::string>> sections; // {name, body}
247+
for (const auto& d : domains) {
248+
auto [c1, l1] = getRevisionContent(revisions[0], d, git);
249+
auto [c2, l2] = getRevisionContent(revisions[1], d, git);
250+
if (c1.empty() && c2.empty()) {
251+
continue; // domain absent at both revisions
252+
}
253+
sections.emplace_back(d.name, executeDiff(c1, c2, l1, l2));
254+
}
255+
if (sections.empty()) {
256+
return "No config found at the given revisions.";
257+
}
258+
std::string out;
259+
const bool multi = sections.size() > 1;
260+
for (const auto& [name, body] : sections) {
261+
appendSection(out, name, body, multi);
262+
}
263+
return out;
177264
}
178265

179266
// More than 2 arguments is an error

0 commit comments

Comments
 (0)