From 096b5b7cdc6ff16e1adadeb12e5a4e8b106c3d00 Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Mon, 29 Jun 2026 03:48:46 +0000 Subject: [PATCH 01/22] fboss2 `config protocol bgp global` command Add `fboss2-dev config protocol bgp global ` on top of the BGP-aware ConfigSession (base PR). Edits the typed bgp::thrift::BgpConfig via ConfigSession::getBgpConfig()/saveBgpConfig() -- the whole-config, scope-agnostic typed API (no global/peer/peer-group special-casing in ConfigSession). - Collapse the 10 per-attribute global command classes into one dispatcher; reject cluster-id (no BgpConfig field) instead of writing dead config; bound switch-limit / max_golden_vips so out-of-range values aren't truncated. - Integration tests: ConfigBgpGlobalTest (each attr set+commit, verified in the promoted /etc/coop/bgpcpp/bgpcpp.conf) and ConfigBgpSessionTest (clear/diff/commit/rollback + a no-op-restart regression), sharing ConfigBgpTestBase. Test Plan: - bazel test //fboss/cli/fboss2/test/config:cmd_config_test - bazel build //fboss/cli/fboss2/test/integration_test:fboss2_integration_test - fboss2_integration_test on a DUT with bgp_pp active: ConfigBgpSessionTest 6/6 pass (clear, diff, commit-restarts-bgp_pp, rollback-restores-config, and the unchanged-config does-not-restart case); agent-session regression (ConfigInterfaceMtuTest) passes. --- cmake/CliFboss2.cmake | 20 - cmake/CliFboss2TestIntegrationTest.cmake | 2 + fboss/cli/fboss2/BUCK | 15 - fboss/cli/fboss2/CmdListConfig.cpp | 99 +---- .../config/protocol/bgp/BgpConfigSession.cpp | 14 - .../config/protocol/bgp/BgpConfigSession.h | 5 - .../bgp/global/CmdConfigProtocolBgpGlobal.cpp | 413 +++++++++++++++++- .../bgp/global/CmdConfigProtocolBgpGlobal.h | 32 +- .../CmdConfigProtocolBgpGlobalClusterId.cpp | 49 --- .../CmdConfigProtocolBgpGlobalClusterId.h | 42 -- .../CmdConfigProtocolBgpGlobalConfedAsn.cpp | 57 --- .../CmdConfigProtocolBgpGlobalConfedAsn.h | 42 -- .../CmdConfigProtocolBgpGlobalHoldTime.cpp | 60 --- .../CmdConfigProtocolBgpGlobalHoldTime.h | 41 -- .../CmdConfigProtocolBgpGlobalLocalAsn.cpp | 59 --- .../CmdConfigProtocolBgpGlobalLocalAsn.h | 41 -- .../CmdConfigProtocolBgpGlobalNetwork6Add.cpp | 84 ---- .../CmdConfigProtocolBgpGlobalNetwork6Add.h | 42 -- .../CmdConfigProtocolBgpGlobalRouterId.cpp | 52 --- .../CmdConfigProtocolBgpGlobalRouterId.h | 41 -- ...tocolBgpGlobalSwitchLimitMaxGoldenVips.cpp | 51 --- ...rotocolBgpGlobalSwitchLimitMaxGoldenVips.h | 44 -- ...lobalSwitchLimitOverloadProtectionMode.cpp | 51 --- ...pGlobalSwitchLimitOverloadProtectionMode.h | 46 -- ...rotocolBgpGlobalSwitchLimitPrefixLimit.cpp | 54 --- ...gProtocolBgpGlobalSwitchLimitPrefixLimit.h | 44 -- ...ocolBgpGlobalSwitchLimitTotalPathLimit.cpp | 51 --- ...otocolBgpGlobalSwitchLimitTotalPathLimit.h | 44 -- .../test/config/BgpConfigSessionTest.cpp | 8 - fboss/cli/fboss2/test/integration_test/BUCK | 2 + .../integration_test/ConfigBgpGlobalTest.cpp | 114 +++++ .../integration_test/ConfigBgpSessionTest.cpp | 218 +++++++++ .../test/integration_test/ConfigBgpTestBase.h | 212 +++++++++ fboss/cli/fboss2/tools/bgp_json_to_cli.py | 64 ++- .../tools/tests/test_bgp_json_to_cli.py | 22 +- 35 files changed, 1027 insertions(+), 1208 deletions(-) delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalClusterId.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalClusterId.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalConfedAsn.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalConfedAsn.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalHoldTime.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalHoldTime.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalLocalAsn.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalLocalAsn.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalNetwork6Add.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalNetwork6Add.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalRouterId.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalRouterId.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit.h create mode 100644 fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp create mode 100644 fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp create mode 100644 fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h diff --git a/cmake/CliFboss2.cmake b/cmake/CliFboss2.cmake index a32f3f39e92a3..95eaf2727fed7 100644 --- a/cmake/CliFboss2.cmake +++ b/cmake/CliFboss2.cmake @@ -944,26 +944,6 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/config/protocol/bgp/CmdConfigProtocolBgp.h fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalClusterId.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalClusterId.h - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalConfedAsn.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalConfedAsn.h - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalHoldTime.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalHoldTime.h - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalLocalAsn.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalLocalAsn.h - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalNetwork6Add.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalNetwork6Add.h - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalRouterId.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalRouterId.h - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips.h - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode.h - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit.h - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit.h fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.cpp diff --git a/cmake/CliFboss2TestIntegrationTest.cmake b/cmake/CliFboss2TestIntegrationTest.cmake index cf5df8c7539f3..930515a509fcf 100644 --- a/cmake/CliFboss2TestIntegrationTest.cmake +++ b/cmake/CliFboss2TestIntegrationTest.cmake @@ -13,6 +13,8 @@ add_executable(fboss2_integration_test fboss/cli/fboss2/test/integration_test/ConfigArpTest.cpp fboss/cli/fboss2/test/integration_test/ConfigSflowRateTest.cpp fboss/cli/fboss2/test/integration_test/ConfigSflowSampleDestTest.cpp + fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp + fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp fboss/cli/fboss2/test/integration_test/ConfigConcurrentSessionsTest.cpp fboss/cli/fboss2/test/integration_test/ConfigHostnameTest.cpp fboss/cli/fboss2/test/integration_test/ConfigIcmpV4UnavailableSrcAddrTest.cpp diff --git a/fboss/cli/fboss2/BUCK b/fboss/cli/fboss2/BUCK index 25daa9463bed8..003e9a0735c69 100644 --- a/fboss/cli/fboss2/BUCK +++ b/fboss/cli/fboss2/BUCK @@ -1164,16 +1164,6 @@ cpp_library( "commands/config/protocol/bgp/BgpConfigSession.cpp", "commands/config/protocol/bgp/CmdConfigProtocolBgp.cpp", "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalClusterId.cpp", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalConfedAsn.cpp", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalHoldTime.cpp", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalLocalAsn.cpp", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalNetwork6Add.cpp", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalRouterId.cpp", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips.cpp", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode.cpp", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit.cpp", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit.cpp", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.cpp", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.cpp", @@ -1336,11 +1326,6 @@ cpp_library( "commands/config/protocol/bgp/BgpConfigSession.h", "commands/config/protocol/bgp/CmdConfigProtocolBgp.h", "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalClusterId.h", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalConfedAsn.h", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalHoldTime.h", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalLocalAsn.h", - "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalRouterId.h", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.h", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.h", diff --git a/fboss/cli/fboss2/CmdListConfig.cpp b/fboss/cli/fboss2/CmdListConfig.cpp index ecf98edf2716a..96cb32c314f44 100644 --- a/fboss/cli/fboss2/CmdListConfig.cpp +++ b/fboss/cli/fboss2/CmdListConfig.cpp @@ -39,16 +39,6 @@ #include "fboss/cli/fboss2/commands/config/protocol/CmdConfigProtocol.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/CmdConfigProtocolBgp.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalClusterId.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalConfedAsn.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalHoldTime.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalLocalAsn.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalNetwork6Add.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalRouterId.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.h" @@ -401,91 +391,14 @@ const CommandTree& kConfigCommandTree() { { { "global", - "Configure BGP global settings", + "Configure BGP global settings: " + "(router-id, local-asn, hold-time, confed-asn, " + "count-confeds-in-as-path-len, " + "graceful-restart-time, rib-allocated-path-ids, " + "network6, switch-limit[-total-path|" + "-max-golden-vips|-overload-protection-mode])", commandHandler, argRegistrar, - { - { - "router-id", - "Set BGP router identifier", - commandHandler< - CmdConfigProtocolBgpGlobalRouterId>, - argRegistrar< - CmdConfigProtocolBgpGlobalRouterIdTraits>, - }, - { - "local-asn", - "Set local AS number", - commandHandler< - CmdConfigProtocolBgpGlobalLocalAsn>, - argRegistrar< - CmdConfigProtocolBgpGlobalLocalAsnTraits>, - }, - { - "hold-time", - "Set BGP hold time in seconds", - commandHandler< - CmdConfigProtocolBgpGlobalHoldTime>, - argRegistrar< - CmdConfigProtocolBgpGlobalHoldTimeTraits>, - }, - { - "confed-asn", - "Set BGP confederation AS number", - commandHandler< - CmdConfigProtocolBgpGlobalConfedAsn>, - argRegistrar< - CmdConfigProtocolBgpGlobalConfedAsnTraits>, - }, - { - "cluster-id", - "Set route reflector cluster ID", - commandHandler< - CmdConfigProtocolBgpGlobalClusterId>, - argRegistrar< - CmdConfigProtocolBgpGlobalClusterIdTraits>, - }, - { - "network6", - "Add IPv6 network to advertise", - commandHandler< - CmdConfigProtocolBgpGlobalNetwork6Add>, - argRegistrar< - CmdConfigProtocolBgpGlobalNetwork6AddTraits>, - }, - { - "switch-limit", - "Set switch limit prefix-limit", - commandHandler< - CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit>, - argRegistrar< - CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimitTraits>, - }, - { - "switch-limit-total-path", - "Set switch limit total-path-limit", - commandHandler< - CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit>, - argRegistrar< - CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimitTraits>, - }, - { - "switch-limit-max-golden-vips", - "Set switch limit max-golden-vips", - commandHandler< - CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips>, - argRegistrar< - CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVipsTraits>, - }, - { - "switch-limit-overload-protection-mode", - "Set switch limit overload-protection-mode", - commandHandler< - CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode>, - argRegistrar< - CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionModeTraits>, - }, - }, }, { "peer-group", diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.cpp index f5378e3609638..91e12b6195302 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.cpp +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.cpp @@ -208,20 +208,6 @@ std::optional BgpConfigSession::getConfedAsn() const { return std::nullopt; } -void BgpConfigSession::setClusterId(const std::string& clusterId) { - ensureConfigLoaded(); - bgpConfig_["cluster_id"] = clusterId; -} - -std::optional BgpConfigSession::getClusterId() const { - const_cast(this)->ensureConfigLoaded(); - if (bgpConfig_.count("cluster_id") && - !bgpConfig_["cluster_id"].asString().empty()) { - return bgpConfig_["cluster_id"].asString(); - } - return std::nullopt; -} - void BgpConfigSession::setListenAddress(const std::string& listenAddr) { ensureConfigLoaded(); bgpConfig_["listen_addr"] = listenAddr; diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h index 30569f5a59bde..449d629027ed5 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h @@ -42,7 +42,6 @@ namespace facebook::fboss { * - local_as_4_byte: i64 - Local AS number (RFC 6793) * - hold_time: i32 - Hold time in seconds (default 30) * - local_confed_as_4_byte: i64 - Confederation AS number - * - cluster_id: string - Route reflector cluster ID * - peers: list - List of BGP peers * - peer_groups: list - List of peer groups * @@ -103,10 +102,6 @@ class BgpConfigSession { void setConfedAsn(uint64_t asn); std::optional getConfedAsn() const; - // cluster_id: string - Route reflector cluster ID - void setClusterId(const std::string& clusterId); - std::optional getClusterId() const; - // listen_addr: string - Listen address for BGP sessions void setListenAddress(const std::string& listenAddr); std::optional getListenAddress() const; diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp index ff97042a344b6..a78fa33b34229 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp @@ -12,11 +12,420 @@ #include "fboss/cli/fboss2/CmdHandler.cpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "fboss/cli/fboss2/session/ConfigSession.h" + namespace facebook::fboss { +namespace { + +// CLI attribute names. Kept here (not as raw string literals at the dispatch +// sites) so the valid-attribute set and the handler table stay in sync. +constexpr std::string_view kRouterId = "router-id"; +constexpr std::string_view kLocalAsn = "local-asn"; +constexpr std::string_view kHoldTime = "hold-time"; +constexpr std::string_view kConfedAsn = "confed-asn"; +constexpr std::string_view kNetwork6 = "network6"; +constexpr std::string_view kSwitchLimit = "switch-limit"; +constexpr std::string_view kSwitchLimitTotalPath = "switch-limit-total-path"; +constexpr std::string_view kSwitchLimitMaxGoldenVips = + "switch-limit-max-golden-vips"; +constexpr std::string_view kSwitchLimitOverloadProtectionMode = + "switch-limit-overload-protection-mode"; +constexpr std::string_view kCountConfedsInAsPathLen = + "count-confeds-in-as-path-len"; +constexpr std::string_view kGracefulRestartTime = "graceful-restart-time"; +constexpr std::string_view kRibAllocatedPathIds = "rib-allocated-path-ids"; + +using Tokens = std::vector; +using BgpConfig = bgp::thrift::BgpConfig; + +// Outcome of an attribute handler: on failure the message is returned to the +// user and the session is NOT persisted, so a rejected value never lands on +// disk. +struct Result { + bool ok; + std::string message; +}; + +Result ok(std::string message) { + return Result{true, std::move(message)}; +} + +Result err(std::string message) { + return Result{false, std::move(message)}; +} + +// ---- value parsers (modular, shared across attributes) -------------------- + +std::optional parseBool(const std::string& value) { + if (value == "true" || value == "1" || value == "yes") { + return true; + } + if (value == "false" || value == "0" || value == "no") { + return false; + } + return std::nullopt; +} + +template +std::optional parseInt(const std::string& value) { + try { + return folly::to(value); + } catch (const std::exception&) { + return std::nullopt; + } +} + +// Parse a non-negative value that must fit in int32 (used for second-valued +// timers and min-routes). +std::optional parseNonNegInt32(const std::string& value) { + auto parsed = parseInt(value); + if (!parsed || *parsed < 0 || *parsed > std::numeric_limits::max()) { + return std::nullopt; + } + return static_cast(*parsed); +} + +// ---- per-attribute handlers ------------------------------------------------ +// Each handler mutates the typed bgp::thrift::BgpConfig directly. Field names +// map 1:1 to bgp_config.thrift, so the compiler validates them. + +Result applyRouterId(BgpConfig& cfg, const Tokens& values) { + if (values.size() != 1) { + return err("Error: router-id requires "); + } + cfg.router_id() = values[0]; + return ok(fmt::format("Successfully set BGP router-id to: {}", values[0])); +} + +Result applyLocalAsn(BgpConfig& cfg, const Tokens& values) { + if (values.size() != 1) { + return err("Error: local-asn requires "); + } + auto asn = parseInt(values[0]); + if (!asn) { + return err(fmt::format("Error: Invalid local-asn value '{}'", values[0])); + } + cfg.local_as_4_byte() = static_cast(*asn); + return ok(fmt::format("Successfully set BGP local-asn to: {}", *asn)); +} + +Result applyConfedAsn(BgpConfig& cfg, const Tokens& values) { + if (values.size() != 1) { + return err("Error: confed-asn requires "); + } + auto asn = parseInt(values[0]); + if (!asn) { + return err(fmt::format("Error: Invalid confed-asn value '{}'", values[0])); + } + cfg.local_confed_as_4_byte() = static_cast(*asn); + return ok( + fmt::format("Successfully set BGP confederation AS number to: {}", *asn)); +} + +Result applyHoldTime(BgpConfig& cfg, const Tokens& values) { + if (values.size() != 1) { + return err("Error: hold-time requires "); + } + auto seconds = parseNonNegInt32(values[0]); + if (!seconds) { + return err(fmt::format("Error: Invalid hold-time value '{}'", values[0])); + } + cfg.hold_time() = *seconds; + return ok( + fmt::format("Successfully set BGP hold-time to: {} seconds", *seconds)); +} + +Result applyGracefulRestartTime(BgpConfig& cfg, const Tokens& values) { + if (values.size() != 1) { + return err("Error: graceful-restart-time requires "); + } + auto seconds = parseNonNegInt32(values[0]); + if (!seconds) { + return err( + fmt::format( + "Error: graceful-restart-time must be a non-negative integer " + "that fits in int32, got '{}'", + values[0])); + } + cfg.graceful_restart_convergence_seconds() = *seconds; + return ok( + fmt::format( + "Successfully set BGP graceful-restart-time to: {} seconds", + *seconds)); +} + +Result applyCountConfedsInAsPathLen(BgpConfig& cfg, const Tokens& values) { + if (values.size() != 1) { + return err("Error: count-confeds-in-as-path-len requires "); + } + auto enable = parseBool(values[0]); + if (!enable) { + return err( + fmt::format( + "Error: Invalid value '{}'; expected true or false", values[0])); + } + cfg.count_confeds_in_as_path_len() = *enable; + return ok( + fmt::format( + "Successfully {} count-confeds-in-as-path-len", + *enable ? "enabled" : "disabled")); +} + +Result applyRibAllocatedPathIds(BgpConfig& cfg, const Tokens& values) { + if (values.size() != 1) { + return err("Error: rib-allocated-path-ids requires "); + } + auto enable = parseBool(values[0]); + if (!enable) { + return err( + fmt::format( + "Error: Invalid value '{}'; expected true or false", values[0])); + } + // enable_rib_allocated_path_id lives inside the nested bgp_setting_config. + cfg.bgp_setting_config().ensure().enable_rib_allocated_path_id() = *enable; + return ok( + fmt::format( + "Successfully {} rib-allocated-path-ids", + *enable ? "enabled" : "disabled")); +} + +// Shared implementation for the switch-limit attributes, which differ only in +// the field setter, the field name in the success message, and the upper bound +// (i64 fields use INT64_MAX; the i32 max_golden_vips field uses INT32_MAX so an +// out-of-range value is rejected rather than silently truncated). All limits +// must be non-negative. +Result applyInt64Limit( + BgpConfig& cfg, + const Tokens& values, + std::string_view attrName, + std::string_view fieldName, + int64_t maxValue, + const std::function& setter) { + if (values.size() != 1) { + return err(fmt::format("Error: {} requires ", attrName)); + } + auto limit = parseInt(values[0]); + if (!limit) { + return err( + fmt::format("Error: Invalid {} value '{}'", attrName, values[0])); + } + if (*limit < 0 || *limit > maxValue) { + return err( + fmt::format( + "Error: {} value '{}' is out of range [0, {}]", + attrName, + values[0], + maxValue)); + } + setter(cfg, *limit); + return ok( + fmt::format( + "Successfully set switch_limit_config {} to: {}", fieldName, *limit)); +} + +Result applyOverloadProtectionMode(BgpConfig& cfg, const Tokens& values) { + if (values.size() != 1) { + return err("Error: switch-limit-overload-protection-mode requires "); + } + auto mode = parseInt(values[0]); + if (!mode) { + return err( + fmt::format( + "Error: Invalid overload-protection-mode value '{}'", values[0])); + } + cfg.switch_limit_config().ensure().overload_protection_mode() = + static_cast(*mode); + return ok( + fmt::format( + "Successfully set switch_limit_config overload_protection_mode to: {}", + *mode)); +} + +Result applyNetwork6(BgpConfig& cfg, const Tokens& values) { + // Usage: network6 add [policy ] [install-to-fib ] + // [min-routes ] + if (values.size() < 2 || values[0] != "add") { + return err( + "Error: usage: network6 add [policy ] " + "[install-to-fib ] [min-routes ]"); + } + std::string prefix = values[1]; + std::string policyName; + bool installToFib = false; + int32_t minSupportingRoutes = 0; + + for (size_t i = 2; i < values.size(); i++) { + if (values[i] == "policy" && i + 1 < values.size()) { + policyName = values[++i]; + } else if (values[i] == "install-to-fib" && i + 1 < values.size()) { + auto parsed = parseBool(values[++i]); + if (!parsed) { + return err( + fmt::format( + "Error: Invalid install-to-fib value '{}'; expected true or false", + values[i])); + } + installToFib = *parsed; + } else if (values[i] == "min-routes" && i + 1 < values.size()) { + auto parsed = parseNonNegInt32(values[++i]); + if (!parsed) { + return err( + fmt::format("Error: Invalid min-routes value '{}'", values[i])); + } + minSupportingRoutes = *parsed; + } else { + return err( + fmt::format("Error: unexpected network6 token '{}'", values[i])); + } + } + + // Find-or-update the networks6 entry keyed by prefix. + auto& networks = *cfg.networks6(); + bgp::thrift::BgpNetwork* entry = nullptr; + for (auto& n : networks) { + if (*n.prefix() == prefix) { + entry = &n; + break; + } + } + if (entry == nullptr) { + networks.emplace_back(); + entry = &networks.back(); + } + entry->prefix() = prefix; + entry->policy_name() = policyName; + entry->install_to_fib() = installToFib; + entry->minimum_supporting_routes() = minSupportingRoutes; + + return ok( + fmt::format( + "Successfully added network6 prefix: {} (policy: {}, install_to_fib: {}, " + "min_routes: {})", + prefix, + policyName.empty() ? "(none)" : policyName, + installToFib ? "true" : "false", + minSupportingRoutes)); +} + +using AttrHandler = std::function; + +const std::map& attrHandlers() { + static const std::map kHandlers = { + {kRouterId, applyRouterId}, + {kLocalAsn, applyLocalAsn}, + {kConfedAsn, applyConfedAsn}, + {kHoldTime, applyHoldTime}, + {kGracefulRestartTime, applyGracefulRestartTime}, + {kCountConfedsInAsPathLen, applyCountConfedsInAsPathLen}, + {kRibAllocatedPathIds, applyRibAllocatedPathIds}, + {kNetwork6, applyNetwork6}, + {kSwitchLimit, + [](BgpConfig& cfg, const Tokens& v) { + return applyInt64Limit( + cfg, + v, + kSwitchLimit, + "prefix_limit", + std::numeric_limits::max(), + [](BgpConfig& c, int64_t l) { + c.switch_limit_config().ensure().prefix_limit() = l; + }); + }}, + {kSwitchLimitTotalPath, + [](BgpConfig& cfg, const Tokens& v) { + return applyInt64Limit( + cfg, + v, + kSwitchLimitTotalPath, + "total_path_limit", + std::numeric_limits::max(), + [](BgpConfig& c, int64_t l) { + c.switch_limit_config().ensure().total_path_limit() = l; + }); + }}, + {kSwitchLimitMaxGoldenVips, + [](BgpConfig& cfg, const Tokens& v) { + // max_golden_vips is an i32 field; bound it so an out-of-range value + // is rejected rather than silently truncated by the static_cast. + return applyInt64Limit( + cfg, + v, + kSwitchLimitMaxGoldenVips, + "max_golden_vips", + std::numeric_limits::max(), + [](BgpConfig& c, int64_t l) { + c.switch_limit_config().ensure().max_golden_vips() = + static_cast(l); + }); + }}, + {kSwitchLimitOverloadProtectionMode, applyOverloadProtectionMode}, + }; + return kHandlers; +} + +std::string validAttrList() { + std::string out; + for (const auto& [name, _] : attrHandlers()) { + if (!out.empty()) { + out += ", "; + } + out += std::string(name); + } + return out; +} + +} // namespace + +// Parse + validate at construction so queryClient stays a thin dispatch. +// Throwing std::invalid_argument is how the framework surfaces arg parse +// errors (same mechanism as InterfacesConfig / parseTokens). +BgpGlobalConfig::BgpGlobalConfig(std::vector v) + : utils::BaseObjectArgType(v) { + if (v.empty()) { + throw std::invalid_argument( + fmt::format( + "Error: attribute is required. Valid attributes: {}", + validAttrList())); + } + if (attrHandlers().find(v[0]) == attrHandlers().end()) { + throw std::invalid_argument( + fmt::format( + "Error: unknown global attribute '{}'. Valid attributes: {}", + v[0], + validAttrList())); + } + attr_ = v[0]; + values_.assign(v.begin() + 1, v.end()); +} + CmdConfigProtocolBgpGlobalTraits::RetType -CmdConfigProtocolBgpGlobal::queryClient(const HostInfo& /* hostInfo */) { - return "BGP global configuration. Use subcommands: router-id, local-asn, hold-time, graceful-restart, best-path, link-bw, nh-tracking, network-originate"; +CmdConfigProtocolBgpGlobal::queryClient( + const HostInfo& /* hostInfo */, + const ObjectArgType& args) { + auto& session = ConfigSession::getInstance(); + + // The attribute is guaranteed valid: BgpGlobalConfig's constructor rejects + // an empty/unknown attribute before we get here. + auto handler = attrHandlers().find(args.attr()); + Result result = handler->second(session.getBgpConfig(), args.values()); + if (result.ok) { + session.saveBgpConfig(); + result.message += + fmt::format("\nConfig saved to: {}", session.getBgpSessionConfigPath()); + } + return result.message; } void CmdConfigProtocolBgpGlobal::printOutput(const RetType& output) { diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h index 0a6faa7a09fb6..e8851b2bc4cea 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h @@ -12,12 +12,40 @@ #include "fboss/cli/fboss2/CmdHandler.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/CmdConfigProtocolBgp.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" namespace facebook::fboss { +// Parsed `config protocol bgp global [value ...]`, validated at +// construction. values() is greedy to keep network6's multi-token tail. +class BgpGlobalConfig : public utils::BaseObjectArgType { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + /* implicit */ BgpGlobalConfig(std::vector v); + const std::string& attr() const { + return attr_; + } + const std::vector& values() const { + return values_; + } + const static utils::ObjectArgTypeId id = + utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_MESSAGE; + + private: + std::string attr_; + std::vector values_; +}; + +// Single handler for the whole `config protocol bgp global` family. The +// attribute name is the first positional token and the remaining tokens are +// its value(s); dispatch and per-attribute parsing live in the .cpp so adding +// a new global tunable is a one-entry change rather than a new command class. struct CmdConfigProtocolBgpGlobalTraits : public WriteCommandTraits { using ParentCmd = CmdConfigProtocolBgp; - using ObjectArgType = std::monostate; + static void addCliArg(CLI::App& cmd, std::vector& args) { + cmd.add_option("args", args, " [value ...]"); + } + using ObjectArgType = BgpGlobalConfig; using RetType = std::string; }; @@ -28,7 +56,7 @@ class CmdConfigProtocolBgpGlobal : public CmdHandler< using ObjectArgType = CmdConfigProtocolBgpGlobalTraits::ObjectArgType; using RetType = CmdConfigProtocolBgpGlobalTraits::RetType; - RetType queryClient(const HostInfo& hostInfo); + RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& args); void printOutput(const RetType& output); }; diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalClusterId.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalClusterId.cpp deleted file mode 100644 index ed1b541d0298f..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalClusterId.cpp +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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/protocol/bgp/global/CmdConfigProtocolBgpGlobalClusterId.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpGlobalClusterIdTraits::RetType -CmdConfigProtocolBgpGlobalClusterId::queryClient( - const HostInfo& /* hostInfo */, - const ObjectArgType& clusterIdList) { - auto& session = BgpConfigSession::getInstance(); - - if (clusterIdList.data().empty()) { - return "Error: cluster-id is required"; - } - - std::string clusterId = clusterIdList.data()[0]; - session.setClusterId(clusterId); - session.saveConfig(); - - return fmt::format( - "Successfully set BGP cluster-id to: {}\nConfig saved to: {}", - clusterId, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpGlobalClusterId::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpGlobalClusterId, - CmdConfigProtocolBgpGlobalClusterIdTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalClusterId.h b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalClusterId.h deleted file mode 100644 index 8345a5b485888..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalClusterId.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpGlobalClusterIdTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpGlobal; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("ipAddrs", args, "IPv4 or IPv6 addr(s)"); - } - using ObjectArgType = utils::IPList; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpGlobalClusterId - : public CmdHandler< - CmdConfigProtocolBgpGlobalClusterId, - CmdConfigProtocolBgpGlobalClusterIdTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpGlobalClusterIdTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpGlobalClusterIdTraits::RetType; - - RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& clusterId); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalConfedAsn.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalConfedAsn.cpp deleted file mode 100644 index bcc6512e89a38..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalConfedAsn.cpp +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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/protocol/bgp/global/CmdConfigProtocolBgpGlobalConfedAsn.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpGlobalConfedAsnTraits::RetType -CmdConfigProtocolBgpGlobalConfedAsn::queryClient( - const HostInfo& /* hostInfo */, - const ObjectArgType& confedAsnMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (confedAsnMsg.data().empty()) { - return "Error: confed-asn value is required"; - } - - std::string asnStr = confedAsnMsg.data()[0]; - uint64_t confedAsn; - try { - confedAsn = folly::to(asnStr); - } catch (const std::exception& e) { - return fmt::format("Error: Invalid confed-asn '{}': {}", asnStr, e.what()); - } - - session.setConfedAsn(confedAsn); - session.saveConfig(); - - return fmt::format( - "Successfully set BGP confederation AS number to: {}\nConfig saved to: {}", - confedAsn, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpGlobalConfedAsn::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpGlobalConfedAsn, - CmdConfigProtocolBgpGlobalConfedAsnTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalConfedAsn.h b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalConfedAsn.h deleted file mode 100644 index 2020e1b4cb1f6..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalConfedAsn.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpGlobalConfedAsnTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpGlobal; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpGlobalConfedAsn - : public CmdHandler< - CmdConfigProtocolBgpGlobalConfedAsn, - CmdConfigProtocolBgpGlobalConfedAsnTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpGlobalConfedAsnTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpGlobalConfedAsnTraits::RetType; - - RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& confedAsn); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalHoldTime.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalHoldTime.cpp deleted file mode 100644 index c526e68c97028..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalHoldTime.cpp +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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/protocol/bgp/global/CmdConfigProtocolBgpGlobalHoldTime.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpGlobalHoldTimeTraits::RetType -CmdConfigProtocolBgpGlobalHoldTime::queryClient( - const HostInfo& /* hostInfo */, - const ObjectArgType& holdTimeMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (holdTimeMsg.data().empty()) { - return "Error: hold-time value is required"; - } - - // Extract string and parse to uint32_t - std::string holdTimeStr = holdTimeMsg.data()[0]; - uint32_t holdTime; - try { - holdTime = folly::to(holdTimeStr); - } catch (const std::exception& e) { - return fmt::format( - "Error: Invalid hold-time value '{}': {}", holdTimeStr, e.what()); - } - - // Set the hold-time in the BGP config session - session.setHoldTime(holdTime); - session.saveConfig(); - - return fmt::format( - "Successfully set BGP hold-time to: {} seconds\nConfig saved to: {}", - holdTime, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpGlobalHoldTime::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpGlobalHoldTime, - CmdConfigProtocolBgpGlobalHoldTimeTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalHoldTime.h b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalHoldTime.h deleted file mode 100644 index 1db1004d320b3..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalHoldTime.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpGlobalHoldTimeTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpGlobal; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpGlobalHoldTime - : public CmdHandler< - CmdConfigProtocolBgpGlobalHoldTime, - CmdConfigProtocolBgpGlobalHoldTimeTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpGlobalHoldTimeTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpGlobalHoldTimeTraits::RetType; - - RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& holdTime); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalLocalAsn.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalLocalAsn.cpp deleted file mode 100644 index 81a8571197a79..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalLocalAsn.cpp +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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/protocol/bgp/global/CmdConfigProtocolBgpGlobalLocalAsn.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpGlobalLocalAsnTraits::RetType -CmdConfigProtocolBgpGlobalLocalAsn::queryClient( - const HostInfo& /* hostInfo */, - const ObjectArgType& localAsnMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (localAsnMsg.data().empty()) { - return "Error: local-asn value is required"; - } - - // Extract string and parse to uint32_t - std::string asnStr = localAsnMsg.data()[0]; - uint32_t localAsn; - try { - localAsn = folly::to(asnStr); - } catch (const std::exception& e) { - return fmt::format( - "Error: Invalid local-asn value '{}': {}", asnStr, e.what()); - } - - // Set the local-asn in the BGP config session - session.setLocalAsn(localAsn); - session.saveConfig(); - - return fmt::format( - "Successfully set BGP local-asn to: {}\nConfig saved to: {}", - localAsn, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpGlobalLocalAsn::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpGlobalLocalAsn, - CmdConfigProtocolBgpGlobalLocalAsnTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalLocalAsn.h b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalLocalAsn.h deleted file mode 100644 index d39cc07e490bf..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalLocalAsn.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpGlobalLocalAsnTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpGlobal; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpGlobalLocalAsn - : public CmdHandler< - CmdConfigProtocolBgpGlobalLocalAsn, - CmdConfigProtocolBgpGlobalLocalAsnTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpGlobalLocalAsnTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpGlobalLocalAsnTraits::RetType; - - RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& localAsn); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalNetwork6Add.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalNetwork6Add.cpp deleted file mode 100644 index 336f5f50f6f70..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalNetwork6Add.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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/protocol/bgp/global/CmdConfigProtocolBgpGlobalNetwork6Add.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpGlobalNetwork6AddTraits::RetType -CmdConfigProtocolBgpGlobalNetwork6Add::queryClient( - const HostInfo& /* hostInfo */, - const ObjectArgType& args) { - auto& session = BgpConfigSession::getInstance(); - - // args.data() returns a vector of strings - // Command: "config protocol bgp global network6 add [policy ] - // [install-to-fib ] [min-routes ]" - // After "network6", args contains: ["add", "", ...] - // So we need to skip tokens[0] which is "add" - const auto& tokens = args.data(); - - if (tokens.size() < 2) { - return "Error: prefix is required. Usage: network6 add [policy " - "] [install-to-fib ] [min-routes ]"; - } - - // tokens[0] is "add", tokens[1] is the actual prefix - std::string prefixStr = tokens[1]; - std::string policyName; - bool installToFib = false; - int32_t minSupportingRoutes = 0; - - // Start parsing optional args from index 2 - for (size_t i = 2; i < tokens.size(); i++) { - if (tokens[i] == "policy" && i + 1 < tokens.size()) { - policyName = tokens[++i]; - } else if (tokens[i] == "install-to-fib" && i + 1 < tokens.size()) { - std::string val = tokens[++i]; - installToFib = (val == "true" || val == "1"); - } else if (tokens[i] == "min-routes" && i + 1 < tokens.size()) { - std::string minRoutesStr = tokens[++i]; - try { - minSupportingRoutes = folly::to(minRoutesStr); - } catch (const std::exception& e) { - return fmt::format( - "Error: Invalid min-routes value '{}': {}", minRoutesStr, e.what()); - } - } - } - - session.addNetwork6(prefixStr, policyName, installToFib, minSupportingRoutes); - session.saveConfig(); - - return fmt::format( - "Successfully added network6 prefix: {} (policy: {}, install_to_fib: {}, min_routes: {})\nConfig saved to: {}", - prefixStr, - policyName.empty() ? "(none)" : policyName, - installToFib ? "true" : "false", - minSupportingRoutes, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpGlobalNetwork6Add::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpGlobalNetwork6Add, - CmdConfigProtocolBgpGlobalNetwork6AddTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalNetwork6Add.h b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalNetwork6Add.h deleted file mode 100644 index 885e7b3e2b63a..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalNetwork6Add.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpGlobalNetwork6AddTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpGlobal; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpGlobalNetwork6Add - : public CmdHandler< - CmdConfigProtocolBgpGlobalNetwork6Add, - CmdConfigProtocolBgpGlobalNetwork6AddTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpGlobalNetwork6AddTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpGlobalNetwork6AddTraits::RetType; - - RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& args); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalRouterId.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalRouterId.cpp deleted file mode 100644 index c7093026cd1e7..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalRouterId.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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/protocol/bgp/global/CmdConfigProtocolBgpGlobalRouterId.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpGlobalRouterIdTraits::RetType -CmdConfigProtocolBgpGlobalRouterId::queryClient( - const HostInfo& /* hostInfo */, - const ObjectArgType& routerIdList) { - auto& session = BgpConfigSession::getInstance(); - - if (routerIdList.data().empty()) { - return "Error: router-id is required"; - } - - // Extract the first IP from the list as router-id - std::string routerId = routerIdList.data()[0]; - - // Set the router-id in the BGP config session - session.setRouterId(routerId); - session.saveConfig(); - - return fmt::format( - "Successfully set BGP router-id to: {}\nConfig saved to: {}", - routerId, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpGlobalRouterId::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpGlobalRouterId, - CmdConfigProtocolBgpGlobalRouterIdTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalRouterId.h b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalRouterId.h deleted file mode 100644 index dd2f2871144c5..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalRouterId.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpGlobalRouterIdTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpGlobal; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("ipAddrs", args, "IPv4 or IPv6 addr(s)"); - } - using ObjectArgType = utils::IPList; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpGlobalRouterId - : public CmdHandler< - CmdConfigProtocolBgpGlobalRouterId, - CmdConfigProtocolBgpGlobalRouterIdTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpGlobalRouterIdTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpGlobalRouterIdTraits::RetType; - - RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& routerId); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips.cpp deleted file mode 100644 index 6ef8c4fd536f5..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVipsTraits::RetType -CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips::queryClient( - const HostInfo& /* hostInfo */, - const ObjectArgType& limitStr) { - auto& session = BgpConfigSession::getInstance(); - - if (limitStr.data().empty()) { - return "Error: max-golden-vips value is required"; - } - - int64_t limit = std::stoll(limitStr.data()[0]); - - session.setSwitchLimitMaxGoldenVips(limit); - session.saveConfig(); - - return fmt::format( - "Successfully set switch_limit_config max_golden_vips to: {}\nConfig saved to: {}", - limit, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips, - CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVipsTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips.h b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips.h deleted file mode 100644 index c28eab45fc3f9..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVipsTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpGlobal; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips - : public CmdHandler< - CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVips, - CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVipsTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVipsTraits::ObjectArgType; - using RetType = - CmdConfigProtocolBgpGlobalSwitchLimitMaxGoldenVipsTraits::RetType; - - RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& limit); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode.cpp deleted file mode 100644 index 78c634bf4bcd2..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionModeTraits::RetType -CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode::queryClient( - const HostInfo& /* hostInfo */, - const ObjectArgType& modeStr) { - auto& session = BgpConfigSession::getInstance(); - - if (modeStr.data().empty()) { - return "Error: overload-protection-mode value is required"; - } - - int32_t mode = std::stoi(modeStr.data()[0]); - - session.setSwitchLimitOverloadProtectionMode(mode); - session.saveConfig(); - - return fmt::format( - "Successfully set switch_limit_config overload_protection_mode to: {}\nConfig saved to: {}", - mode, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode, - CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionModeTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode.h b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode.h deleted file mode 100644 index 2a155cf1460a4..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionModeTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpGlobal; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode - : public CmdHandler< - CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionMode, - CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionModeTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionModeTraits:: - ObjectArgType; - using RetType = - CmdConfigProtocolBgpGlobalSwitchLimitOverloadProtectionModeTraits:: - RetType; - - RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& mode); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit.cpp deleted file mode 100644 index 0241e189f6f1c..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit.cpp +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimitTraits::RetType -CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit::queryClient( - const HostInfo& /* hostInfo */, - const ObjectArgType& limitStr) { - auto& session = BgpConfigSession::getInstance(); - - // Command: "config protocol bgp global switch-limit " - // After "switch-limit", args contains just [""] - const auto& tokens = limitStr.data(); - if (tokens.empty()) { - return "Error: prefix-limit value is required. Usage: switch-limit "; - } - - int64_t limit = std::stoll(tokens[0]); - - session.setSwitchLimitPrefixLimit(limit); - session.saveConfig(); - - return fmt::format( - "Successfully set switch_limit_config prefix_limit to: {}\nConfig saved to: {}", - limit, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit, - CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimitTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit.h b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit.h deleted file mode 100644 index d6496f037c749..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimitTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpGlobal; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit - : public CmdHandler< - CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimit, - CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimitTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimitTraits::ObjectArgType; - using RetType = - CmdConfigProtocolBgpGlobalSwitchLimitPrefixLimitTraits::RetType; - - RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& limit); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit.cpp deleted file mode 100644 index b0d67f57ec616..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimitTraits::RetType -CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit::queryClient( - const HostInfo& /* hostInfo */, - const ObjectArgType& limitStr) { - auto& session = BgpConfigSession::getInstance(); - - if (limitStr.data().empty()) { - return "Error: total-path-limit value is required"; - } - - int64_t limit = std::stoll(limitStr.data()[0]); - - session.setSwitchLimitTotalPathLimit(limit); - session.saveConfig(); - - return fmt::format( - "Successfully set switch_limit_config total_path_limit to: {}\nConfig saved to: {}", - limit, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit, - CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimitTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit.h b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit.h deleted file mode 100644 index 23e702fa743d7..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimitTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpGlobal; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit - : public CmdHandler< - CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimit, - CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimitTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimitTraits::ObjectArgType; - using RetType = - CmdConfigProtocolBgpGlobalSwitchLimitTotalPathLimitTraits::RetType; - - RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& limit); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/config/BgpConfigSessionTest.cpp b/fboss/cli/fboss2/test/config/BgpConfigSessionTest.cpp index efaa968b47704..da9d2698aa0b8 100644 --- a/fboss/cli/fboss2/test/config/BgpConfigSessionTest.cpp +++ b/fboss/cli/fboss2/test/config/BgpConfigSessionTest.cpp @@ -75,14 +75,6 @@ TEST_F(BgpConfigSessionTest, setConfedAsn) { EXPECT_EQ(config["local_confed_as_4_byte"].asInt(), 65100); } -TEST_F(BgpConfigSessionTest, setClusterId) { - session_->setClusterId("1.2.3.4"); - EXPECT_EQ(session_->getClusterId(), "1.2.3.4"); - - auto& config = session_->getBgpConfig(); - EXPECT_EQ(config["cluster_id"].asString(), "1.2.3.4"); -} - TEST_F(BgpConfigSessionTest, setListenAddress) { session_->setListenAddress("0.0.0.0"); EXPECT_EQ(session_->getListenAddress(), "0.0.0.0"); diff --git a/fboss/cli/fboss2/test/integration_test/BUCK b/fboss/cli/fboss2/test/integration_test/BUCK index 2c95f4ddadf44..d65ae8a4809bc 100644 --- a/fboss/cli/fboss2/test/integration_test/BUCK +++ b/fboss/cli/fboss2/test/integration_test/BUCK @@ -19,6 +19,8 @@ cpp_binary( "ConfigAclRuleTest.cpp", "ConfigAdminDistanceTest.cpp", "ConfigArpTest.cpp", + "ConfigBgpGlobalTest.cpp", + "ConfigBgpSessionTest.cpp", "ConfigConcurrentSessionsTest.cpp", "ConfigCoppTest.cpp", "ConfigDhcpSourceOverrideTest.cpp", diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp new file mode 100644 index 0000000000000..b99b765b09e4f --- /dev/null +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp @@ -0,0 +1,114 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +/** + * End-to-end tests for `fboss2-dev config protocol bgp global `. + * + * Scope: the BGP *global* tunables only. Each positive test stages the change + * AND commits it, then asserts the value landed at the correct thrift field + * path in the promoted system config (/etc/coop/bgpcpp/bgpcpp.conf) that the + * bgp_pp daemon consumes. Session-lifecycle behavior (clear / diff / rollback / + * commit-restart mechanics) lives in ConfigBgpSessionTest. + * + * - count-confeds-in-as-path-len + * -> BgpConfig.count_confeds_in_as_path_len + * - graceful-restart-time + * -> BgpConfig.graceful_restart_convergence_seconds + * - rib-allocated-path-ids + * -> BgpConfig.bgp_setting_config.enable_rib_allocated_path_id + * + * Requirements: + * - The fboss2-dev binary under test (config subcommand tree). + * - HOME is set (the session file lives under $HOME/.fboss2). + * - bgp_pp is installed/active (commit restarts it). + */ + +#include +#include +#include + +#include "fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h" + +using namespace facebook::fboss; +using ::testing::HasSubstr; + +class ConfigBgpGlobalTest : public ConfigBgpTestBase {}; + +TEST_F(ConfigBgpGlobalTest, SetCountConfedsInAsPathLenTrue) { + auto config = setAndCommit("count-confeds-in-as-path-len", "true"); + ASSERT_TRUE(config.count("count_confeds_in_as_path_len")); + EXPECT_TRUE(config["count_confeds_in_as_path_len"].asBool()); +} + +TEST_F(ConfigBgpGlobalTest, SetCountConfedsInAsPathLenFalse) { + auto config = setAndCommit("count-confeds-in-as-path-len", "false"); + ASSERT_TRUE(config.count("count_confeds_in_as_path_len")); + EXPECT_FALSE(config["count_confeds_in_as_path_len"].asBool()); +} + +TEST_F(ConfigBgpGlobalTest, SetGracefulRestartTime) { + // Stage a value that differs from the current system config: committing an + // unchanged config is a no-op that yields no SHA, and the shipped + // bgpcpp.conf already sets 120. + int64_t target = 120; + try { + auto current = readSystemBgpConfig(); + if (current.count("graceful_restart_convergence_seconds") && + current["graceful_restart_convergence_seconds"].asInt() == target) { + target = 121; + } + } catch (const std::exception&) { + // No readable system config yet — any value is a change. + } + auto config = setAndCommit("graceful-restart-time", std::to_string(target)); + ASSERT_TRUE(config.count("graceful_restart_convergence_seconds")); + EXPECT_EQ(config["graceful_restart_convergence_seconds"].asInt(), target); + // Must not write the per-peer timer field name. + EXPECT_FALSE(config.count("graceful_restart_seconds")); +} + +TEST_F(ConfigBgpGlobalTest, SetRibAllocatedPathIdsTrue) { + auto config = setAndCommit("rib-allocated-path-ids", "true"); + ASSERT_TRUE(config.count("bgp_setting_config")); + ASSERT_TRUE( + config["bgp_setting_config"].count("enable_rib_allocated_path_id")); + EXPECT_TRUE( + config["bgp_setting_config"]["enable_rib_allocated_path_id"].asBool()); + // Must be nested, not at the top level. + EXPECT_FALSE(config.count("enable_rib_allocated_path_id")); +} + +TEST_F(ConfigBgpGlobalTest, SetRibAllocatedPathIdsFalse) { + auto config = setAndCommit("rib-allocated-path-ids", "false"); + ASSERT_TRUE(config.count("bgp_setting_config")); + ASSERT_TRUE( + config["bgp_setting_config"].count("enable_rib_allocated_path_id")); + EXPECT_FALSE( + config["bgp_setting_config"]["enable_rib_allocated_path_id"].asBool()); +} + +TEST_F(ConfigBgpGlobalTest, InvalidBoolValueRejected) { + clearBgpSession(); + auto result = runCli( + {"config", + "protocol", + "bgp", + "global", + "count-confeds-in-as-path-len", + "maybe"}); + // The handler reports the error on stdout; ensure it did not silently + // succeed and persist a bogus value. + EXPECT_THAT(result.stdout, HasSubstr("Invalid value")); + // The handler must bail out before saving, so no session file is written for + // the rejected input. + EXPECT_FALSE(std::filesystem::exists(bgpSessionPath())) + << "session file should not exist after rejected input"; +} + +TEST_F(ConfigBgpGlobalTest, NegativeGracefulRestartTimeRejected) { + clearBgpSession(); + auto result = runCli( + {"config", "protocol", "bgp", "global", "graceful-restart-time", "-1"}); + EXPECT_THAT(result.stdout, HasSubstr("non-negative")); + EXPECT_FALSE(std::filesystem::exists(bgpSessionPath())) + << "session file should not exist after rejected input"; +} diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp new file mode 100644 index 0000000000000..9dc600dd85ef8 --- /dev/null +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp @@ -0,0 +1,218 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +/** + * End-to-end tests for BGP config *session* management, mirroring the agent + * config session tests (e.g. ConfigSessionClearTest) but for the BGP domain. + * + * BGP global edits stage ~/.fboss2/bgp_config.json (no agent.conf), and + * `config session commit` promotes it to /etc/coop/bgpcpp/bgpcpp.conf and + * restarts bgp_pp. These tests exercise the session lifecycle around that: + * - clear (including a BGP-only session) + * - diff (a staged BGP change must be visible) + * - commit (bgp_pp is restarted) + * - rollback (the bgp_pp system config is restored) + * + * Per-attribute global config behavior lives in ConfigBgpGlobalTest. + * + * Requirements: + * - The fboss2-dev binary under test (config subcommand tree). + * - HOME is set (the session file lives under $HOME/.fboss2). + * - bgp_pp installed; commit/rollback tests skip if bgp_pp is not active. + */ + +#include +#include +#include + +#include "fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h" + +namespace fs = std::filesystem; + +using namespace facebook::fboss; +using ::testing::HasSubstr; +using ::testing::Not; + +class ConfigBgpSessionTest : public ConfigBgpTestBase {}; + +// Staging a BGP global change creates a session file; `config session clear` +// removes it (a BGP-only session has no agent.conf to key off of). +TEST_F(ConfigBgpSessionTest, CreateAndClearBgpSession) { + runBgpGlobal("graceful-restart-time", "55"); + ASSERT_TRUE(fs::exists(bgpSessionPath())) + << "expected a staged BGP session after a global change"; + + auto result = runCli({"config", "session", "clear"}); + ASSERT_EQ(result.exitCode, 0) << "stderr=" << result.stderr; + + EXPECT_FALSE(fs::exists(bgpSessionPath())) + << "config session clear must remove the staged BGP config"; +} + +TEST_F(ConfigBgpSessionTest, ClearNonExistentBgpSession) { + // Start fully clean: no agent session, no BGP session, no metadata. + discardSession(); + clearBgpSession(); + + auto result = runCli({"config", "session", "clear"}); + ASSERT_EQ(result.exitCode, 0) << "stderr=" << result.stderr; + EXPECT_THAT(result.stdout, HasSubstr("No config session exists")); +} + +// `config session diff` must surface a staged BGP global change. +TEST_F(ConfigBgpSessionTest, DiffShowsStagedBgpChange) { + runBgpGlobal("graceful-restart-time", "77"); + + auto result = runCli({"config", "session", "diff"}); + ASSERT_EQ(result.exitCode, 0) << "stderr=" << result.stderr; + EXPECT_THAT(result.stdout, Not(HasSubstr("No config session exists"))); + EXPECT_THAT(result.stdout, HasSubstr("graceful_restart_convergence_seconds")); +} + +// Committing a staged BGP change restarts the bgp_pp daemon. +TEST_F(ConfigBgpSessionTest, CommitRestartsBgpDaemon) { + if (bgpDaemonActiveState() != "active") { + GTEST_SKIP() << "bgp_pp is not active on this DUT; skipping commit/restart " + "verification"; + } + discardSession(); + const std::string pidBefore = bgpDaemonMainPid(); + + runBgpGlobal("graceful-restart-time", "123"); + ASSERT_FALSE(commitAndGetSha().empty()); + + EXPECT_TRUE(waitForBgpDaemonActive()) + << "bgp_pp did not return to active after commit; state=" + << bgpDaemonActiveState(); + + const std::string pidAfter = bgpDaemonMainPid(); + EXPECT_NE(pidAfter, "0") << "bgp_pp has no MainPID after commit"; + EXPECT_NE(pidAfter, pidBefore) << "bgp_pp MainPID unchanged (" << pidBefore + << "); expected a restart on commit"; +} + +// Rolling back a committed BGP change restores the bgp_pp system config and +// restarts bgp_pp (not just the agent config). +TEST_F(ConfigBgpSessionTest, RollbackRestoresBgpConfig) { + if (bgpDaemonActiveState() != "active") { + GTEST_SKIP() << "bgp_pp is not active on this DUT; skipping rollback " + "verification"; + } + discardSession(); + + auto convergenceSeconds = [&]() { + return readSystemBgpConfig()["graceful_restart_convergence_seconds"] + .asInt(); + }; + + // Commit A (111), then commit B (222). + runBgpGlobal("graceful-restart-time", "111"); + std::string shaA = commitAndGetSha(); + ASSERT_FALSE(shaA.empty()) << "could not parse commit A sha"; + ASSERT_TRUE(waitForBgpDaemonActive()); + + runBgpGlobal("graceful-restart-time", "222"); + ASSERT_FALSE(commitAndGetSha().empty()) << "could not parse commit B sha"; + ASSERT_TRUE(waitForBgpDaemonActive()); + + EXPECT_EQ(convergenceSeconds(), 222) + << "system bgp config should reflect the latest commit"; + + // Roll back to commit A -> system bgp config restored to 111, bgp_pp restart. + resetBgpDaemonLimit(); // avoid systemd start-limit on the rollback's restart + auto rb = runCli({"config", "rollback", shaA}); + EXPECT_EQ(rb.exitCode, 0) << "stderr=" << rb.stderr; + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgp_pp did not return active after rollback"; + + EXPECT_EQ(convergenceSeconds(), 111) + << "rollback must restore the bgp_pp system config"; +} + +// Committing a BGP config identical to what is already running must be a no-op: +// "Nothing to commit" and, crucially, NO bgp_pp restart (a restart is traffic +// affecting). saveBgpConfig() always records BGP_RESTART, so commit() has to +// compare the staged config against the running one. +TEST_F(ConfigBgpSessionTest, CommitUnchangedBgpConfigDoesNotRestart) { + if (bgpDaemonActiveState() != "active") { + GTEST_SKIP() << "bgp_pp is not active on this DUT; skipping no-op restart " + "verification"; + } + discardSession(); + + // Establish a known running value. + runBgpGlobal("graceful-restart-time", "88"); + ASSERT_FALSE(commitAndGetSha().empty()); + ASSERT_TRUE(waitForBgpDaemonActive()); + const std::string pidBefore = bgpDaemonMainPid(); + ASSERT_NE(pidBefore, "0"); + + // Stage the SAME value and commit again -> nothing to commit, no restart. + runBgpGlobal("graceful-restart-time", "88"); + auto result = runCli({"config", "session", "commit"}); + EXPECT_EQ(result.exitCode, 0) << "stderr=" << result.stderr; + EXPECT_THAT(result.stdout, HasSubstr("Nothing to commit")) + << "an unchanged BGP commit should report nothing to commit"; + + EXPECT_EQ(bgpDaemonMainPid(), pidBefore) + << "bgp_pp was restarted for an unchanged BGP config (pidBefore=" + << pidBefore << ")"; +} + +// An agent-only commit (no BGP staged) must still snapshot the running bgp_pp +// config, so a rollback to it restores BGP instead of wiping it. Pre-fix, +// bgpcpp.conf was untracked at such commits and rollback deleted it. +TEST_F(ConfigBgpSessionTest, AgentCommitSnapshotsBgpAndRollbackPreservesIt) { + if (bgpDaemonActiveState() != "active") { + GTEST_SKIP() << "bgp_pp is not active on this DUT; skipping cross-domain " + "rollback verification"; + } + discardSession(); + clearBgpSession(); + + // The running (default) BGP config that must survive a rollback unchanged. + ASSERT_TRUE(fs::exists(systemBgpConfigPath())) + << "expected a running bgp_pp config on an active-BGP DUT"; + const folly::dynamic defaultBgp = readSystemBgpConfig(); + + // Step 1: an agent-only commit (interface description). No BGP is staged. + Interface intf = getInterfaceInfo(getRandomInterfacePortName()); + auto setRes = + runCli({"config", "interface", intf.name, "description", "bgp-rb-test"}); + ASSERT_EQ(setRes.exitCode, 0) << "stderr=" << setRes.stderr; + ASSERT_FALSE(fs::exists(bgpSessionPath())) + << "an interface-description change must not stage a BGP session"; + resetBgpDaemonLimit(); + commitConfig(); + waitForAgentReady(); + const std::string agentSha = gitHead(); + ASSERT_FALSE(agentSha.empty()) + << "could not read git HEAD after agent commit"; + + // The fix: bgpcpp.conf must be tracked at the agent-only commit, so a later + // rollback to it has a faithful BGP snapshot to restore. + EXPECT_TRUE(bgpTrackedAtRevision(agentSha)) + << "agent-only commit " << agentSha + << " did not snapshot bgpcpp.conf; a rollback to it would wipe BGP"; + + // Step 2: stage + commit a BGP change so the running config diverges from the + // snapshot captured at agentSha. + runBgpGlobal("graceful-restart-time", "222"); + ASSERT_FALSE(commitAndGetSha().empty()) << "BGP commit produced no SHA"; + ASSERT_TRUE(waitForBgpDaemonActive()); + EXPECT_EQ( + readSystemBgpConfig()["graceful_restart_convergence_seconds"].asInt(), + 222); + + // Step 3: roll back to the agent-only commit. BGP must be RESTORED to the + // original default snapshot (not deleted), and bgp_pp must come back. + resetBgpDaemonLimit(); + auto rb = runCli({"config", "rollback", agentSha}); + EXPECT_EQ(rb.exitCode, 0) << "stderr=" << rb.stderr; + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgp_pp did not return active after rollback"; + + EXPECT_TRUE(fs::exists(systemBgpConfigPath())) + << "rollback to an agent-era commit wiped the running bgp_pp config"; + EXPECT_EQ(readSystemBgpConfig(), defaultBgp) + << "rollback did not restore the original (default) bgp_pp config"; +} diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h b/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h new file mode 100644 index 0000000000000..1010fc3227855 --- /dev/null +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h @@ -0,0 +1,212 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#pragma once + +/** + * Shared fixture + helpers for BGP config integration tests. + * + * BGP config is not pushed into the agent's running config like the + * SwitchConfig-backed commands. Instead the CLI stages a BGP++ + * thrift-compatible JSON file at ~/.fboss2/bgp_config.json; `config session + * commit` promotes it to /etc/coop/bgpcpp/bgpcpp.conf and restarts the bgp_pp + * daemon. These helpers let the global-attribute tests and the + * session-lifecycle tests share the staging, commit, daemon-state, and + * system-config plumbing. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fboss/cli/fboss2/test/integration_test/Fboss2IntegrationTest.h" + +namespace facebook::fboss { + +class ConfigBgpTestBase : public Fboss2IntegrationTest { + protected: + void SetUp() override { + Fboss2IntegrationTest::SetUp(); + if (bgpDaemonActiveState() != "active") { + GTEST_SKIP() << "skipping because bgp is not active"; + } + } + + // Staged session file written by the CLI (~/.fboss2/bgp_config.json). + std::string bgpSessionPath() const { + // NOLINTNEXTLINE(concurrency-mt-unsafe): HOME is read-only in practice + const char* home = std::getenv("HOME"); + if (home == nullptr) { + throw std::runtime_error("HOME environment variable not set"); + } + return std::string(home) + "/.fboss2/bgp_config.json"; + } + + // System config consumed by the bgp_pp daemon (written on commit). + std::string systemBgpConfigPath() const { + return "/etc/coop/bgpcpp/bgpcpp.conf"; + } + + // Remove any pre-existing staged BGP session so each test starts clean. + void clearBgpSession() const { + std::remove(bgpSessionPath().c_str()); + } + + folly::dynamic readBgpSessionConfig() const { + std::string content; + if (!folly::readFile(bgpSessionPath().c_str(), content)) { + throw std::runtime_error( + "Failed to read BGP session config at " + bgpSessionPath()); + } + return folly::parseJson(content); + } + + folly::dynamic readSystemBgpConfig() const { + std::string content; + if (!folly::readFile(systemBgpConfigPath().c_str(), content)) { + throw std::runtime_error( + "Failed to read system BGP config at " + systemBgpConfigPath()); + } + return folly::parseJson(content); + } + + // Root of the git repo that versions both the agent config (cli/agent.conf) + // and the BGP config (bgpcpp/bgpcpp.conf). + std::string coopRepoDir() const { + return "/etc/coop"; + } + + // Current git HEAD sha of the /etc/coop repo (trimmed; "" on failure). + // runCmd uses folly::Subprocess, which does not search PATH, so git is + // invoked by full path. + std::string gitHead() const { + auto r = runCmd({"/usr/bin/git", "-C", coopRepoDir(), "rev-parse", "HEAD"}); + std::string s = r.stdout; + while (!s.empty() && + (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) { + s.pop_back(); + } + return s; + } + + // True if bgpcpp/bgpcpp.conf is tracked (present in the tree) at the given + // revision. `git cat-file -e :` exits 0 iff the blob exists. + bool bgpTrackedAtRevision(const std::string& sha) const { + auto r = runCmd( + {"/usr/bin/git", + "-C", + coopRepoDir(), + "cat-file", + "-e", + sha + ":bgpcpp/bgpcpp.conf"}); + return r.exitCode == 0; + } + + // Stage a `config protocol bgp global ` change (no commit). + // Returns the staged session JSON. + folly::dynamic runBgpGlobal(const std::string& attr, const std::string& value) + const { + clearBgpSession(); + XLOG(INFO) << "Running: config protocol bgp global " << attr << " " + << value; + auto result = runCli({"config", "protocol", "bgp", "global", attr, value}); + EXPECT_EQ(result.exitCode, 0) + << "stdout=" << result.stdout << " stderr=" << result.stderr; + // Handler error strings are printed to stdout with exit code 0, so guard + // against silent failures by rejecting an "Error" prefix. + EXPECT_THAT(result.stdout, ::testing::Not(::testing::HasSubstr("Error:"))); + return readBgpSessionConfig(); + } + + // Reset bgp_pp's systemd start-limit burst counter. Tests commit several + // times in quick succession, and each commit restarts bgp_pp; without this, + // systemd's default StartLimitBurst (5 starts / 10s) refuses a rapid restart + // with "start limit hit" — a test artifact, not a product issue. + void resetBgpDaemonLimit() const { + runCmd({"/usr/bin/systemctl", "reset-failed", "bgp_pp"}); + } + + // Run `config session commit` and return the short SHA parsed from + // "Config session committed successfully as ". Empty string on failure. + std::string commitAndGetSha() const { + resetBgpDaemonLimit(); + auto r = runCli({"config", "session", "commit"}); + EXPECT_EQ(r.exitCode, 0) << "stderr=" << r.stderr; + const std::string marker = "as "; + auto pos = r.stdout.find(marker); + if (pos == std::string::npos) { + return ""; + } + pos += marker.size(); + auto end = r.stdout.find_first_of(" .\n", pos); + return r.stdout.substr(pos, end - pos); + } + + // Stage a global change, commit it, wait for bgp_pp to come back, and return + // the promoted system config (the authoritative post-commit location). + // A no-op commit (staged value already in the system config) yields no SHA + // and is fine: the caller's assertions on the returned config still verify + // the value; commit/restart mechanics are covered by ConfigBgpSessionTest. + folly::dynamic setAndCommit(const std::string& attr, const std::string& value) + const { + discardSession(); + runBgpGlobal(attr, value); + commitAndGetSha(); + EXPECT_TRUE(waitForBgpDaemonActive()) + << "bgp_pp did not return active after commit; state=" + << bgpDaemonActiveState(); + return readSystemBgpConfig(); + } + + // Trailing-whitespace-trimmed stdout of a `systemctl` query for bgp_pp. + std::string systemctlValue(const std::string& property) const { + // Full path: folly::Subprocess (used by runCmd) does not search PATH. + auto r = runCmd( + {"/usr/bin/systemctl", "show", "bgp_pp", "-p", property, "--value"}); + std::string s = r.stdout; + while (!s.empty() && + (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) { + s.pop_back(); + } + return s; + } + + // "active" / "inactive" / "failed" / ... (trimmed). + std::string bgpDaemonActiveState() const { + auto r = runCmd({"/usr/bin/systemctl", "is-active", "bgp_pp"}); + std::string s = r.stdout; + while (!s.empty() && + (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) { + s.pop_back(); + } + return s; + } + + // bgp_pp main PID per systemd ("0" if not running). + std::string bgpDaemonMainPid() const { + return systemctlValue("MainPID"); + } + + // Poll until bgp_pp reports active or the timeout elapses. + bool waitForBgpDaemonActive( + std::chrono::seconds timeout = std::chrono::seconds(60)) const { + auto deadline = std::chrono::steady_clock::now() + timeout; + while (std::chrono::steady_clock::now() < deadline) { + if (bgpDaemonActiveState() == "active") { + return true; + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + return bgpDaemonActiveState() == "active"; + } +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/tools/bgp_json_to_cli.py b/fboss/cli/fboss2/tools/bgp_json_to_cli.py index ddf328c4fd3ca..cbc096e7a0bcd 100644 --- a/fboss/cli/fboss2/tools/bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/bgp_json_to_cli.py @@ -20,7 +20,7 @@ import json import shlex import sys -from typing import Any, Dict, List +from typing import Any def escape_shell_arg(arg: str) -> str: @@ -28,7 +28,7 @@ def escape_shell_arg(arg: str) -> str: return shlex.quote(str(arg)) -def _generate_global_basic_commands(config: Dict[str, Any]) -> List[str]: +def _generate_global_basic_commands(config: dict[str, Any]) -> list[str]: """Generate basic global BGP commands.""" commands = [] if "router_id" in config: @@ -47,14 +47,10 @@ def _generate_global_basic_commands(config: Dict[str, Any]) -> List[str]: commands.append( f"config protocol bgp global confed-asn {escape_shell_arg(str(config['local_confed_as_4_byte']))}" ) - if "cluster_id" in config: - commands.append( - f"config protocol bgp global cluster-id {escape_shell_arg(config['cluster_id'])}" - ) return commands -def _generate_network6_commands(networks: List[Dict[str, Any]]) -> List[str]: +def _generate_network6_commands(networks: list[dict[str, Any]]) -> list[str]: """Generate network6 commands for global BGP config.""" commands = [] for network in networks: @@ -76,7 +72,7 @@ def _generate_network6_commands(networks: List[Dict[str, Any]]) -> List[str]: return commands -def _generate_switch_limit_commands(switch_limit: Dict[str, Any]) -> List[str]: +def _generate_switch_limit_commands(switch_limit: dict[str, Any]) -> list[str]: """Generate switch_limit_config commands.""" commands = [] if "prefix_limit" in switch_limit: @@ -98,7 +94,7 @@ def _generate_switch_limit_commands(switch_limit: Dict[str, Any]) -> List[str]: return commands -def generate_global_commands(config: Dict[str, Any]) -> List[str]: +def generate_global_commands(config: dict[str, Any]) -> list[str]: """Generate CLI commands for global BGP settings.""" commands = [] commands.extend(_generate_global_basic_commands(config)) @@ -110,8 +106,8 @@ def generate_global_commands(config: Dict[str, Any]) -> List[str]: def _generate_peer_group_basic_commands( - peer_group: Dict[str, Any], escaped_name: str -) -> List[str]: + peer_group: dict[str, Any], escaped_name: str +) -> list[str]: """Generate basic peer-group commands (remote-asn, description, policies).""" commands = [] if "remote_as_4_byte" in peer_group: @@ -138,8 +134,8 @@ def _generate_peer_group_basic_commands( def _generate_peer_group_bool_commands( - peer_group: Dict[str, Any], escaped_name: str -) -> List[str]: + peer_group: dict[str, Any], escaped_name: str +) -> list[str]: """Generate boolean flag commands for peer-group.""" commands = [] bool_fields = [ @@ -158,8 +154,8 @@ def _generate_peer_group_bool_commands( def _generate_peer_group_timer_commands( - timers: Dict[str, Any], escaped_name: str -) -> List[str]: + timers: dict[str, Any], escaped_name: str +) -> list[str]: """Generate timer commands for peer-group.""" commands = [] if timers.get("hold_time_seconds"): @@ -182,8 +178,8 @@ def _generate_peer_group_timer_commands( def _generate_peer_group_prefilter_commands( - pre_filter: Dict[str, Any], escaped_name: str -) -> List[str]: + pre_filter: dict[str, Any], escaped_name: str +) -> list[str]: """Generate pre_filter commands for peer-group.""" commands = [] if pre_filter.get("max_routes"): @@ -201,7 +197,7 @@ def _generate_peer_group_prefilter_commands( return commands -def generate_peer_group_commands(peer_group: Dict[str, Any]) -> List[str]: +def generate_peer_group_commands(peer_group: dict[str, Any]) -> list[str]: """Generate CLI commands for a peer group.""" name = peer_group.get("name", "") if not name: @@ -228,16 +224,16 @@ def format_bandwidth(bps: int) -> str: """Format bandwidth in bps to human-readable format (e.g., 10G).""" if bps >= 1_000_000_000_000 and bps % 1_000_000_000_000 == 0: return f"{bps // 1_000_000_000_000}T" - elif bps >= 1_000_000_000 and bps % 1_000_000_000 == 0: + if bps >= 1_000_000_000 and bps % 1_000_000_000 == 0: return f"{bps // 1_000_000_000}G" - elif bps >= 1_000_000 and bps % 1_000_000 == 0: + if bps >= 1_000_000 and bps % 1_000_000 == 0: return f"{bps // 1_000_000}M" - elif bps >= 1_000 and bps % 1_000 == 0: + if bps >= 1_000 and bps % 1_000 == 0: return f"{bps // 1_000}K" return str(bps) -def _generate_peer_basic_commands(peer: Dict[str, Any], peer_addr: str) -> List[str]: +def _generate_peer_basic_commands(peer: dict[str, Any], peer_addr: str) -> list[str]: """Generate basic peer commands (remote-asn, peer-group, description, etc.).""" commands = [] if "remote_as_4_byte" in peer: @@ -267,7 +263,7 @@ def _generate_peer_basic_commands(peer: Dict[str, Any], peer_addr: str) -> List[ return commands -def _generate_peer_bool_commands(peer: Dict[str, Any], peer_addr: str) -> List[str]: +def _generate_peer_bool_commands(peer: dict[str, Any], peer_addr: str) -> list[str]: """Generate boolean flag commands for peer.""" commands = [] bool_fields = [ @@ -286,7 +282,7 @@ def _generate_peer_bool_commands(peer: Dict[str, Any], peer_addr: str) -> List[s return commands -def _generate_peer_nexthop_commands(peer: Dict[str, Any], peer_addr: str) -> List[str]: +def _generate_peer_nexthop_commands(peer: dict[str, Any], peer_addr: str) -> list[str]: """Generate next-hop and link-bandwidth commands for peer.""" commands = [] if "next_hop4" in peer: @@ -306,7 +302,7 @@ def _generate_peer_nexthop_commands(peer: Dict[str, Any], peer_addr: str) -> Lis return commands -def _generate_peer_timer_commands(timers: Dict[str, Any], peer_addr: str) -> List[str]: +def _generate_peer_timer_commands(timers: dict[str, Any], peer_addr: str) -> list[str]: """Generate timer commands for peer.""" commands = [] if timers.get("hold_time_seconds"): @@ -329,8 +325,8 @@ def _generate_peer_timer_commands(timers: Dict[str, Any], peer_addr: str) -> Lis def _generate_peer_prefilter_commands( - pre_filter: Dict[str, Any], peer_addr: str -) -> List[str]: + pre_filter: dict[str, Any], peer_addr: str +) -> list[str]: """Generate pre_filter commands for peer.""" commands = [] if pre_filter.get("max_routes"): @@ -348,7 +344,7 @@ def _generate_peer_prefilter_commands( return commands -def _generate_peer_id_commands(peer: Dict[str, Any], peer_addr: str) -> List[str]: +def _generate_peer_id_commands(peer: dict[str, Any], peer_addr: str) -> list[str]: """Generate peer identification commands.""" commands = [] if "peer_id" in peer: @@ -362,7 +358,7 @@ def _generate_peer_id_commands(peer: Dict[str, Any], peer_addr: str) -> List[str return commands -def generate_peer_commands(peer: Dict[str, Any]) -> List[str]: +def generate_peer_commands(peer: dict[str, Any]) -> list[str]: """Generate CLI commands for a peer.""" peer_addr = peer.get("peer_addr", "") if not peer_addr: @@ -385,7 +381,7 @@ def generate_peer_commands(peer: Dict[str, Any]) -> List[str]: return commands -def generate_stub_commands(commands: List[str], binary: str = "fboss2") -> List[str]: +def generate_stub_commands(commands: list[str], binary: str = "fboss2") -> list[str]: """Wrap commands for stub mode execution.""" stub_commands = [ "#!/bin/bash", @@ -400,7 +396,7 @@ def generate_stub_commands(commands: List[str], binary: str = "fboss2") -> List[ return stub_commands -def generate_exec_commands(commands: List[str], binary: str = "fboss2") -> List[str]: +def generate_exec_commands(commands: list[str], binary: str = "fboss2") -> list[str]: """Generate executable commands with custom binary.""" exec_commands = [ "#!/bin/bash", @@ -425,8 +421,8 @@ def generate_exec_commands(commands: List[str], binary: str = "fboss2") -> List[ def json_to_cli( - config: Dict[str, Any], stub: bool = False, binary: str = "fboss2" -) -> List[str]: + config: dict[str, Any], stub: bool = False, binary: str = "fboss2" +) -> list[str]: """Convert BGP++ JSON config to CLI commands.""" commands = [] @@ -474,7 +470,7 @@ def main() -> int: args = parser.parse_args() try: - with open(args.input, "r") as f: + with open(args.input) as f: config = json.load(f) except FileNotFoundError: print(f"Error: Input file '{args.input}' not found", file=sys.stderr) diff --git a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py index 0c5f7a604aad2..9d5377cb34730 100644 --- a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py @@ -134,14 +134,6 @@ def test_confed_asn(self) -> None: commands = generate_global_commands(config) self.assertEqual(commands, ["config protocol bgp global confed-asn 65001"]) - def test_cluster_id(self) -> None: - """Cluster ID should generate correct command.""" - config = {"cluster_id": "192.168.1.1"} - commands = generate_global_commands(config) - self.assertEqual( - commands, ["config protocol bgp global cluster-id 192.168.1.1"] - ) - def test_networks6_with_all_fields(self) -> None: """networks6 should generate command with all fields.""" config = { @@ -162,14 +154,7 @@ def test_networks6_with_all_fields(self) -> None: def test_networks6_with_install_to_fib_true(self) -> None: """networks6 with install_to_fib=true should preserve value.""" - config = { - "networks6": [ - { - "prefix": "2001:db8::/32", - "install_to_fib": True, - } - ] - } + config = {"networks6": [{"prefix": "2001:db8::/32", "install_to_fib": True}]} commands = generate_global_commands(config) self.assertIn("install-to-fib true", commands[0]) @@ -569,10 +554,7 @@ def test_full_rsw_config(self) -> None: "keep_alive_seconds": 10, }, }, - { - "name": "RSW-RTSW-V6", - "disable_ipv4_afi": True, - }, + {"name": "RSW-RTSW-V6", "disable_ipv4_afi": True}, ], "peers": [ { From 2a087fdf5bdf82f43b9c19dbb5658b4bd23c2183 Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Thu, 16 Jul 2026 08:09:01 +0000 Subject: [PATCH 02/22] bgp global command fixes - Extract the value parsers (parseBool/parseInt/parseNonNegInt32, handler Result) into a shared BgpCliValueParsers.h so sibling BGP dispatchers can reuse them, and add a bounded parseAsn4Byte: local-asn/confed-asn accepted any uint64 and silently persisted out-of-range ASNs (>= 2^32 wrap the i64 field negative). - positionals_at_end() on the global command: CLI11's parent-chain subcommand fallthrough steals value tokens that match a sibling command name (e.g. a policy named "peer-group") and misparses the command. - Integration test base: probe the bgpd unit, and pass -c safe.directory=/etc/coop on the raw git invocations (gitHead / bgpTrackedAtRevision), mirroring the CLI's Git class -- /etc/coop is owned by another user (e.g. coop) on provisioned devices, which git otherwise rejects as dubious ownership and the helpers silently return empty results. --- cmake/CliFboss2.cmake | 1 + fboss/cli/fboss2/BUCK | 1 + .../config/protocol/bgp/BgpCliValueParsers.h | 96 +++++++++++++++++++ .../bgp/global/CmdConfigProtocolBgpGlobal.cpp | 85 ++++++---------- .../bgp/global/CmdConfigProtocolBgpGlobal.h | 8 ++ .../integration_test/ConfigBgpGlobalTest.cpp | 4 +- .../integration_test/ConfigBgpSessionTest.cpp | 54 +++++------ .../test/integration_test/ConfigBgpTestBase.h | 39 +++++--- .../tools/tests/test_bgp_json_to_cli.py | 9 +- 9 files changed, 191 insertions(+), 106 deletions(-) create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h diff --git a/cmake/CliFboss2.cmake b/cmake/CliFboss2.cmake index 95eaf2727fed7..4d302785d03f2 100644 --- a/cmake/CliFboss2.cmake +++ b/cmake/CliFboss2.cmake @@ -938,6 +938,7 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/config/mac/aging_time/CmdConfigMacAgingTime.h fboss/cli/fboss2/commands/config/protocol/CmdConfigProtocol.cpp fboss/cli/fboss2/commands/config/protocol/CmdConfigProtocol.h + fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.cpp fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h fboss/cli/fboss2/commands/config/protocol/bgp/CmdConfigProtocolBgp.cpp diff --git a/fboss/cli/fboss2/BUCK b/fboss/cli/fboss2/BUCK index 003e9a0735c69..be654f984cb11 100644 --- a/fboss/cli/fboss2/BUCK +++ b/fboss/cli/fboss2/BUCK @@ -1323,6 +1323,7 @@ cpp_library( "commands/config/mac/CmdConfigMac.h", "commands/config/mac/aging_time/CmdConfigMacAgingTime.h", "commands/config/protocol/CmdConfigProtocol.h", + "commands/config/protocol/bgp/BgpCliValueParsers.h", "commands/config/protocol/bgp/BgpConfigSession.h", "commands/config/protocol/bgp/CmdConfigProtocolBgp.h", "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h", diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h new file mode 100644 index 0000000000000..6e80deec368b4 --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h @@ -0,0 +1,96 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include + +/** + * Value parsing helpers shared by the BGP config dispatchers + * (`config protocol bgp global` / `config protocol bgp neighbor`). + * + * All parsers return std::nullopt on invalid input instead of throwing so a + * handler can reject the value with a user-facing message and leave the + * session unpersisted. + */ +namespace facebook::fboss::bgpcli { + +// Outcome of an attribute handler: on failure the message is returned to the +// user and the session is NOT persisted, so a rejected value never lands on +// disk. +struct Result { + bool ok; + std::string message; +}; + +inline Result ok(std::string message) { + return Result{true, std::move(message)}; +} + +inline Result err(std::string message) { + return Result{false, std::move(message)}; +} + +inline std::optional parseBool(const std::string& value) { + if (value == "true" || value == "1" || value == "yes") { + return true; + } + if (value == "false" || value == "0" || value == "no") { + return false; + } + return std::nullopt; +} + +template +std::optional parseInt(const std::string& value) { + try { + return folly::to(value); + } catch (const std::exception&) { + return std::nullopt; + } +} + +// Parse a non-negative value that must fit in int32 (used for second-valued +// timers and min-routes). +inline std::optional parseNonNegInt32(const std::string& value) { + auto parsed = parseInt(value); + if (!parsed || *parsed < 0 || *parsed > std::numeric_limits::max()) { + return std::nullopt; + } + return static_cast(*parsed); +} + +// Parse a non-negative int64 (used for route limits). +inline std::optional parseNonNegInt64(const std::string& value) { + auto parsed = parseInt(value); + if (!parsed || *parsed < 0) { + return std::nullopt; + } + return parsed; +} + +// Parse a 4-byte ASN (RFC 6793): an unsigned value in [0, 2^32-1]. The thrift +// fields are i64, so an unchecked uint64 parse would let an out-of-range ASN +// wrap or exceed the protocol range and be persisted. +inline std::optional parseAsn4Byte(const std::string& value) { + auto parsed = parseInt(value); + if (!parsed || *parsed > std::numeric_limits::max()) { + return std::nullopt; + } + return static_cast(*parsed); +} + +} // namespace facebook::fboss::bgpcli diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp index a78fa33b34229..b824d1aaae0e7 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp @@ -13,17 +13,23 @@ #include "fboss/cli/fboss2/CmdHandler.cpp" #include -#include #include +#include #include #include +#include #include #include -#include +#include +#include #include #include #include +#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h" #include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" +#include "fmt/format.h" namespace facebook::fboss { @@ -49,53 +55,14 @@ constexpr std::string_view kRibAllocatedPathIds = "rib-allocated-path-ids"; using Tokens = std::vector; using BgpConfig = bgp::thrift::BgpConfig; - -// Outcome of an attribute handler: on failure the message is returned to the -// user and the session is NOT persisted, so a rejected value never lands on -// disk. -struct Result { - bool ok; - std::string message; -}; - -Result ok(std::string message) { - return Result{true, std::move(message)}; -} - -Result err(std::string message) { - return Result{false, std::move(message)}; -} - -// ---- value parsers (modular, shared across attributes) -------------------- - -std::optional parseBool(const std::string& value) { - if (value == "true" || value == "1" || value == "yes") { - return true; - } - if (value == "false" || value == "0" || value == "no") { - return false; - } - return std::nullopt; -} - -template -std::optional parseInt(const std::string& value) { - try { - return folly::to(value); - } catch (const std::exception&) { - return std::nullopt; - } -} - -// Parse a non-negative value that must fit in int32 (used for second-valued -// timers and min-routes). -std::optional parseNonNegInt32(const std::string& value) { - auto parsed = parseInt(value); - if (!parsed || *parsed < 0 || *parsed > std::numeric_limits::max()) { - return std::nullopt; - } - return static_cast(*parsed); -} +// Value parsers and the handler Result type are shared with the neighbor +// dispatcher (see BgpCliValueParsers.h). +using bgpcli::err; +using bgpcli::ok; +using bgpcli::parseBool; +using bgpcli::parseInt; +using bgpcli::parseNonNegInt32; +using bgpcli::Result; // ---- per-attribute handlers ------------------------------------------------ // Each handler mutates the typed bgp::thrift::BgpConfig directly. Field names @@ -113,11 +80,15 @@ Result applyLocalAsn(BgpConfig& cfg, const Tokens& values) { if (values.size() != 1) { return err("Error: local-asn requires "); } - auto asn = parseInt(values[0]); + auto asn = bgpcli::parseAsn4Byte(values[0]); if (!asn) { - return err(fmt::format("Error: Invalid local-asn value '{}'", values[0])); + return err( + fmt::format( + "Error: Invalid local-asn value '{}'; expected an unsigned " + "4-byte ASN", + values[0])); } - cfg.local_as_4_byte() = static_cast(*asn); + cfg.local_as_4_byte() = *asn; return ok(fmt::format("Successfully set BGP local-asn to: {}", *asn)); } @@ -125,11 +96,15 @@ Result applyConfedAsn(BgpConfig& cfg, const Tokens& values) { if (values.size() != 1) { return err("Error: confed-asn requires "); } - auto asn = parseInt(values[0]); + auto asn = bgpcli::parseAsn4Byte(values[0]); if (!asn) { - return err(fmt::format("Error: Invalid confed-asn value '{}'", values[0])); + return err( + fmt::format( + "Error: Invalid confed-asn value '{}'; expected an unsigned " + "4-byte ASN", + values[0])); } - cfg.local_confed_as_4_byte() = static_cast(*asn); + cfg.local_confed_as_4_byte() = *asn; return ok( fmt::format("Successfully set BGP confederation AS number to: {}", *asn)); } diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h index e8851b2bc4cea..2c64bc0d4d537 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h @@ -10,9 +10,13 @@ #pragma once +#include +#include +#include "CLI/App.hpp" #include "fboss/cli/fboss2/CmdHandler.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/CmdConfigProtocolBgp.h" #include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" namespace facebook::fboss { @@ -43,6 +47,10 @@ class BgpGlobalConfig : public utils::BaseObjectArgType { struct CmdConfigProtocolBgpGlobalTraits : public WriteCommandTraits { using ParentCmd = CmdConfigProtocolBgp; static void addCliArg(CLI::App& cmd, std::vector& args) { + // Stop CLI11's parent-chain subcommand fallthrough from stealing value + // tokens that happen to match a sibling command name (e.g. a policy + // named "peer-group"); see CmdConfigProtocolBgpNeighborTraits. + cmd.positionals_at_end(); cmd.add_option("args", args, " [value ...]"); } using ObjectArgType = BgpGlobalConfig; diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp index b99b765b09e4f..d9449833e37cb 100644 --- a/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp @@ -6,7 +6,7 @@ * Scope: the BGP *global* tunables only. Each positive test stages the change * AND commits it, then asserts the value landed at the correct thrift field * path in the promoted system config (/etc/coop/bgpcpp/bgpcpp.conf) that the - * bgp_pp daemon consumes. Session-lifecycle behavior (clear / diff / rollback / + * bgpd daemon consumes. Session-lifecycle behavior (clear / diff / rollback / * commit-restart mechanics) lives in ConfigBgpSessionTest. * * - count-confeds-in-as-path-len @@ -19,7 +19,7 @@ * Requirements: * - The fboss2-dev binary under test (config subcommand tree). * - HOME is set (the session file lives under $HOME/.fboss2). - * - bgp_pp is installed/active (commit restarts it). + * - bgpd is installed/active (commit restarts it). */ #include diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp index 9dc600dd85ef8..ead4bf28cecd2 100644 --- a/fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp @@ -6,18 +6,18 @@ * * BGP global edits stage ~/.fboss2/bgp_config.json (no agent.conf), and * `config session commit` promotes it to /etc/coop/bgpcpp/bgpcpp.conf and - * restarts bgp_pp. These tests exercise the session lifecycle around that: + * restarts bgpd. These tests exercise the session lifecycle around that: * - clear (including a BGP-only session) * - diff (a staged BGP change must be visible) - * - commit (bgp_pp is restarted) - * - rollback (the bgp_pp system config is restored) + * - commit (bgpd is restarted) + * - rollback (the bgpd system config is restored) * * Per-attribute global config behavior lives in ConfigBgpGlobalTest. * * Requirements: * - The fboss2-dev binary under test (config subcommand tree). * - HOME is set (the session file lives under $HOME/.fboss2). - * - bgp_pp installed; commit/rollback tests skip if bgp_pp is not active. + * - bgpd installed; commit/rollback tests skip if bgpd is not active. */ #include @@ -68,10 +68,10 @@ TEST_F(ConfigBgpSessionTest, DiffShowsStagedBgpChange) { EXPECT_THAT(result.stdout, HasSubstr("graceful_restart_convergence_seconds")); } -// Committing a staged BGP change restarts the bgp_pp daemon. +// Committing a staged BGP change restarts the bgpd daemon. TEST_F(ConfigBgpSessionTest, CommitRestartsBgpDaemon) { if (bgpDaemonActiveState() != "active") { - GTEST_SKIP() << "bgp_pp is not active on this DUT; skipping commit/restart " + GTEST_SKIP() << "bgpd is not active on this DUT; skipping commit/restart " "verification"; } discardSession(); @@ -81,20 +81,20 @@ TEST_F(ConfigBgpSessionTest, CommitRestartsBgpDaemon) { ASSERT_FALSE(commitAndGetSha().empty()); EXPECT_TRUE(waitForBgpDaemonActive()) - << "bgp_pp did not return to active after commit; state=" + << "bgpd did not return to active after commit; state=" << bgpDaemonActiveState(); const std::string pidAfter = bgpDaemonMainPid(); - EXPECT_NE(pidAfter, "0") << "bgp_pp has no MainPID after commit"; - EXPECT_NE(pidAfter, pidBefore) << "bgp_pp MainPID unchanged (" << pidBefore + EXPECT_NE(pidAfter, "0") << "bgpd has no MainPID after commit"; + EXPECT_NE(pidAfter, pidBefore) << "bgpd MainPID unchanged (" << pidBefore << "); expected a restart on commit"; } -// Rolling back a committed BGP change restores the bgp_pp system config and -// restarts bgp_pp (not just the agent config). +// Rolling back a committed BGP change restores the bgpd system config and +// restarts bgpd (not just the agent config). TEST_F(ConfigBgpSessionTest, RollbackRestoresBgpConfig) { if (bgpDaemonActiveState() != "active") { - GTEST_SKIP() << "bgp_pp is not active on this DUT; skipping rollback " + GTEST_SKIP() << "bgpd is not active on this DUT; skipping rollback " "verification"; } discardSession(); @@ -117,24 +117,24 @@ TEST_F(ConfigBgpSessionTest, RollbackRestoresBgpConfig) { EXPECT_EQ(convergenceSeconds(), 222) << "system bgp config should reflect the latest commit"; - // Roll back to commit A -> system bgp config restored to 111, bgp_pp restart. + // Roll back to commit A -> system bgp config restored to 111, bgpd restart. resetBgpDaemonLimit(); // avoid systemd start-limit on the rollback's restart auto rb = runCli({"config", "rollback", shaA}); EXPECT_EQ(rb.exitCode, 0) << "stderr=" << rb.stderr; ASSERT_TRUE(waitForBgpDaemonActive()) - << "bgp_pp did not return active after rollback"; + << "bgpd did not return active after rollback"; EXPECT_EQ(convergenceSeconds(), 111) - << "rollback must restore the bgp_pp system config"; + << "rollback must restore the bgpd system config"; } // Committing a BGP config identical to what is already running must be a no-op: -// "Nothing to commit" and, crucially, NO bgp_pp restart (a restart is traffic -// affecting). saveBgpConfig() always records BGP_RESTART, so commit() has to -// compare the staged config against the running one. +// "Nothing to commit" and, crucially, NO bgpd restart (a restart is traffic +// affecting). saveBgpConfig() always records SERVICE_RESTART, so commit() has +// to compare the staged config against the running one. TEST_F(ConfigBgpSessionTest, CommitUnchangedBgpConfigDoesNotRestart) { if (bgpDaemonActiveState() != "active") { - GTEST_SKIP() << "bgp_pp is not active on this DUT; skipping no-op restart " + GTEST_SKIP() << "bgpd is not active on this DUT; skipping no-op restart " "verification"; } discardSession(); @@ -154,16 +154,16 @@ TEST_F(ConfigBgpSessionTest, CommitUnchangedBgpConfigDoesNotRestart) { << "an unchanged BGP commit should report nothing to commit"; EXPECT_EQ(bgpDaemonMainPid(), pidBefore) - << "bgp_pp was restarted for an unchanged BGP config (pidBefore=" + << "bgpd was restarted for an unchanged BGP config (pidBefore=" << pidBefore << ")"; } -// An agent-only commit (no BGP staged) must still snapshot the running bgp_pp +// An agent-only commit (no BGP staged) must still snapshot the running bgpd // config, so a rollback to it restores BGP instead of wiping it. Pre-fix, // bgpcpp.conf was untracked at such commits and rollback deleted it. TEST_F(ConfigBgpSessionTest, AgentCommitSnapshotsBgpAndRollbackPreservesIt) { if (bgpDaemonActiveState() != "active") { - GTEST_SKIP() << "bgp_pp is not active on this DUT; skipping cross-domain " + GTEST_SKIP() << "bgpd is not active on this DUT; skipping cross-domain " "rollback verification"; } discardSession(); @@ -171,7 +171,7 @@ TEST_F(ConfigBgpSessionTest, AgentCommitSnapshotsBgpAndRollbackPreservesIt) { // The running (default) BGP config that must survive a rollback unchanged. ASSERT_TRUE(fs::exists(systemBgpConfigPath())) - << "expected a running bgp_pp config on an active-BGP DUT"; + << "expected a running bgpd config on an active-BGP DUT"; const folly::dynamic defaultBgp = readSystemBgpConfig(); // Step 1: an agent-only commit (interface description). No BGP is staged. @@ -204,15 +204,15 @@ TEST_F(ConfigBgpSessionTest, AgentCommitSnapshotsBgpAndRollbackPreservesIt) { 222); // Step 3: roll back to the agent-only commit. BGP must be RESTORED to the - // original default snapshot (not deleted), and bgp_pp must come back. + // original default snapshot (not deleted), and bgpd must come back. resetBgpDaemonLimit(); auto rb = runCli({"config", "rollback", agentSha}); EXPECT_EQ(rb.exitCode, 0) << "stderr=" << rb.stderr; ASSERT_TRUE(waitForBgpDaemonActive()) - << "bgp_pp did not return active after rollback"; + << "bgpd did not return active after rollback"; EXPECT_TRUE(fs::exists(systemBgpConfigPath())) - << "rollback to an agent-era commit wiped the running bgp_pp config"; + << "rollback to an agent-era commit wiped the running bgpd config"; EXPECT_EQ(readSystemBgpConfig(), defaultBgp) - << "rollback did not restore the original (default) bgp_pp config"; + << "rollback did not restore the original (default) bgpd config"; } diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h b/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h index 1010fc3227855..00caadaeac3e7 100644 --- a/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h @@ -8,7 +8,7 @@ * BGP config is not pushed into the agent's running config like the * SwitchConfig-backed commands. Instead the CLI stages a BGP++ * thrift-compatible JSON file at ~/.fboss2/bgp_config.json; `config session - * commit` promotes it to /etc/coop/bgpcpp/bgpcpp.conf and restarts the bgp_pp + * commit` promotes it to /etc/coop/bgpcpp/bgpcpp.conf and restarts the bgpd * daemon. These helpers let the global-attribute tests and the * session-lifecycle tests share the staging, commit, daemon-state, and * system-config plumbing. @@ -50,7 +50,7 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { return std::string(home) + "/.fboss2/bgp_config.json"; } - // System config consumed by the bgp_pp daemon (written on commit). + // System config consumed by the bgpd daemon (written on commit). std::string systemBgpConfigPath() const { return "/etc/coop/bgpcpp/bgpcpp.conf"; } @@ -86,9 +86,18 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { // Current git HEAD sha of the /etc/coop repo (trimmed; "" on failure). // runCmd uses folly::Subprocess, which does not search PATH, so git is - // invoked by full path. + // invoked by full path. -c safe.directory mirrors the CLI's Git class: + // /etc/coop is typically owned by another user (e.g. coop), which git + // otherwise rejects as dubious ownership. std::string gitHead() const { - auto r = runCmd({"/usr/bin/git", "-C", coopRepoDir(), "rev-parse", "HEAD"}); + auto r = runCmd( + {"/usr/bin/git", + "-c", + "safe.directory=" + coopRepoDir(), + "-C", + coopRepoDir(), + "rev-parse", + "HEAD"}); std::string s = r.stdout; while (!s.empty() && (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) { @@ -102,6 +111,8 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { bool bgpTrackedAtRevision(const std::string& sha) const { auto r = runCmd( {"/usr/bin/git", + "-c", + "safe.directory=" + coopRepoDir(), "-C", coopRepoDir(), "cat-file", @@ -126,12 +137,12 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { return readBgpSessionConfig(); } - // Reset bgp_pp's systemd start-limit burst counter. Tests commit several - // times in quick succession, and each commit restarts bgp_pp; without this, + // Reset bgpd's systemd start-limit burst counter. Tests commit several + // times in quick succession, and each commit restarts bgpd; without this, // systemd's default StartLimitBurst (5 starts / 10s) refuses a rapid restart // with "start limit hit" — a test artifact, not a product issue. void resetBgpDaemonLimit() const { - runCmd({"/usr/bin/systemctl", "reset-failed", "bgp_pp"}); + runCmd({"/usr/bin/systemctl", "reset-failed", "bgpd"}); } // Run `config session commit` and return the short SHA parsed from @@ -150,7 +161,7 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { return r.stdout.substr(pos, end - pos); } - // Stage a global change, commit it, wait for bgp_pp to come back, and return + // Stage a global change, commit it, wait for bgpd to come back, and return // the promoted system config (the authoritative post-commit location). // A no-op commit (staged value already in the system config) yields no SHA // and is fine: the caller's assertions on the returned config still verify @@ -161,16 +172,16 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { runBgpGlobal(attr, value); commitAndGetSha(); EXPECT_TRUE(waitForBgpDaemonActive()) - << "bgp_pp did not return active after commit; state=" + << "bgpd did not return active after commit; state=" << bgpDaemonActiveState(); return readSystemBgpConfig(); } - // Trailing-whitespace-trimmed stdout of a `systemctl` query for bgp_pp. + // Trailing-whitespace-trimmed stdout of a `systemctl` query for bgpd. std::string systemctlValue(const std::string& property) const { // Full path: folly::Subprocess (used by runCmd) does not search PATH. auto r = runCmd( - {"/usr/bin/systemctl", "show", "bgp_pp", "-p", property, "--value"}); + {"/usr/bin/systemctl", "show", "bgpd", "-p", property, "--value"}); std::string s = r.stdout; while (!s.empty() && (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) { @@ -181,7 +192,7 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { // "active" / "inactive" / "failed" / ... (trimmed). std::string bgpDaemonActiveState() const { - auto r = runCmd({"/usr/bin/systemctl", "is-active", "bgp_pp"}); + auto r = runCmd({"/usr/bin/systemctl", "is-active", "bgpd"}); std::string s = r.stdout; while (!s.empty() && (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) { @@ -190,12 +201,12 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { return s; } - // bgp_pp main PID per systemd ("0" if not running). + // bgpd main PID per systemd ("0" if not running). std::string bgpDaemonMainPid() const { return systemctlValue("MainPID"); } - // Poll until bgp_pp reports active or the timeout elapses. + // Poll until bgpd reports active or the timeout elapses. bool waitForBgpDaemonActive( std::chrono::seconds timeout = std::chrono::seconds(60)) const { auto deadline = std::chrono::steady_clock::now() + timeout; diff --git a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py index 9d5377cb34730..265e9db61af00 100644 --- a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py @@ -620,14 +620,7 @@ def test_injection_in_router_id_neutralized(self) -> None: def test_injection_in_peer_addr_neutralized(self) -> None: """Command injection in peer_addr should be single-quoted.""" - config = { - "peers": [ - { - "peer_addr": "$(malicious)", - "remote_as_4_byte": 65000, - } - ] - } + config = {"peers": [{"peer_addr": "$(malicious)", "remote_as_4_byte": 65000}]} commands = json_to_cli(config, stub=False, binary="fboss2") joined = "\n".join(commands) self.assertIn("peer '$(malicious)'", joined) From 7d4f4bd484799c7aeebbda9a9e512f865e67f1af Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Fri, 17 Jul 2026 06:14:05 +0000 Subject: [PATCH 03/22] Verify committed global config via bgpd getRunningConfig RPC The commit-path global-attribute tests asserted on the promoted /etc/coop/bgpcpp/bgpcpp.conf, which only proves what the CLI wrote to disk. Route ConfigBgpTestBase::setAndCommit() through a new readRunningBgpConfigViaRpc() -- TBgpService::getRunningConfig against the local bgpd -- so every positive ConfigBgpGlobalTest asserts its attribute in the daemon's own view of its config, proving bgpd parsed and adopted the promoted file after the commit-triggered restart. The helper retries briefly on connection errors: systemd reports bgpd active as soon as the process starts, but its thrift server binds the port a few seconds later, so an RPC issued right after a restart races that window. Test Plan: - bazel build //fboss/cli/fboss2/test/integration_test:fboss2_integration_test - fboss2_integration_test on a DUT with bgpd active: ConfigBgpGlobalTest 7/7 pass (each attribute set+commit verified in bgpd's getRunningConfig view, plus the invalid-bool and negative-graceful-restart-time reject paths). --- fboss/cli/fboss2/test/integration_test/BUCK | 1 + .../integration_test/ConfigBgpGlobalTest.cpp | 9 ++-- .../test/integration_test/ConfigBgpTestBase.h | 51 +++++++++++++++++-- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/fboss/cli/fboss2/test/integration_test/BUCK b/fboss/cli/fboss2/test/integration_test/BUCK index d65ae8a4809bc..e1af58457345c 100644 --- a/fboss/cli/fboss2/test/integration_test/BUCK +++ b/fboss/cli/fboss2/test/integration_test/BUCK @@ -13,6 +13,7 @@ oncall("fboss_oss") # # Unlike the Python-based CLI tests, these C++ tests directly invoke the CLI # library code rather than spawning a subprocess. +# bazelify: extra_dep = //neteng/fboss/bgp/if:bgp_thrift cpp_binary( name = "fboss2_integration_test", srcs = [ diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp index d9449833e37cb..a4afe16a8f35c 100644 --- a/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp @@ -5,9 +5,12 @@ * * Scope: the BGP *global* tunables only. Each positive test stages the change * AND commits it, then asserts the value landed at the correct thrift field - * path in the promoted system config (/etc/coop/bgpcpp/bgpcpp.conf) that the - * bgpd daemon consumes. Session-lifecycle behavior (clear / diff / rollback / - * commit-restart mechanics) lives in ConfigBgpSessionTest. + * path in bgpd's own running config — fetched over its getRunningConfig RPC + * (see ConfigBgpTestBase::setAndCommit) — which proves the daemon parsed and + * adopted the promoted /etc/coop/bgpcpp/bgpcpp.conf after the commit-triggered + * restart, not merely that the CLI wrote the file. Session-lifecycle behavior + * (clear / diff / rollback / commit-restart mechanics) lives in + * ConfigBgpSessionTest. * * - count-confeds-in-as-path-len * -> BgpConfig.count_confeds_in_as_path_len diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h b/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h index 00caadaeac3e7..2103373448c8a 100644 --- a/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h @@ -28,6 +28,8 @@ #include #include "fboss/cli/fboss2/test/integration_test/Fboss2IntegrationTest.h" +#include "fboss/cli/fboss2/utils/CmdClientUtilsCommon.h" +#include "neteng/fboss/bgp/if/gen-cpp2/TBgpService.h" namespace facebook::fboss { @@ -78,6 +80,36 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { return folly::parseJson(content); } + // The running config as reported by the bgpd daemon itself, over its + // TBgpService::getRunningConfig thrift RPC. Unlike readSystemBgpConfig() + // (the promoted file on disk, which only proves what the CLI wrote), this + // proves the daemon parsed and adopted the config after the post-commit + // restart. Mirrors Fboss2IntegrationTest::getRunningConfig() for the agent. + // + // Retries on connection errors: systemd reports bgpd "active" as soon as + // the process starts, but the thrift server only binds its port a few + // seconds later — an RPC issued right after a commit-triggered restart + // would otherwise race that window and get ECONNREFUSED. + folly::dynamic readRunningBgpConfigViaRpc( + std::chrono::seconds timeout = std::chrono::seconds(30)) const { + auto deadline = std::chrono::steady_clock::now() + timeout; + while (true) { + try { + HostInfo hostInfo("localhost"); + auto client = utils::createClient>(hostInfo); + std::string configStr; + client->sync_getRunningConfig(configStr); + return folly::parseJson(configStr); + } catch (const std::exception&) { + if (std::chrono::steady_clock::now() >= deadline) { + throw; + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + } + } + // Root of the git repo that versions both the agent config (cli/agent.conf) // and the BGP config (bgpcpp/bgpcpp.conf). std::string coopRepoDir() const { @@ -162,10 +194,19 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { } // Stage a global change, commit it, wait for bgpd to come back, and return - // the promoted system config (the authoritative post-commit location). - // A no-op commit (staged value already in the system config) yields no SHA - // and is fine: the caller's assertions on the returned config still verify - // the value; commit/restart mechanics are covered by ConfigBgpSessionTest. + // bgpd's own running config as reported over its getRunningConfig RPC. + // + // The returned config is the daemon's adopted view, which is strictly + // stronger than the promoted file on disk (readSystemBgpConfig): a value + // present here proves the commit promoted /etc/coop/bgpcpp/bgpcpp.conf AND + // that bgpd parsed and adopted it after the commit-triggered restart, not + // merely that the CLI wrote the file. The running config shares the same + // BgpConfig JSON schema as the promoted file, so callers assert the same + // field paths against it. + // + // A no-op commit (staged value already current) yields no SHA and is fine: + // the caller's assertions on the returned config still verify the value; + // commit/restart mechanics are covered by ConfigBgpSessionTest. folly::dynamic setAndCommit(const std::string& attr, const std::string& value) const { discardSession(); @@ -174,7 +215,7 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { EXPECT_TRUE(waitForBgpDaemonActive()) << "bgpd did not return active after commit; state=" << bgpDaemonActiveState(); - return readSystemBgpConfig(); + return readRunningBgpConfigViaRpc(); } // Trailing-whitespace-trimmed stdout of a `systemctl` query for bgpd. From 8a8c97e8421dae2bd0962db21ee694f0af3c119d Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Tue, 11 Aug 2026 05:50:31 +0000 Subject: [PATCH 04/22] fboss2 BGP test base reads the daemon's config path Companion to "fboss2 config session seeds BGP config from the daemon's config path": ConfigBgpTestBase::systemBgpConfigPath() reads the stable /etc/coop/bgpcpp.conf daemon path instead of the promoted file, so the fixture no longer requires a prior commit to have happened and BGP tests can run first-thing on a freshly imaged box. Co-Authored-By: Claude Fable 5 --- .../cli/fboss2/test/integration_test/ConfigBgpTestBase.h | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h b/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h index 2103373448c8a..48d722a4ff6ab 100644 --- a/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h @@ -52,9 +52,14 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { return std::string(home) + "/.fboss2/bgp_config.json"; } - // System config consumed by the bgpd daemon (written on commit). + // System config consumed by the bgpd daemon: the stable path bgpd is started + // with (--config). After the first commit this is a symlink to the promoted + // bgpcpp/bgpcpp.conf; before it, it is the plain file the bgp++ RPM installs. + // Reading it (rather than the promoted path directly) means the fixture does + // not require a commit to have happened, so these tests can run first on a + // freshly imaged box. std::string systemBgpConfigPath() const { - return "/etc/coop/bgpcpp/bgpcpp.conf"; + return "/etc/coop/bgpcpp.conf"; } // Remove any pre-existing staged BGP session so each test starts clean. From af8471ca8ffc3794152fb0b225ee1b4ff39f7800 Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Tue, 15 Sep 2026 11:29:55 +0000 Subject: [PATCH 05/22] Validate router-id and overload-protection-mode in config protocol bgp global `router-id` stored whatever token it was given. bgpd builds an IPAddress from it at config load (an unparseable string fails the load) and later calls asV4() on it, so a v6 address passes construction and throws afterwards. Parse with folly::IPAddress::tryFromString, require IPv4 and store the normalized form. `switch-limit-overload-protection-mode` cast any int32 straight into the enum; bgpd switches on the value without a default and logs anything but 2 as DROP_EXCESS_PREFIXES, so -5 or 999 silently behaved as mode 1. Accept the enum name or its integer, reject everything else listing the valid names, and validate before ensure() so a rejected value does not materialize switch_limit_config. The success message keeps printing the integer so the j2c round-trip is unchanged. Adds CmdConfigBgpGlobalTest.cpp (the global command had no unit fixture) and rejection/positive-commit cases to ConfigBgpGlobalTest.cpp. Co-Authored-By: Claude Fable 5.1 --- cmake/CliFboss2TestConfig.cmake | 1 + .../bgp/global/CmdConfigProtocolBgpGlobal.cpp | 52 +++++- fboss/cli/fboss2/test/config/BUCK | 1 + .../test/config/CmdConfigBgpGlobalTest.cpp | 173 ++++++++++++++++++ .../integration_test/ConfigBgpGlobalTest.cpp | 43 +++++ 5 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 fboss/cli/fboss2/test/config/CmdConfigBgpGlobalTest.cpp diff --git a/cmake/CliFboss2TestConfig.cmake b/cmake/CliFboss2TestConfig.cmake index 0b5d0cd23bd95..050d4e264dfd2 100644 --- a/cmake/CliFboss2TestConfig.cmake +++ b/cmake/CliFboss2TestConfig.cmake @@ -9,6 +9,7 @@ add_executable(fboss2_cmd_config_test fboss/cli/fboss2/test/config/CmdConfigSrv6MySidTest.cpp fboss/cli/fboss2/test/config/CmdConfigAclRuleTest.cpp fboss/cli/fboss2/test/config/CmdConfigArpTest.cpp + fboss/cli/fboss2/test/config/CmdConfigBgpGlobalTest.cpp fboss/cli/fboss2/test/config/CmdConfigIcmpV4UnavailableSrcAddrTest.cpp fboss/cli/fboss2/test/config/CmdConfigCoppTest.cpp fboss/cli/fboss2/test/config/CmdConfigDhcpTest.cpp diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp index b824d1aaae0e7..a1660ae69ea6c 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp @@ -13,6 +13,7 @@ #include "fboss/cli/fboss2/CmdHandler.cpp" #include +#include #include #include #include @@ -30,6 +31,7 @@ #include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" #include "fboss/cli/fboss2/utils/HostInfo.h" #include "fmt/format.h" +#include "thrift/lib/cpp/Thrift.h" namespace facebook::fboss { @@ -68,12 +70,24 @@ using bgpcli::Result; // Each handler mutates the typed bgp::thrift::BgpConfig directly. Field names // map 1:1 to bgp_config.thrift, so the compiler validates them. +// bgpd constructs an IPAddress from router_id at config load (a bad string +// fails the load) and later calls asV4() on it, so only an IPv4 address is +// safe to persist. Stored normalized. Result applyRouterId(BgpConfig& cfg, const Tokens& values) { if (values.size() != 1) { return err("Error: router-id requires "); } - cfg.router_id() = values[0]; - return ok(fmt::format("Successfully set BGP router-id to: {}", values[0])); + auto addr = folly::IPAddress::tryFromString(values[0]); + if (!addr.hasValue()) { + return err(fmt::format("Error: Invalid router-id address '{}'", values[0])); + } + if (!addr->isV4()) { + return err( + fmt::format( + "Error: router-id requires an IPv4 address, got '{}'", values[0])); + } + cfg.router_id() = addr->str(); + return ok(fmt::format("Successfully set BGP router-id to: {}", addr->str())); } Result applyLocalAsn(BgpConfig& cfg, const Tokens& values) { @@ -210,22 +224,44 @@ Result applyInt64Limit( "Successfully set switch_limit_config {} to: {}", fieldName, *limit)); } +// Accepts the enum value name or its integer; anything outside the enum is +// rejected (bgpd would otherwise treat an unknown value as +// DROP_EXCESS_PREFIXES without complaint). Validated before ensure() so a +// rejected value leaves switch_limit_config untouched. The success message +// prints the integer so the j2c round-trip is stable. Result applyOverloadProtectionMode(BgpConfig& cfg, const Tokens& values) { + using OverloadProtectionMode = bgp::thrift::OverloadProtectionMode; if (values.size() != 1) { return err("Error: switch-limit-overload-protection-mode requires "); } - auto mode = parseInt(values[0]); - if (!mode) { + OverloadProtectionMode mode; + bool valid = false; + if (auto parsed = parseInt(values[0])) { + mode = static_cast(*parsed); + valid = apache::thrift::TEnumTraits::findName( + mode) != nullptr; + } else { + valid = apache::thrift::TEnumTraits::findValue( + values[0], &mode); + } + if (!valid) { + std::string names; + for (auto n : apache::thrift::TEnumTraits::names) { + names += names.empty() ? "" : ", "; + names += std::string(n); + } return err( fmt::format( - "Error: Invalid overload-protection-mode value '{}'", values[0])); + "Error: switch-limit-overload-protection-mode value '{}' is not a " + "valid mode; expected one of: {}", + values[0], + names)); } - cfg.switch_limit_config().ensure().overload_protection_mode() = - static_cast(*mode); + cfg.switch_limit_config().ensure().overload_protection_mode() = mode; return ok( fmt::format( "Successfully set switch_limit_config overload_protection_mode to: {}", - *mode)); + static_cast(mode))); } Result applyNetwork6(BgpConfig& cfg, const Tokens& values) { diff --git a/fboss/cli/fboss2/test/config/BUCK b/fboss/cli/fboss2/test/config/BUCK index 7c8b16e885119..a1c201b2a3c40 100644 --- a/fboss/cli/fboss2/test/config/BUCK +++ b/fboss/cli/fboss2/test/config/BUCK @@ -11,6 +11,7 @@ cpp_unittest( "CmdConfigAdminDistanceTest.cpp", "CmdConfigAppliedInfoTest.cpp", "CmdConfigArpTest.cpp", + "CmdConfigBgpGlobalTest.cpp", "CmdConfigCoppTest.cpp", "CmdConfigDhcpTest.cpp", "CmdConfigHistoryTest.cpp", diff --git a/fboss/cli/fboss2/test/config/CmdConfigBgpGlobalTest.cpp b/fboss/cli/fboss2/test/config/CmdConfigBgpGlobalTest.cpp new file mode 100644 index 0000000000000..15f543608052b --- /dev/null +++ b/fboss/cli/fboss2/test/config/CmdConfigBgpGlobalTest.cpp @@ -0,0 +1,173 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include +#include +#include + +#include +#include +#include +#include + +#include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/test/config/CmdConfigTestBase.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +using namespace ::testing; + +namespace facebook::fboss { + +// The global dispatcher only touches the BGP side of ConfigSession, which +// seeds from thrift schema defaults when neither a staged session nor a +// system bgpcpp.conf exists — so no seed agent config is needed (mirrors +// CmdConfigBgpNeighborTest). +class CmdConfigBgpGlobalTestFixture : public CmdConfigTestBase { + public: + CmdConfigBgpGlobalTestFixture() + : CmdConfigTestBase("bgp_global_test_%%%%-%%%%-%%%%", "") {} + + void SetUp() override { + CmdConfigTestBase::SetUp(); + setupTestableConfigSession(); + } + + std::string run(const std::vector& tokens) { + CmdConfigProtocolBgpGlobal cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpGlobalConfig(tokens)); + } + + bgp::thrift::BgpConfig& config() { + return ConfigSession::getInstance().getBgpConfig(); + } + + bool sessionFileExists() { + return std::filesystem::exists( + ConfigSession::getInstance().getBgpSessionConfigPath()); + } +}; + +// ============================================================================== +// BgpGlobalConfig (arg) validation +// ============================================================================== + +TEST_F(CmdConfigBgpGlobalTestFixture, argValidation) { + auto parsed = BgpGlobalConfig({"router-id", "10.0.0.1"}); + EXPECT_EQ(parsed.attr(), "router-id"); + EXPECT_EQ(parsed.values(), std::vector({"10.0.0.1"})); + + // Invalid: empty, unknown attribute. + EXPECT_THROW(BgpGlobalConfig({}), std::invalid_argument); + EXPECT_THROW(BgpGlobalConfig({"no-such-attr", "1"}), std::invalid_argument); +} + +// ============================================================================== +// router-id +// ============================================================================== + +TEST_F(CmdConfigBgpGlobalTestFixture, routerIdAccepted) { + auto result = run({"router-id", "10.0.0.1"}); + EXPECT_THAT(result, HasSubstr("Successfully set BGP router-id to: 10.0.0.1")); + EXPECT_EQ(*config().router_id(), "10.0.0.1"); + EXPECT_TRUE(sessionFileExists()); +} + +TEST_F(CmdConfigBgpGlobalTestFixture, routerIdInvalidRejected) { + const auto before = *config().router_id(); + auto result = run({"router-id", "not-an-ip"}); + EXPECT_THAT(result, HasSubstr("Invalid router-id address")); + EXPECT_EQ(*config().router_id(), before); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +TEST_F(CmdConfigBgpGlobalTestFixture, routerIdV6Rejected) { + const auto before = *config().router_id(); + auto result = run({"router-id", "2001:db8::1"}); + EXPECT_THAT(result, HasSubstr("requires an IPv4 address")); + EXPECT_EQ(*config().router_id(), before); + EXPECT_FALSE(sessionFileExists()); +} + +TEST_F(CmdConfigBgpGlobalTestFixture, routerIdArityRejected) { + EXPECT_THAT(run({"router-id"}), HasSubstr("requires ")); + EXPECT_THAT( + run({"router-id", "1.1.1.1", "2.2.2.2"}), + HasSubstr("requires ")); + EXPECT_FALSE(sessionFileExists()); +} + +TEST_F(CmdConfigBgpGlobalTestFixture, rejectedRouterIdKeepsStagedValue) { + run({"router-id", "10.0.0.1"}); + auto result = run({"router-id", "not-an-ip"}); + EXPECT_THAT(result, HasSubstr("Invalid router-id address")); + EXPECT_EQ(*config().router_id(), "10.0.0.1"); +} + +// ============================================================================== +// switch-limit-overload-protection-mode +// ============================================================================== + +TEST_F(CmdConfigBgpGlobalTestFixture, overloadProtectionModeIntegerAccepted) { + auto result = run({"switch-limit-overload-protection-mode", "1"}); + EXPECT_THAT(result, HasSubstr("overload_protection_mode to: 1")); + ASSERT_TRUE(config().switch_limit_config().has_value()); + EXPECT_EQ( + *config().switch_limit_config()->overload_protection_mode(), + bgp::thrift::OverloadProtectionMode::DROP_EXCESS_PREFIXES); + + result = run({"switch-limit-overload-protection-mode", "2"}); + EXPECT_THAT(result, HasSubstr("overload_protection_mode to: 2")); + EXPECT_EQ( + *config().switch_limit_config()->overload_protection_mode(), + bgp::thrift::OverloadProtectionMode::APPLY_GOLDEN_PREFIX_POLICY); + EXPECT_TRUE(sessionFileExists()); +} + +TEST_F(CmdConfigBgpGlobalTestFixture, overloadProtectionModeNameAccepted) { + auto result = + run({"switch-limit-overload-protection-mode", "DROP_EXCESS_PREFIXES"}); + // The success message prints the integer so the j2c round-trip is stable. + EXPECT_THAT(result, HasSubstr("overload_protection_mode to: 1")); + ASSERT_TRUE(config().switch_limit_config().has_value()); + EXPECT_EQ( + *config().switch_limit_config()->overload_protection_mode(), + bgp::thrift::OverloadProtectionMode::DROP_EXCESS_PREFIXES); + + result = run( + {"switch-limit-overload-protection-mode", "APPLY_GOLDEN_PREFIX_POLICY"}); + EXPECT_THAT(result, HasSubstr("overload_protection_mode to: 2")); + EXPECT_EQ( + *config().switch_limit_config()->overload_protection_mode(), + bgp::thrift::OverloadProtectionMode::APPLY_GOLDEN_PREFIX_POLICY); +} + +TEST_F(CmdConfigBgpGlobalTestFixture, overloadProtectionModeInvalidRejected) { + for (const auto& bad : {"0", "-5", "999", "fast"}) { + auto result = run({"switch-limit-overload-protection-mode", bad}); + EXPECT_THAT(result, HasSubstr("is not a valid mode")) << bad; + // The rejection lists every valid name. + EXPECT_THAT(result, HasSubstr("DROP_EXCESS_PREFIXES")) << bad; + EXPECT_THAT(result, HasSubstr("APPLY_GOLDEN_PREFIX_POLICY")) << bad; + } + // Validation happens before ensure(): a rejected value must not create the + // nested switch_limit_config. + EXPECT_FALSE(config().switch_limit_config().has_value()); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +TEST_F( + CmdConfigBgpGlobalTestFixture, + rejectedOverloadProtectionModeKeepsStagedValue) { + run({"switch-limit-overload-protection-mode", "2"}); + auto result = run({"switch-limit-overload-protection-mode", "999"}); + EXPECT_THAT(result, HasSubstr("is not a valid mode")); + ASSERT_TRUE(config().switch_limit_config().has_value()); + EXPECT_EQ( + *config().switch_limit_config()->overload_protection_mode(), + bgp::thrift::OverloadProtectionMode::APPLY_GOLDEN_PREFIX_POLICY); +} + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp index a4afe16a8f35c..9c3a68d779672 100644 --- a/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp @@ -18,6 +18,8 @@ * -> BgpConfig.graceful_restart_convergence_seconds * - rib-allocated-path-ids * -> BgpConfig.bgp_setting_config.enable_rib_allocated_path_id + * - switch-limit-overload-protection-mode + * -> BgpConfig.switch_limit_config.overload_protection_mode * * Requirements: * - The fboss2-dev binary under test (config subcommand tree). @@ -107,6 +109,47 @@ TEST_F(ConfigBgpGlobalTest, InvalidBoolValueRejected) { << "session file should not exist after rejected input"; } +TEST_F(ConfigBgpGlobalTest, RouterIdInvalidRejected) { + clearBgpSession(); + auto result = + runCli({"config", "protocol", "bgp", "global", "router-id", "not-an-ip"}); + EXPECT_THAT(result.stdout, HasSubstr("Invalid router-id address")); + EXPECT_FALSE(std::filesystem::exists(bgpSessionPath())) + << "session file should not exist after rejected input"; +} + +TEST_F(ConfigBgpGlobalTest, RouterIdV6Rejected) { + clearBgpSession(); + auto result = runCli( + {"config", "protocol", "bgp", "global", "router-id", "2001:db8::1"}); + EXPECT_THAT(result.stdout, HasSubstr("requires an IPv4 address")); + EXPECT_FALSE(std::filesystem::exists(bgpSessionPath())) + << "session file should not exist after rejected input"; +} + +TEST_F(ConfigBgpGlobalTest, OverloadProtectionModeOutOfRangeRejected) { + clearBgpSession(); + auto result = runCli( + {"config", + "protocol", + "bgp", + "global", + "switch-limit-overload-protection-mode", + "999"}); + EXPECT_THAT(result.stdout, HasSubstr("is not a valid mode")); + EXPECT_THAT(result.stdout, HasSubstr("APPLY_GOLDEN_PREFIX_POLICY")); + EXPECT_FALSE(std::filesystem::exists(bgpSessionPath())) + << "session file should not exist after rejected input"; +} + +TEST_F(ConfigBgpGlobalTest, SetOverloadProtectionModeAndCommit) { + auto config = setAndCommit("switch-limit-overload-protection-mode", "2"); + ASSERT_TRUE(config.count("switch_limit_config")); + ASSERT_TRUE(config["switch_limit_config"].count("overload_protection_mode")); + EXPECT_EQ( + config["switch_limit_config"]["overload_protection_mode"].asInt(), 2); +} + TEST_F(ConfigBgpGlobalTest, NegativeGracefulRestartTimeRejected) { clearBgpSession(); auto result = runCli( From 76a9436c72f903ed320bc8ba3a98ab7140a26a92 Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Fri, 7 Aug 2026 18:16:57 +0000 Subject: [PATCH 06/22] fboss2 config/delete protocol bgp neighbor commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `config protocol bgp neighbor [ ...]` and `delete protocol bgp neighbor ` — the first per-peer BGP config family, beneath the `protocol bgp` grouping node. - neighbor dispatcher keyed by peer address, writing bgp_config.BgpPeer through the typed ConfigSession. Attributes cover the peer identity (remote/local ASN, description, peer-tag, peer-group), policy bindings (ingress/egress policy), session tunables (timers, route limits, next-hop, add-path, RR client, and the rest of the documented set), and the sheet-documented attributes bgpd does not support, which are rejected with an explanatory message rather than staged. - delete removes the peer by address. - ConfigBgpTestBase restores the committed BGP config after every test, so a failed run cannot leave a device carrying test config; commit-path integration tests verify each value through bgpd's own getRunningConfig RPC rather than only the staged file. Co-Authored-By: Claude Fable 5 --- cmake/CliFboss2.cmake | 60 +- cmake/CliFboss2TestConfig.cmake | 2 + cmake/CliFboss2TestIntegrationTest.cmake | 1 + docs/docs/debugging/fboss_cli.md | 2 +- fboss/cli/fboss2/BUCK | 52 +- fboss/cli/fboss2/CmdListConfig.cpp | 302 +------ .../config/protocol/bgp/BgpCliAttrHandlers.h | 446 ++++++++++ .../config/protocol/bgp/BgpConfigSession.cpp | 328 +------ .../config/protocol/bgp/BgpConfigSession.h | 129 +-- .../config/protocol/bgp/BgpPeerAddr.h | 66 ++ .../neighbor/CmdConfigProtocolBgpNeighbor.cpp | 797 ++++++++++++++++++ .../neighbor/CmdConfigProtocolBgpNeighbor.h | 97 +++ .../bgp/peer/CmdConfigProtocolBgpPeer.cpp | 52 -- .../bgp/peer/CmdConfigProtocolBgpPeer.h | 42 - .../CmdConfigProtocolBgpPeerAdvertiseLbw.cpp | 60 -- .../CmdConfigProtocolBgpPeerAdvertiseLbw.h | 45 - .../CmdConfigProtocolBgpPeerDescription.cpp | 58 -- .../CmdConfigProtocolBgpPeerDescription.h | 45 - ...CmdConfigProtocolBgpPeerDisableIpv4Afi.cpp | 62 -- .../CmdConfigProtocolBgpPeerDisableIpv4Afi.h | 46 - .../CmdConfigProtocolBgpPeerEgressPolicy.cpp | 59 -- .../CmdConfigProtocolBgpPeerEgressPolicy.h | 45 - .../peer/CmdConfigProtocolBgpPeerHoldTime.cpp | 69 -- .../peer/CmdConfigProtocolBgpPeerHoldTime.h | 44 - .../CmdConfigProtocolBgpPeerIngressPolicy.cpp | 59 -- .../CmdConfigProtocolBgpPeerIngressPolicy.h | 45 - .../CmdConfigProtocolBgpPeerLinkBandwidth.cpp | 95 --- .../CmdConfigProtocolBgpPeerLinkBandwidth.h | 45 - .../CmdConfigProtocolBgpPeerLocalAddr.cpp | 59 -- .../peer/CmdConfigProtocolBgpPeerLocalAddr.h | 44 - .../CmdConfigProtocolBgpPeerMaxRoutes.cpp | 69 -- .../peer/CmdConfigProtocolBgpPeerMaxRoutes.h | 44 - .../peer/CmdConfigProtocolBgpPeerNextHop4.cpp | 59 -- .../peer/CmdConfigProtocolBgpPeerNextHop4.h | 44 - .../peer/CmdConfigProtocolBgpPeerNextHop6.cpp | 59 -- .../peer/CmdConfigProtocolBgpPeerNextHop6.h | 44 - .../CmdConfigProtocolBgpPeerNextHopSelf.cpp | 62 -- .../CmdConfigProtocolBgpPeerNextHopSelf.h | 45 - .../peer/CmdConfigProtocolBgpPeerPassive.cpp | 60 -- .../peer/CmdConfigProtocolBgpPeerPassive.h | 44 - .../CmdConfigProtocolBgpPeerPeerGroup.cpp | 56 -- .../peer/CmdConfigProtocolBgpPeerPeerGroup.h | 44 - .../peer/CmdConfigProtocolBgpPeerPeerId.cpp | 58 -- .../bgp/peer/CmdConfigProtocolBgpPeerPeerId.h | 44 - .../CmdConfigProtocolBgpPeerRemoteAsn.cpp | 69 -- .../peer/CmdConfigProtocolBgpPeerRemoteAsn.h | 44 - .../peer/CmdConfigProtocolBgpPeerRrClient.cpp | 62 -- .../peer/CmdConfigProtocolBgpPeerRrClient.h | 44 - .../peer/CmdConfigProtocolBgpPeerTimers.cpp | 47 -- .../bgp/peer/CmdConfigProtocolBgpPeerTimers.h | 40 - ...mdConfigProtocolBgpPeerTimersKeepalive.cpp | 70 -- .../CmdConfigProtocolBgpPeerTimersKeepalive.h | 46 - ...CmdConfigProtocolBgpPeerTimersOutDelay.cpp | 70 -- .../CmdConfigProtocolBgpPeerTimersOutDelay.h | 46 - ...otocolBgpPeerTimersWithdrawUnprogDelay.cpp | 70 -- ...ProtocolBgpPeerTimersWithdrawUnprogDelay.h | 47 -- .../bgp/peer/CmdConfigProtocolBgpPeerType.cpp | 58 -- .../bgp/peer/CmdConfigProtocolBgpPeerType.h | 43 - .../CmdConfigProtocolBgpPeerV4OverV6Nh.cpp | 60 -- .../peer/CmdConfigProtocolBgpPeerV4OverV6Nh.h | 44 - .../CmdConfigProtocolBgpPeerWarningLimit.cpp | 68 -- .../CmdConfigProtocolBgpPeerWarningLimit.h | 45 - .../CmdConfigProtocolBgpPeerWarningOnly.cpp | 71 -- .../CmdConfigProtocolBgpPeerWarningOnly.h | 45 - .../protocol/bgp/CmdDeleteProtocolBgp.cpp | 32 + .../protocol/bgp/CmdDeleteProtocolBgp.h | 41 + .../neighbor/CmdDeleteProtocolBgpNeighbor.cpp | 77 ++ .../neighbor/CmdDeleteProtocolBgpNeighbor.h | 61 ++ fboss/cli/fboss2/docs/BGP_CLI_TEST_PLAN.md | 21 +- fboss/cli/fboss2/test/config/BUCK | 2 + .../test/config/BgpConfigSessionTest.cpp | 260 +----- .../test/config/CmdConfigBgpNeighborTest.cpp | 540 ++++++++++++ .../test/config/CmdDeleteBgpNeighborTest.cpp | 128 +++ fboss/cli/fboss2/test/integration_test/BUCK | 6 + .../ConfigBgpNeighborTest.cpp | 153 ++++ .../test/integration_test/ConfigBgpTestBase.h | 122 ++- fboss/cli/fboss2/tools/bgp_json_to_cli.py | 270 +++--- .../tools/tests/test_bgp_json_to_cli.py | 233 +++-- 78 files changed, 2914 insertions(+), 4109 deletions(-) create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliAttrHandlers.h create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/BgpPeerAddr.h create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.cpp create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerAdvertiseLbw.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerAdvertiseLbw.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDescription.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDescription.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDisableIpv4Afi.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDisableIpv4Afi.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerEgressPolicy.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerEgressPolicy.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerHoldTime.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerHoldTime.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerIngressPolicy.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerIngressPolicy.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLinkBandwidth.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLinkBandwidth.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLocalAddr.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLocalAddr.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerMaxRoutes.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerMaxRoutes.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop4.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop4.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop6.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop6.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHopSelf.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHopSelf.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPassive.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPassive.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerGroup.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerGroup.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerId.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerId.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRemoteAsn.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRemoteAsn.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRrClient.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRrClient.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersKeepalive.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersKeepalive.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersOutDelay.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersOutDelay.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerType.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerType.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerV4OverV6Nh.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerV4OverV6Nh.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningLimit.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningLimit.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningOnly.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningOnly.h create mode 100644 fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.cpp create mode 100644 fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h create mode 100644 fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.cpp create mode 100644 fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h create mode 100644 fboss/cli/fboss2/test/config/CmdConfigBgpNeighborTest.cpp create mode 100644 fboss/cli/fboss2/test/config/CmdDeleteBgpNeighborTest.cpp create mode 100644 fboss/cli/fboss2/test/integration_test/ConfigBgpNeighborTest.cpp diff --git a/cmake/CliFboss2.cmake b/cmake/CliFboss2.cmake index 4d302785d03f2..d5bcb348a0762 100644 --- a/cmake/CliFboss2.cmake +++ b/cmake/CliFboss2.cmake @@ -938,13 +938,17 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/config/mac/aging_time/CmdConfigMacAgingTime.h fboss/cli/fboss2/commands/config/protocol/CmdConfigProtocol.cpp fboss/cli/fboss2/commands/config/protocol/CmdConfigProtocol.h + fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliAttrHandlers.h fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h + fboss/cli/fboss2/commands/config/protocol/bgp/BgpPeerAddr.h fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.cpp fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h fboss/cli/fboss2/commands/config/protocol/bgp/CmdConfigProtocolBgp.cpp fboss/cli/fboss2/commands/config/protocol/bgp/CmdConfigProtocolBgp.h fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h + fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.cpp + fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.cpp @@ -983,58 +987,6 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.h fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.cpp fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerAdvertiseLbw.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerAdvertiseLbw.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDescription.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDescription.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDisableIpv4Afi.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDisableIpv4Afi.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerEgressPolicy.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerEgressPolicy.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerHoldTime.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerHoldTime.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerIngressPolicy.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerIngressPolicy.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLinkBandwidth.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLinkBandwidth.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLocalAddr.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLocalAddr.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerMaxRoutes.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerMaxRoutes.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop4.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop4.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop6.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop6.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHopSelf.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHopSelf.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPassive.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPassive.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerGroup.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerGroup.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerId.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerId.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRemoteAsn.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRemoteAsn.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRrClient.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRrClient.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersKeepalive.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersKeepalive.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersOutDelay.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersOutDelay.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerType.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerType.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerV4OverV6Nh.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerV4OverV6Nh.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningLimit.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningLimit.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningOnly.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningOnly.h fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.cpp fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.h fboss/cli/fboss2/commands/config/ptp/transparent_clock/CmdConfigPtpTransparentClock.cpp @@ -1147,6 +1099,10 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/delete/interface/sflow/CmdDeleteInterfaceSflow.cpp fboss/cli/fboss2/commands/delete/interface/sflow/CmdDeleteInterfaceSflow.h fboss/cli/fboss2/commands/delete/protocol/CmdDeleteProtocol.cpp + fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.cpp + fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h + fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.cpp + fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h fboss/cli/fboss2/commands/delete/protocol/CmdDeleteProtocol.h fboss/cli/fboss2/commands/delete/protocol/static/CmdDeleteProtocolStatic.cpp fboss/cli/fboss2/commands/delete/protocol/static/CmdDeleteProtocolStatic.h diff --git a/cmake/CliFboss2TestConfig.cmake b/cmake/CliFboss2TestConfig.cmake index 050d4e264dfd2..fa42a48694e00 100644 --- a/cmake/CliFboss2TestConfig.cmake +++ b/cmake/CliFboss2TestConfig.cmake @@ -11,6 +11,7 @@ add_executable(fboss2_cmd_config_test fboss/cli/fboss2/test/config/CmdConfigArpTest.cpp fboss/cli/fboss2/test/config/CmdConfigBgpGlobalTest.cpp fboss/cli/fboss2/test/config/CmdConfigIcmpV4UnavailableSrcAddrTest.cpp + fboss/cli/fboss2/test/config/CmdConfigBgpNeighborTest.cpp fboss/cli/fboss2/test/config/CmdConfigCoppTest.cpp fboss/cli/fboss2/test/config/CmdConfigDhcpTest.cpp fboss/cli/fboss2/test/config/CmdConfigHostnameTest.cpp @@ -43,6 +44,7 @@ add_executable(fboss2_cmd_config_test fboss/cli/fboss2/test/config/CmdConfigVlanTest.cpp fboss/cli/fboss2/test/config/CmdDeleteAclRuleTest.cpp fboss/cli/fboss2/test/config/CmdDeleteArpTest.cpp + fboss/cli/fboss2/test/config/CmdDeleteBgpNeighborTest.cpp fboss/cli/fboss2/test/config/CmdDeleteConfigInterfaceTest.cpp fboss/cli/fboss2/test/config/CmdDeleteCoppQueueTest.cpp fboss/cli/fboss2/test/config/CmdDeleteCoppReasonTest.cpp diff --git a/cmake/CliFboss2TestIntegrationTest.cmake b/cmake/CliFboss2TestIntegrationTest.cmake index 930515a509fcf..84e2861766058 100644 --- a/cmake/CliFboss2TestIntegrationTest.cmake +++ b/cmake/CliFboss2TestIntegrationTest.cmake @@ -14,6 +14,7 @@ add_executable(fboss2_integration_test fboss/cli/fboss2/test/integration_test/ConfigSflowRateTest.cpp fboss/cli/fboss2/test/integration_test/ConfigSflowSampleDestTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp + fboss/cli/fboss2/test/integration_test/ConfigBgpNeighborTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp fboss/cli/fboss2/test/integration_test/ConfigConcurrentSessionsTest.cpp fboss/cli/fboss2/test/integration_test/ConfigHostnameTest.cpp diff --git a/docs/docs/debugging/fboss_cli.md b/docs/docs/debugging/fboss_cli.md index cc3e3212d05e0..53dcc7e3ddfc8 100644 --- a/docs/docs/debugging/fboss_cli.md +++ b/docs/docs/debugging/fboss_cli.md @@ -1247,7 +1247,7 @@ BGP is configured through the same session model, but the changes are staged for The BGP command families are: - **`config protocol bgp global [ ...]`**: process-wide settings such as `router-id`, `local-asn`, `hold-time`, `confed-asn`, `cluster-id`, `network6` (advertised networks), and `switch-limit` overload-protection settings. -- **`config protocol bgp neighbor [ ...]`**: per-neighbor settings such as `remote-asn`, `local-asn`, `peer-group`, `description`, `bind-addr address`, `next-hop4`/`next-hop6`/`next-hop-self`, `connect-mode `, `afi disable-ipv4-afi`/`disable-ipv6-afi`, `rr-client`, `confed-peer`, `type`, `ingress-policy`/`egress-policy`, `add-path send|receive`, `link-bandwidth`, `graceful-restart restart-time`, `timers hold-time|keepalive|out-delay|withdraw-unprog-delay`, and `max-route pre-filter|post-filter` limits. A bare `neighbor ` creates the neighbor. +- **`config protocol bgp neighbor [ ...]`**: per-neighbor settings such as `remote-asn`, `local-asn`, `peer-group`, `description`, `bind-addr address`, `next-hop4`/`next-hop6`/`next-hop-self`, `passive `, `afi disable-ipv4-afi`/`disable-ipv6-afi`, `rr-client`, `confed-peer`, `type`, `ingress-policy`/`egress-policy`, `add-path send|receive`, `link-bandwidth`, `graceful-restart restart-time`, `timers hold-time|keepalive|out-delay|withdraw-unprog-delay`, and `max-route pre-filter|post-filter` limits. A bare `neighbor ` creates the neighbor. - **`config protocol bgp peer-group [ ...]`**: the same attribute family applied to a peer group, which neighbors can then reference via `peer-group`. - **`config protocol bgp policy as-path-list [entry ] [ ...]`**: BGP routing policy objects. `as-path-list` entries support `asn-regexp`, `description`, and `match-logic `. More policy object types (community-list, prefix-list, routing-policy) will follow the same pattern. diff --git a/fboss/cli/fboss2/BUCK b/fboss/cli/fboss2/BUCK index be654f984cb11..4db1c8e7d5a41 100644 --- a/fboss/cli/fboss2/BUCK +++ b/fboss/cli/fboss2/BUCK @@ -1164,6 +1164,7 @@ cpp_library( "commands/config/protocol/bgp/BgpConfigSession.cpp", "commands/config/protocol/bgp/CmdConfigProtocolBgp.cpp", "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp", + "commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.cpp", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.cpp", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.cpp", @@ -1183,32 +1184,6 @@ cpp_library( "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.cpp", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.cpp", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerAdvertiseLbw.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDescription.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDisableIpv4Afi.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerEgressPolicy.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerHoldTime.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerIngressPolicy.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLinkBandwidth.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLocalAddr.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerMaxRoutes.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop4.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop6.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHopSelf.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPassive.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerGroup.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerId.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRemoteAsn.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRrClient.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersKeepalive.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersOutDelay.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerType.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerV4OverV6Nh.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningLimit.cpp", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningOnly.cpp", "commands/config/protocol/static/CmdConfigProtocolStatic.cpp", "commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.cpp", "commands/config/ptp/CmdConfigPtp.cpp", @@ -1264,6 +1239,8 @@ cpp_library( "commands/delete/interface/ipv6/ndp/CmdDeleteInterfaceIpv6Ndp.cpp", "commands/delete/interface/sflow/CmdDeleteInterfaceSflow.cpp", "commands/delete/protocol/CmdDeleteProtocol.cpp", + "commands/delete/protocol/bgp/CmdDeleteProtocolBgp.cpp", + "commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.cpp", "commands/delete/protocol/static/CmdDeleteProtocolStatic.cpp", "commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.cpp", "commands/delete/qos/CmdDeleteQos.cpp", @@ -1323,10 +1300,13 @@ cpp_library( "commands/config/mac/CmdConfigMac.h", "commands/config/mac/aging_time/CmdConfigMacAgingTime.h", "commands/config/protocol/CmdConfigProtocol.h", + "commands/config/protocol/bgp/BgpCliAttrHandlers.h", "commands/config/protocol/bgp/BgpCliValueParsers.h", + "commands/config/protocol/bgp/BgpPeerAddr.h", "commands/config/protocol/bgp/BgpConfigSession.h", "commands/config/protocol/bgp/CmdConfigProtocolBgp.h", "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h", + "commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.h", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.h", @@ -1342,24 +1322,6 @@ cpp_library( "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.h", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.h", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerAdvertiseLbw.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDescription.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDisableIpv4Afi.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerEgressPolicy.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerHoldTime.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerIngressPolicy.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLinkBandwidth.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLocalAddr.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerMaxRoutes.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop4.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop6.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHopSelf.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPassive.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerGroup.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRemoteAsn.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRrClient.h", - "commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerV4OverV6Nh.h", "commands/config/protocol/static/CmdConfigProtocolStatic.h", "commands/config/protocol/static/route/StaticRouteUtils.h", "commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.h", @@ -1416,6 +1378,8 @@ cpp_library( "commands/delete/interface/ipv6/ndp/CmdDeleteInterfaceIpv6Ndp.h", "commands/delete/interface/sflow/CmdDeleteInterfaceSflow.h", "commands/delete/protocol/CmdDeleteProtocol.h", + "commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h", + "commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h", "commands/delete/protocol/static/CmdDeleteProtocolStatic.h", "commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.h", "commands/delete/qos/CmdDeleteQos.h", diff --git a/fboss/cli/fboss2/CmdListConfig.cpp b/fboss/cli/fboss2/CmdListConfig.cpp index 96cb32c314f44..b141cc0924e01 100644 --- a/fboss/cli/fboss2/CmdListConfig.cpp +++ b/fboss/cli/fboss2/CmdListConfig.cpp @@ -39,6 +39,7 @@ #include "fboss/cli/fboss2/commands/config/protocol/CmdConfigProtocol.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/CmdConfigProtocolBgp.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.h" @@ -58,32 +59,6 @@ #include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerAdvertiseLbw.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDescription.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDisableIpv4Afi.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerEgressPolicy.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerHoldTime.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerIngressPolicy.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLinkBandwidth.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLocalAddr.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerMaxRoutes.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop4.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop6.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHopSelf.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPassive.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerGroup.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerId.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRemoteAsn.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRrClient.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersKeepalive.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersOutDelay.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerType.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerV4OverV6Nh.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningLimit.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningOnly.h" #include "fboss/cli/fboss2/commands/config/protocol/static/CmdConfigProtocolStatic.h" #include "fboss/cli/fboss2/commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.h" #include "fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.h" @@ -136,6 +111,8 @@ #include "fboss/cli/fboss2/commands/delete/interface/ipv6/ndp/CmdDeleteInterfaceIpv6Ndp.h" #include "fboss/cli/fboss2/commands/delete/interface/sflow/CmdDeleteInterfaceSflow.h" #include "fboss/cli/fboss2/commands/delete/protocol/CmdDeleteProtocol.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h" #include "fboss/cli/fboss2/commands/delete/protocol/static/CmdDeleteProtocolStatic.h" #include "fboss/cli/fboss2/commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.h" #include "fboss/cli/fboss2/commands/delete/qos/CmdDeleteQos.h" @@ -555,213 +532,12 @@ const CommandTree& kConfigCommandTree() { }, }, { - "peer", - "Configure BGP peer", - commandHandler, - argRegistrar, - { - { - "peer-group", - "Set peer group", - commandHandler< - CmdConfigProtocolBgpPeerPeerGroup>, - argRegistrar< - CmdConfigProtocolBgpPeerPeerGroupTraits>, - }, - { - "description", - "Set peer description", - commandHandler< - CmdConfigProtocolBgpPeerDescription>, - argRegistrar< - CmdConfigProtocolBgpPeerDescriptionTraits>, - }, - { - "remote-asn", - "Set remote AS number", - commandHandler< - CmdConfigProtocolBgpPeerRemoteAsn>, - argRegistrar< - CmdConfigProtocolBgpPeerRemoteAsnTraits>, - }, - { - "local-addr", - "Set local address", - commandHandler< - CmdConfigProtocolBgpPeerLocalAddr>, - argRegistrar< - CmdConfigProtocolBgpPeerLocalAddrTraits>, - }, - { - "next-hop4", - "Set IPv4 next-hop", - commandHandler< - CmdConfigProtocolBgpPeerNextHop4>, - argRegistrar< - CmdConfigProtocolBgpPeerNextHop4Traits>, - }, - { - "next-hop6", - "Set IPv6 next-hop", - commandHandler< - CmdConfigProtocolBgpPeerNextHop6>, - argRegistrar< - CmdConfigProtocolBgpPeerNextHop6Traits>, - }, - { - "next-hop-self", - "Enable/disable next-hop-self", - commandHandler< - CmdConfigProtocolBgpPeerNextHopSelf>, - argRegistrar< - CmdConfigProtocolBgpPeerNextHopSelfTraits>, - }, - { - "disable-ipv4-afi", - "Enable/disable IPv4 AFI", - commandHandler< - CmdConfigProtocolBgpPeerDisableIpv4Afi>, - argRegistrar< - CmdConfigProtocolBgpPeerDisableIpv4AfiTraits>, - }, - { - "v4-over-v6-nh", - "Enable/disable v4-over-v6 next-hop", - commandHandler< - CmdConfigProtocolBgpPeerV4OverV6Nh>, - argRegistrar< - CmdConfigProtocolBgpPeerV4OverV6NhTraits>, - }, - { - "rr-client", - "Enable/disable route reflector client", - commandHandler< - CmdConfigProtocolBgpPeerRrClient>, - argRegistrar< - CmdConfigProtocolBgpPeerRrClientTraits>, - }, - { - "passive", - "Enable/disable passive mode", - commandHandler< - CmdConfigProtocolBgpPeerPassive>, - argRegistrar< - CmdConfigProtocolBgpPeerPassiveTraits>, - }, - { - "type", - "Set peer type", - commandHandler, - argRegistrar< - CmdConfigProtocolBgpPeerTypeTraits>, - }, - { - "ingress-policy", - "Set ingress policy", - commandHandler< - CmdConfigProtocolBgpPeerIngressPolicy>, - argRegistrar< - CmdConfigProtocolBgpPeerIngressPolicyTraits>, - }, - { - "egress-policy", - "Set egress policy", - commandHandler< - CmdConfigProtocolBgpPeerEgressPolicy>, - argRegistrar< - CmdConfigProtocolBgpPeerEgressPolicyTraits>, - }, - { - "max-routes", - "Set maximum routes", - commandHandler< - CmdConfigProtocolBgpPeerMaxRoutes>, - argRegistrar< - CmdConfigProtocolBgpPeerMaxRoutesTraits>, - }, - { - "peer-id", - "Set peer identifier", - commandHandler< - CmdConfigProtocolBgpPeerPeerId>, - argRegistrar< - CmdConfigProtocolBgpPeerPeerIdTraits>, - }, - { - "advertise-lbw", - "Enable/disable link bandwidth advertisement", - commandHandler< - CmdConfigProtocolBgpPeerAdvertiseLbw>, - argRegistrar< - CmdConfigProtocolBgpPeerAdvertiseLbwTraits>, - }, - { - "link-bandwidth", - "Set link bandwidth", - commandHandler< - CmdConfigProtocolBgpPeerLinkBandwidth>, - argRegistrar< - CmdConfigProtocolBgpPeerLinkBandwidthTraits>, - }, - { - "hold-time", - "Set hold time", - commandHandler< - CmdConfigProtocolBgpPeerHoldTime>, - argRegistrar< - CmdConfigProtocolBgpPeerHoldTimeTraits>, - }, - { - "warning-only", - "Enable/disable warning-only mode", - commandHandler< - CmdConfigProtocolBgpPeerWarningOnly>, - argRegistrar< - CmdConfigProtocolBgpPeerWarningOnlyTraits>, - }, - { - "warning-limit", - "Set warning limit percentage", - commandHandler< - CmdConfigProtocolBgpPeerWarningLimit>, - argRegistrar< - CmdConfigProtocolBgpPeerWarningLimitTraits>, - }, - { - "timers", - "Configure BGP timers", - commandHandler< - CmdConfigProtocolBgpPeerTimers>, - argRegistrar< - CmdConfigProtocolBgpPeerTimersTraits>, - { - { - "keepalive", - "Set keepalive interval", - commandHandler< - CmdConfigProtocolBgpPeerTimersKeepalive>, - argRegistrar< - CmdConfigProtocolBgpPeerTimersKeepaliveTraits>, - }, - { - "out-delay", - "Set output delay", - commandHandler< - CmdConfigProtocolBgpPeerTimersOutDelay>, - argRegistrar< - CmdConfigProtocolBgpPeerTimersOutDelayTraits>, - }, - { - "withdraw-unprog-delay", - "Set withdraw unprogrammed delay", - commandHandler< - CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay>, - argRegistrar< - CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelayTraits>, - }, - }, - }, - }, + "neighbor", + "Configure BGP neighbor: " + "[ ...]; `neighbor --help` " + "lists the attributes", + commandHandler, + argRegistrar, }, }, }, @@ -1089,31 +865,43 @@ const CommandTree& kConfigCommandTree() { commandHandler, argTypeHandler, {{ - "static", - "Delete static routing configuration", - commandHandler, - argTypeHandler, - {{ - "ip", - "Delete IPv4 static routes", - {{ - "route", - "Delete IPv4 static route", - commandHandler, - argRegistrar, - }}, - }, - { - "ipv6", - "Delete IPv6 static routes", - {{ - "route", - "Delete IPv6 static route", - commandHandler, - argRegistrar, - }}, + "bgp", + "Delete BGP configuration objects", + commandHandler, + argTypeHandler, + {{ + "neighbor", + "Delete a BGP neighbor: ", + commandHandler, + argRegistrar, }}, - }}, + }, + { + "static", + "Delete static routing configuration", + commandHandler, + argTypeHandler, + {{ + "ip", + "Delete IPv4 static routes", + {{ + "route", + "Delete IPv4 static route", + commandHandler, + argRegistrar, + }}, + }, + { + "ipv6", + "Delete IPv6 static routes", + {{ + "route", + "Delete IPv6 static route", + commandHandler, + argRegistrar, + }}, + }}, + }}, }, { diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliAttrHandlers.h b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliAttrHandlers.h new file mode 100644 index 0000000000000..c1195994f47a1 --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliAttrHandlers.h @@ -0,0 +1,446 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h" +#include "fmt/format.h" +#include "thrift/lib/cpp2/gen/module_types_h.h" + +/** + * Generic, target-typed per-attribute handler factories shared by every + * `config protocol bgp ...` dispatcher. + * + * Each factory owns one value SHAPE (a bool, a bounded int, a 4-byte ASN, an + * IP address, a seconds-valued timer, ...): it validates the CLI tokens, + * produces the usage and rejection text for that shape, and on success calls + * the caller-supplied setter with an already-parsed value. They are templated + * on the target type T so the same logic mutates any BGP thrift object + * (BgpPeer, PeerGroup, AsPathList, PrefixListEntry, BgpPolicyTerm, ...). + * + * A dispatcher's registry is therefore one line per attribute -- key, shape, + * setter -- with no handler bodies inline. If an attribute seems to need a + * hand-written handler, that means its value shape has no factory yet, and the + * fix is to add the factory here. + */ +namespace facebook::fboss::bgpcli { + +using Tokens = std::vector; + +// A per-attribute handler mutating a typed thrift target T in place. +template +using AttrHandler = std::function; + +// A single-token string value (names, regex patterns, policy names). +template +AttrHandler stringAttr( + std::string_view name, + std::string_view valueName, + std::function set) { + return [name, valueName, set = std::move(set)]( + T& target, const Tokens& values) -> Result { + if (values.size() != 1) { + return err(fmt::format("Error: {} requires <{}>", name, valueName)); + } + set(target, values[0]); + return ok(fmt::format("Successfully set {} to: {}", name, values[0])); + }; +} + +// A free-text string value that may span multiple CLI tokens (descriptions); +// the tokens are re-joined with single spaces. +template +AttrHandler joinedStringAttr( + std::string_view name, + std::function set) { + return + [name, set = std::move(set)](T& target, const Tokens& values) -> Result { + if (values.empty()) { + return err(fmt::format("Error: {} requires ", name)); + } + std::string joined = values[0]; + for (size_t i = 1; i < values.size(); ++i) { + joined += " " + values[i]; + } + set(target, joined); + return ok(fmt::format("Successfully set {} to: {}", name, joined)); + }; +} + +// A true/false value (accepts the parseBool spellings). +template +AttrHandler boolAttr( + std::string_view name, + std::function set) { + return + [name, set = std::move(set)](T& target, const Tokens& values) -> Result { + if (values.size() != 1) { + return err(fmt::format("Error: {} requires ", name)); + } + auto enable = parseBool(values[0]); + if (!enable) { + return err( + fmt::format( + "Error: Invalid {} value '{}'; expected true or false", + name, + values[0])); + } + set(target, *enable); + return ok( + fmt::format( + "Successfully {} {}", *enable ? "enabled" : "disabled", name)); + }; +} + +// A bounded integer value in [minValue, maxValue]. `valueDesc` documents the +// accepted range in both the usage and the rejection message; it is taken by +// value for the same dangling-temporary reason as enumAttr's. +template +AttrHandler intAttr( + std::string_view name, + std::string valueDesc, + int32_t minValue, + int32_t maxValue, + std::function set) { + return [name, + valueDesc = std::move(valueDesc), + minValue, + maxValue, + set = std::move(set)](T& target, const Tokens& values) -> Result { + if (values.size() != 1) { + return err(fmt::format("Error: {} requires <{}>", name, valueDesc)); + } + auto parsed = parseInt(values[0]); + if (!parsed || *parsed < minValue || *parsed > maxValue) { + return err( + fmt::format( + "Error: Invalid {} value '{}'; expected {}", + name, + values[0], + valueDesc)); + } + set(target, static_cast(*parsed)); + return ok(fmt::format("Successfully set {} to: {}", name, values[0])); + }; +} + +// A non-negative integer value in [0, maxValue], for the thrift fields that +// hold an unsigned quantity in a signed i64 (local-pref, med, ...). Unlike +// intAttr the accepted range is DERIVED from maxValue for both the usage and +// the rejection message, so the two cannot drift apart. The setter takes +// int64_t to match those fields directly, with no cast at the call site. +template +AttrHandler uintAttr( + std::string_view name, + int64_t maxValue, + std::function set) { + return [name, maxValue, set = std::move(set)]( + T& target, const Tokens& values) -> Result { + auto parsed = values.size() == 1 ? parseInt(values[0]) + : std::optional(); + if (!parsed || *parsed < 0 || *parsed > maxValue) { + return err(fmt::format("Error: {} requires <0-{}>", name, maxValue)); + } + set(target, *parsed); + return ok(fmt::format("Successfully set {} to: {}", name, values[0])); + }; +} + +// A 4-byte ASN value (RFC 6793). The thrift fields are i64, so an unchecked +// parse would let an out-of-range ASN wrap or exceed the protocol range. +template +AttrHandler asnAttr( + std::string_view name, + std::function set) { + return + [name, set = std::move(set)](T& target, const Tokens& values) -> Result { + if (values.size() != 1) { + return err(fmt::format("Error: {} requires ", name)); + } + auto asn = parseAsn4Byte(values[0]); + if (!asn) { + return err( + fmt::format( + "Error: Invalid {} value '{}'; expected an unsigned 4-byte " + "ASN", + name, + values[0])); + } + set(target, *asn); + return ok(fmt::format("Successfully set {} to: {}", name, *asn)); + }; +} + +// A validated IP address value, optionally restricted to one family, stored +// normalized. +template +AttrHandler ipAttr( + std::string_view name, + std::function set, + std::optional requireV6 = std::nullopt) { + return [name, set = std::move(set), requireV6]( + T& target, const Tokens& values) -> Result { + if (values.size() != 1) { + return err(fmt::format("Error: {} requires ", name)); + } + auto addr = folly::IPAddress::tryFromString(values[0]); + if (!addr.hasValue()) { + return err( + fmt::format("Error: Invalid {} address '{}'", name, values[0])); + } + if (requireV6.has_value() && addr->isV6() != *requireV6) { + return err( + fmt::format( + "Error: {} requires an IPv{} address, got '{}'", + name, + *requireV6 ? 6 : 4, + values[0])); + } + set(target, addr->str()); + return ok(fmt::format("Successfully set {} to: {}", name, addr->str())); + }; +} + +// The protocol range of a second-valued timer: 0 (disabled) is always +// accepted, any other value must lie in [min, max]. The default is "anything +// non-negative that fits in int32"; callers narrow it where the wire format or +// the daemon does (a 16-bit BGP hold time, a 12-bit graceful-restart time). +struct SecondsRange { + int32_t min = 0; + int32_t max = std::numeric_limits::max(); +}; + +// A second-valued timer: 0 or a value within `range`. +template +AttrHandler secondsAttr( + std::string_view name, + std::function set, + SecondsRange range = {}) { + return [name, set = std::move(set), range]( + T& target, const Tokens& values) -> Result { + if (values.size() != 1) { + return err(fmt::format("Error: {} requires ", name)); + } + auto seconds = parseNonNegInt32(values[0]); + if (!seconds) { + return err( + fmt::format( + "Error: {} must be a non-negative integer that fits in " + "int32, got '{}'", + name, + values[0])); + } + if (*seconds != 0 && (*seconds < range.min || *seconds > range.max)) { + return err( + fmt::format( + "Error: {} must be 0 or between {} and {} seconds, got {}", + name, + range.min, + range.max, + *seconds)); + } + set(target, *seconds); + return ok( + fmt::format("Successfully set {} to: {} seconds", name, *seconds)); + }; +} + +// A non-negative int64 route-count value (RouteLimit fields). +template +AttrHandler routeCountAttr( + std::string_view name, + std::function set) { + return + [name, set = std::move(set)](T& target, const Tokens& values) -> Result { + if (values.size() != 1) { + return err(fmt::format("Error: {} requires ", name)); + } + auto limit = parseNonNegInt64(values[0]); + if (!limit) { + return err( + fmt::format( + "Error: {} must be a non-negative integer, got '{}'", + name, + values[0])); + } + set(target, *limit); + return ok(fmt::format("Successfully set {} to: {}", name, *limit)); + }; +} + +// An attribute the CLI must refuse: either the thrift has no field for it +// (persisting it would stage dead config the daemon ignores) or the knob is +// not settable at this level. The reason is surfaced to the user instead. +template +AttrHandler rejectedAttr(std::string_view name, std::string_view reason) { + return [name, reason](T& /* target */, const Tokens& /* values */) -> Result { + return err(fmt::format("Error: {} is not supported: {}", name, reason)); + }; +} + +// The spelling bgpd's link-bandwidth parser accepts (parseLinkBandwidthBps in +// its policy utils): a decimal number with at most one '.', an optional +// case-insensitive K/M/G multiplier as the last character, or the literal +// "auto" (bgpd derives the value from the port speed). nullopt if bgpd would +// reject it. +inline std::optional parseLinkBandwidth(const std::string& raw) { + std::string v = folly::trimWhitespace(raw).str(); + if (v == "auto") { + return v; + } + if (std::count(v.begin(), v.end(), '.') > 1) { + return std::nullopt; + } + std::string number = v; + const size_t suffixAt = v.find_first_not_of(".0123456789"); + if (suffixAt != std::string::npos) { + const char suffix = + static_cast(std::toupper(static_cast(v.back()))); + if (suffixAt != v.size() - 1 || + (suffix != 'K' && suffix != 'M' && suffix != 'G')) { + return std::nullopt; + } + number.pop_back(); + } + if (number.empty() || number == ".") { + return std::nullopt; + } + try { + std::stod(number); + } catch (const std::exception&) { + return std::nullopt; + } + return v; +} + +// A link-bandwidth value in the spelling bgpd accepts (see +// parseLinkBandwidth). The thrift field is a string that bgpd re-parses, so +// the CLI validates with the same rules rather than a stricter subset -- an +// existing config's "1.5G", "150g" or "auto" must round-trip through the CLI. +template +AttrHandler bitRateAttr( + std::string_view name, + std::function set) { + return + [name, set = std::move(set)](T& target, const Tokens& values) -> Result { + if (values.size() != 1) { + return err( + fmt::format( + "Error: {} requires [K|M|G] or auto", name)); + } + auto v = parseLinkBandwidth(values[0]); + if (!v) { + return err( + fmt::format( + "Error: Invalid {} value '{}'; expected a number with an " + "optional K/M/G suffix (e.g. 100G, 1.5G) or auto", + name, + values[0])); + } + set(target, *v); + return ok(fmt::format("Successfully set {} to: {}", name, *v)); + }; +} + +// A value drawn from a fixed set of names, mapped to an enum by `lookup` +// (returns std::nullopt for an unrecognized name). `valueDesc` documents the +// accepted names in both the usage and the rejection message; it is taken by +// value because call sites build it with fmt::format, and a string_view +// capture would dangle once that temporary dies. +template +AttrHandler enumAttr( + std::string_view name, + std::string valueDesc, + std::function(const std::string&)> lookup, + std::function set) { + return [name, + valueDesc = std::move(valueDesc), + lookup = std::move(lookup), + set = std::move(set)](T& target, const Tokens& values) -> Result { + if (values.size() != 1) { + return err(fmt::format("Error: {} requires <{}>", name, valueDesc)); + } + auto parsed = lookup(values[0]); + if (!parsed) { + return err( + fmt::format( + "Error: Invalid {} value '{}'; expected {}", + name, + values[0], + valueDesc)); + } + set(target, *parsed); + return ok(fmt::format("Successfully set {} to: {}", name, values[0])); + }; +} + +// A thrift enum attribute identified by its generated metadata rather than a +// hand-written lookup: accepts the enum value name (e.g. BEST_PATH), the +// integer value, or true/false as aliases for 1/0 (the per-attribute commands +// this replaced took booleans, so existing usage keeps working). Prefer +// enumAttr when only a documented subset of the enum is valid; use this when +// the whole enum is. +template +AttrHandler thriftEnumAttr( + std::string_view name, + std::function set) { + return [name, set = std::move(set)]( + T& target, const Tokens& values) -> Result { + if (values.size() != 1) { + return err(fmt::format("Error: {} requires ", name)); + } + EnumT mode; + bool valid = false; + if (auto parsed = parseInt(values[0])) { + mode = static_cast(*parsed); + valid = apache::thrift::TEnumTraits::findName(mode) != nullptr; + } else if (auto enable = parseBool(values[0])) { + mode = static_cast(*enable ? 1 : 0); + valid = true; + } else { + valid = apache::thrift::TEnumTraits::findValue(values[0], &mode); + } + if (!valid) { + std::string names; + for (auto n : apache::thrift::TEnumTraits::names) { + names += names.empty() ? "" : ", "; + names += std::string(n); + } + return err( + fmt::format( + "Error: {} value '{}' is not a valid mode; expected one of: " + "{}", + name, + values[0], + names)); + } + set(target, mode); + return ok( + fmt::format( + "Successfully set {} to: {}", name, static_cast(mode))); + }; +} + +} // namespace facebook::fboss::bgpcli diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.cpp index 91e12b6195302..637d5aa16337f 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.cpp +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.cpp @@ -13,9 +13,16 @@ #include #include #include +#include #include +#include #include #include +#include +#include +#include +#include +#include "folly/json/dynamic.h" namespace fs = std::filesystem; @@ -42,6 +49,7 @@ folly::dynamic getDefaultBgpConfig() { } // namespace BgpConfigSession::BgpConfigSession() { + // NOLINTNEXTLINE(concurrency-mt-unsafe): single-threaded CLI const char* homeDir = std::getenv("HOME"); if (homeDir == nullptr) { throw std::runtime_error("HOME environment variable not set"); @@ -56,6 +64,7 @@ BgpConfigSession::BgpConfigSession() { bgpConfig_ = getDefaultBgpConfig(); // Check for stub mode via environment variable + // NOLINTNEXTLINE(concurrency-mt-unsafe): single-threaded CLI const char* stubEnv = std::getenv("FBOSS_BGP_STUB_MODE"); if (stubEnv != nullptr && std::string(stubEnv) == "1") { stubMode_ = true; @@ -448,284 +457,6 @@ std::optional BgpConfigSession::getUcmpWidth() const { return std::nullopt; } -// ==================== Peer Configuration ==================== - -folly::dynamic& BgpConfigSession::findOrCreatePeer( - const std::string& peerAddr) { - ensureConfigLoaded(); - auto& peers = bgpConfig_["peers"]; - - // Find existing peer - for (auto& peer : peers) { - if (peer["peer_addr"].asString() == peerAddr) { - return peer; - } - } - - // Create new peer with required fields - folly::dynamic newPeer = folly::dynamic::object("peer_addr", peerAddr)( - "local_addr", "")("next_hop4", "")("next_hop6", ""); - - peers.push_back(std::move(newPeer)); - return peers[peers.size() - 1]; -} - -void BgpConfigSession::createPeer(const std::string& peerAddr) { - findOrCreatePeer(peerAddr); -} - -void BgpConfigSession::setPeerRemoteAsn( - const std::string& peerAddr, - uint64_t remoteAsn) { - findOrCreatePeer(peerAddr)["remote_as_4_byte"] = - static_cast(remoteAsn); -} - -void BgpConfigSession::setPeerLocalAsn( - const std::string& peerAddr, - uint64_t localAsn) { - findOrCreatePeer(peerAddr)["local_as"] = static_cast(localAsn); -} - -void BgpConfigSession::setPeerDescription( - const std::string& peerAddr, - const std::string& description) { - findOrCreatePeer(peerAddr)["description"] = description; -} - -void BgpConfigSession::setPeerLocalAddr( - const std::string& peerAddr, - const std::string& localAddr) { - findOrCreatePeer(peerAddr)["local_addr"] = localAddr; -} - -void BgpConfigSession::setPeerNextHop4( - const std::string& peerAddr, - const std::string& nextHop4) { - findOrCreatePeer(peerAddr)["next_hop4"] = nextHop4; -} - -void BgpConfigSession::setPeerNextHop6( - const std::string& peerAddr, - const std::string& nextHop6) { - findOrCreatePeer(peerAddr)["next_hop6"] = nextHop6; -} - -void BgpConfigSession::setPeerGroupName( - const std::string& peerAddr, - const std::string& peerGroupName) { - findOrCreatePeer(peerAddr)["peer_group_name"] = peerGroupName; -} - -void BgpConfigSession::setPeerPassive( - const std::string& peerAddr, - bool isPassive) { - findOrCreatePeer(peerAddr)["is_passive"] = isPassive; -} - -void BgpConfigSession::setPeerRrClient( - const std::string& peerAddr, - bool isRrClient) { - findOrCreatePeer(peerAddr)["is_rr_client"] = isRrClient; -} - -void BgpConfigSession::setPeerNextHopSelf( - const std::string& peerAddr, - bool nextHopSelf) { - findOrCreatePeer(peerAddr)["next_hop_self"] = nextHopSelf; -} - -void BgpConfigSession::setPeerConfedPeer( - const std::string& peerAddr, - bool isConfedPeer) { - findOrCreatePeer(peerAddr)["is_confed_peer"] = isConfedPeer; -} - -void BgpConfigSession::setPeerPort(const std::string& peerAddr, int32_t port) { - findOrCreatePeer(peerAddr)["peer_port"] = port; -} - -void BgpConfigSession::setPeerLocalPort( - const std::string& peerAddr, - int32_t port) { - findOrCreatePeer(peerAddr)["local_port"] = port; -} - -void BgpConfigSession::setPeerConnectMode( - const std::string& peerAddr, - const std::string& connectMode) { - findOrCreatePeer(peerAddr)["connect_mode"] = connectMode; -} - -// Peer timers helper -folly::dynamic& BgpConfigSession::ensurePeerTimers( - const std::string& peerAddr) { - auto& peer = findOrCreatePeer(peerAddr); - if (!peer.count("bgp_peer_timers")) { - peer["bgp_peer_timers"] = folly::dynamic::object; - } - return peer["bgp_peer_timers"]; -} - -void BgpConfigSession::setPeerHoldTime( - const std::string& peerAddr, - int32_t holdTime) { - ensurePeerTimers(peerAddr)["hold_time_seconds"] = holdTime; -} - -void BgpConfigSession::setPeerKeepalive( - const std::string& peerAddr, - int32_t keepalive) { - ensurePeerTimers(peerAddr)["keep_alive_seconds"] = keepalive; -} - -void BgpConfigSession::setPeerOutDelay( - const std::string& peerAddr, - int32_t outDelay) { - ensurePeerTimers(peerAddr)["out_delay_seconds"] = outDelay; -} - -void BgpConfigSession::setPeerGracefulRestartTime( - const std::string& peerAddr, - int32_t seconds) { - findOrCreatePeer(peerAddr)["graceful_restart_seconds"] = seconds; -} - -void BgpConfigSession::setPeerStatefulHa( - const std::string& peerAddr, - bool enable) { - findOrCreatePeer(peerAddr)["enable_stateful_ha"] = enable; -} - -// Peer AFI/SAFI -void BgpConfigSession::setPeerDisableIpv4Afi( - const std::string& peerAddr, - bool disable) { - findOrCreatePeer(peerAddr)["disable_ipv4_afi"] = disable; -} - -void BgpConfigSession::setPeerDisableIpv6Afi( - const std::string& peerAddr, - bool disable) { - findOrCreatePeer(peerAddr)["disable_ipv6_afi"] = disable; -} - -void BgpConfigSession::setPeerIpv4LabeledUnicast( - const std::string& peerAddr, - bool enable) { - findOrCreatePeer(peerAddr)["is_afi_ipv4_lu_configured"] = enable; -} - -void BgpConfigSession::setPeerIpv6LabeledUnicast( - const std::string& peerAddr, - bool enable) { - findOrCreatePeer(peerAddr)["is_afi_ipv6_lu_configured"] = enable; -} - -void BgpConfigSession::setPeerIpv4OverIpv6Nh( - const std::string& peerAddr, - bool enable) { - findOrCreatePeer(peerAddr)["v4_over_v6_nexthop"] = enable; -} - -// Peer add-path helper -folly::dynamic& BgpConfigSession::ensurePeerAddPath( - const std::string& peerAddr) { - auto& peer = findOrCreatePeer(peerAddr); - if (!peer.count("add_path")) { - peer["add_path"] = folly::dynamic::object; - } - return peer["add_path"]; -} - -void BgpConfigSession::setPeerAddPathSend( - const std::string& peerAddr, - bool enable) { - ensurePeerAddPath(peerAddr)["send"] = enable; -} - -void BgpConfigSession::setPeerAddPathReceive( - const std::string& peerAddr, - bool enable) { - ensurePeerAddPath(peerAddr)["receive"] = enable; -} - -// Peer AS-path settings -void BgpConfigSession::setPeerRemovePrivateAs( - const std::string& peerAddr, - bool remove) { - findOrCreatePeer(peerAddr)["remove_private_as"] = remove; -} - -void BgpConfigSession::setPeerEnforceFirstAs( - const std::string& peerAddr, - bool enforce) { - findOrCreatePeer(peerAddr)["enforce_first_as"] = enforce; -} - -// Peer policies -void BgpConfigSession::setPeerIngressPolicy( - const std::string& peerAddr, - const std::string& policy) { - findOrCreatePeer(peerAddr)["ingress_policy_name"] = policy; -} - -void BgpConfigSession::setPeerEgressPolicy( - const std::string& peerAddr, - const std::string& policy) { - findOrCreatePeer(peerAddr)["egress_policy_name"] = policy; -} - -// Peer route limits helpers -folly::dynamic& BgpConfigSession::ensurePeerPreFilter( - const std::string& peerAddr) { - auto& peer = findOrCreatePeer(peerAddr); - if (!peer.count("pre_filter")) { - peer["pre_filter"] = folly::dynamic::object; - } - return peer["pre_filter"]; -} - -folly::dynamic& BgpConfigSession::ensurePeerPostFilter( - const std::string& peerAddr) { - auto& peer = findOrCreatePeer(peerAddr); - if (!peer.count("post_filter")) { - peer["post_filter"] = folly::dynamic::object; - } - return peer["post_filter"]; -} - -void BgpConfigSession::setPeerPreFilterMaxRoutes( - const std::string& peerAddr, - int64_t maxRoutes) { - ensurePeerPreFilter(peerAddr)["max_routes"] = maxRoutes; -} - -void BgpConfigSession::setPeerPostFilterMaxRoutes( - const std::string& peerAddr, - int64_t maxRoutes) { - ensurePeerPostFilter(peerAddr)["max_routes"] = maxRoutes; -} - -// Peer link bandwidth -void BgpConfigSession::setPeerAdvertiseLbw( - const std::string& peerAddr, - bool advertise) { - findOrCreatePeer(peerAddr)["advertise_link_bandwidth"] = advertise; -} - -void BgpConfigSession::setPeerReceiveLbw( - const std::string& peerAddr, - bool receive) { - findOrCreatePeer(peerAddr)["receive_link_bandwidth"] = receive; -} - -void BgpConfigSession::setPeerLbwValue( - const std::string& peerAddr, - int64_t bps) { - findOrCreatePeer(peerAddr)["link_bandwidth_bps"] = bps; -} - // ==================== Peer Group Configuration ==================== folly::dynamic& BgpConfigSession::findOrCreatePeerGroup( @@ -1024,47 +755,6 @@ void BgpConfigSession::setPeerGroupWithdrawUnprogDelay( ensurePeerGroupTimers(groupName)["withdraw_unprog_delay_seconds"] = seconds; } -// ==================== Peer Identification (RSW compatibility) -// ==================== - -void BgpConfigSession::setPeerPeerId( - const std::string& peerAddr, - const std::string& peerId) { - findOrCreatePeer(peerAddr)["peer_id"] = peerId; -} - -void BgpConfigSession::setPeerType( - const std::string& peerAddr, - const std::string& type) { - findOrCreatePeer(peerAddr)["type"] = type; -} - -void BgpConfigSession::setPeerPeerTag( - const std::string& peerAddr, - const std::string& peerTag) { - findOrCreatePeer(peerAddr)["peer_tag"] = peerTag; -} - -// Peer pre_filter warning settings -void BgpConfigSession::setPeerPreFilterWarningLimit( - const std::string& peerAddr, - int64_t limit) { - ensurePeerPreFilter(peerAddr)["warning_limit"] = limit; -} - -void BgpConfigSession::setPeerPreFilterWarningOnly( - const std::string& peerAddr, - bool warningOnly) { - ensurePeerPreFilter(peerAddr)["warning_only"] = warningOnly; -} - -// Peer timer: withdraw_unprog_delay_seconds -void BgpConfigSession::setPeerWithdrawUnprogDelay( - const std::string& peerAddr, - int32_t seconds) { - ensurePeerTimers(peerAddr)["withdraw_unprog_delay_seconds"] = seconds; -} - // ==================== Networks Configuration ==================== void BgpConfigSession::addNetwork6( diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h index 449d629027ed5..1ff564e7f70ad 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h @@ -11,6 +11,7 @@ #pragma once #include +#include #include #include @@ -177,125 +178,6 @@ class BgpConfigSession { void setUcmpWidth(int32_t width); std::optional getUcmpWidth() const; - // ==================== Peer Configuration (Level: P) ==================== - // These map to BgpPeer thrift struct fields - - // Create/get peer by peer_addr - void createPeer(const std::string& peerAddr); - - // remote_as_4_byte: i64 - Peer's ASN - void setPeerRemoteAsn(const std::string& peerAddr, uint64_t remoteAsn); - - // local_as: i64 - Local ASN override for this peer - void setPeerLocalAsn(const std::string& peerAddr, uint64_t localAsn); - - // description: string - Peer description - void setPeerDescription( - const std::string& peerAddr, - const std::string& description); - - // local_addr: string - Local address to connect/listen on - void setPeerLocalAddr( - const std::string& peerAddr, - const std::string& localAddr); - - // next_hop4: string - IPv4 next-hop for outgoing updates - void setPeerNextHop4( - const std::string& peerAddr, - const std::string& nextHop4); - - // next_hop6: string - IPv6 next-hop for outgoing updates - void setPeerNextHop6( - const std::string& peerAddr, - const std::string& nextHop6); - - // peer_group_name: string - Reference to peer group - void setPeerGroupName( - const std::string& peerAddr, - const std::string& peerGroupName); - - // is_passive: bool - Listen for incoming connections - void setPeerPassive(const std::string& peerAddr, bool isPassive); - - // is_rr_client: bool - Route reflector client - void setPeerRrClient(const std::string& peerAddr, bool isRrClient); - - // next_hop_self: bool - Set next-hop to self - void setPeerNextHopSelf(const std::string& peerAddr, bool nextHopSelf); - - // is_confed_peer: bool - Confederation peer - void setPeerConfedPeer(const std::string& peerAddr, bool isConfedPeer); - - // peer_port: i32 - Remote peer port - void setPeerPort(const std::string& peerAddr, int32_t port); - - // local_port: i32 - Local source port - void setPeerLocalPort(const std::string& peerAddr, int32_t port); - - // connect_mode: string - PASSIVE_ACTIVE, PASSIVE_ONLY, ACTIVE_ONLY - void setPeerConnectMode( - const std::string& peerAddr, - const std::string& connectMode); - - // Peer timers - void setPeerHoldTime(const std::string& peerAddr, int32_t holdTime); - void setPeerKeepalive(const std::string& peerAddr, int32_t keepalive); - void setPeerOutDelay(const std::string& peerAddr, int32_t outDelay); - - // Peer graceful restart - void setPeerGracefulRestartTime(const std::string& peerAddr, int32_t seconds); - void setPeerStatefulHa(const std::string& peerAddr, bool enable); - - // Peer AFI/SAFI - void setPeerDisableIpv4Afi(const std::string& peerAddr, bool disable); - void setPeerDisableIpv6Afi(const std::string& peerAddr, bool disable); - void setPeerIpv4LabeledUnicast(const std::string& peerAddr, bool enable); - void setPeerIpv6LabeledUnicast(const std::string& peerAddr, bool enable); - void setPeerIpv4OverIpv6Nh(const std::string& peerAddr, bool enable); - - // Peer add-path - void setPeerAddPathSend(const std::string& peerAddr, bool enable); - void setPeerAddPathReceive(const std::string& peerAddr, bool enable); - - // Peer AS-path settings - void setPeerRemovePrivateAs(const std::string& peerAddr, bool remove); - void setPeerEnforceFirstAs(const std::string& peerAddr, bool enforce); - - // Peer policies - void setPeerIngressPolicy( - const std::string& peerAddr, - const std::string& policy); - void setPeerEgressPolicy( - const std::string& peerAddr, - const std::string& policy); - - // Peer route limits - void setPeerPreFilterMaxRoutes( - const std::string& peerAddr, - int64_t maxRoutes); - void setPeerPostFilterMaxRoutes( - const std::string& peerAddr, - int64_t maxRoutes); - - // Peer link bandwidth - void setPeerAdvertiseLbw(const std::string& peerAddr, bool advertise); - void setPeerReceiveLbw(const std::string& peerAddr, bool receive); - void setPeerLbwValue(const std::string& peerAddr, int64_t bps); - - // Peer identification (for RSW config compatibility) - void setPeerPeerId(const std::string& peerAddr, const std::string& peerId); - void setPeerType(const std::string& peerAddr, const std::string& type); - void setPeerPeerTag(const std::string& peerAddr, const std::string& peerTag); - - // Peer pre_filter warning settings - void setPeerPreFilterWarningLimit(const std::string& peerAddr, int64_t limit); - void setPeerPreFilterWarningOnly( - const std::string& peerAddr, - bool warningOnly); - - // Peer timer: withdraw_unprog_delay_seconds - void setPeerWithdrawUnprogDelay(const std::string& peerAddr, int32_t seconds); - // ==================== Peer Group Configuration (Level: PG) // ==================== These map to PeerGroup thrift struct fields @@ -463,23 +345,16 @@ class BgpConfigSession { void loadConfig(); void ensureConfigLoaded(); - // Helper to find or create a peer in the peers list - folly::dynamic& findOrCreatePeer(const std::string& peerAddr); - // Helper to find or create a peer group in the peer_groups list folly::dynamic& findOrCreatePeerGroup(const std::string& groupName); - // Helper to ensure peer timers exist - folly::dynamic& ensurePeerTimers(const std::string& peerAddr); + // Helper to ensure peer group timers exist folly::dynamic& ensurePeerGroupTimers(const std::string& groupName); // Helper to ensure add_path config exists - folly::dynamic& ensurePeerAddPath(const std::string& peerAddr); folly::dynamic& ensurePeerGroupAddPath(const std::string& groupName); // Helper to ensure pre_filter/post_filter config exists - folly::dynamic& ensurePeerPreFilter(const std::string& peerAddr); - folly::dynamic& ensurePeerPostFilter(const std::string& peerAddr); folly::dynamic& ensurePeerGroupPreFilter(const std::string& groupName); folly::dynamic& ensurePeerGroupPostFilter(const std::string& groupName); }; diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/BgpPeerAddr.h b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpPeerAddr.h new file mode 100644 index 0000000000000..63ee0225f9a18 --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpPeerAddr.h @@ -0,0 +1,66 @@ +/* + * 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 +#include +#include +#include +#include + +/** + * Peer-address helpers shared by the BGP neighbor commands (`config protocol + * bgp neighbor` / `delete protocol bgp neighbor`). + * + * peer_addr is the identity of a BgpPeer, but the same address can be spelled + * many ways (2001:DB8::1 vs 2001:db8::1), and passive listening sessions may + * use prefix notation (2001:db8::/64, per bgp_config.thrift). Both commands + * normalize the CLI-supplied address once and compare stored entries in + * normalized form so every spelling addresses the same peer. + * + * Prefixes are masked (10.0.0.1/24 -> 10.0.0.0/24) because bgpd keys dynamic + * peers by the masked network (IPAddress::createNetwork in its config loader), + * so two spellings that differ only in host bits are the same neighbor to it. + */ +namespace facebook::fboss::bgpcli { + +// Normalize an address (or prefix, for passive listening sessions) to its +// canonical string form; a prefix has its host bits cleared. nullopt if the +// input parses as neither. +inline std::optional normalizeBgpPeerAddr(const std::string& s) { + if (auto addr = folly::IPAddress::tryFromString(s)) { + return addr->str(); + } + if (auto network = folly::IPAddress::tryCreateNetwork( + s, /* defaultCidr */ -1, /* applyMask */ true)) { + return folly::IPAddress::networkToString(*network); + } + return std::nullopt; +} + +// Find the peer whose (normalized) peer_addr matches the given normalized +// address; peers().end() if absent. Stored entries may predate normalization, +// so their normalized form is compared when possible. +inline std::vector::iterator findBgpPeer( + bgp::thrift::BgpConfig& cfg, + const std::string& normalizedAddr) { + auto& peers = *cfg.peers(); + for (auto it = peers.begin(); it != peers.end(); ++it) { + const std::string key = + normalizeBgpPeerAddr(*it->peer_addr()).value_or(*it->peer_addr()); + if (key == normalizedAddr) { + return it; + } + } + return peers.end(); +} + +} // namespace facebook::fboss::bgpcli diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.cpp new file mode 100644 index 0000000000000..1df7813f49e63 --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.cpp @@ -0,0 +1,797 @@ +/* + * 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/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h" + +#include "fboss/cli/fboss2/CmdHandler.cpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "configerator/structs/neteng/fboss/bgp/if/gen-cpp2/bgp_attr_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliAttrHandlers.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpPeerAddr.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" +#include "fmt/format.h" +#include "thrift/lib/cpp/Thrift.h" + +namespace facebook::fboss { + +namespace { + +// CLI attribute names, exactly as documented (two-token attributes keep their +// space). Kept here (not as raw string literals at the dispatch sites) so the +// valid-attribute set and the handler table stay in sync. +constexpr std::string_view kRemoteAsn = "remote-asn"; +constexpr std::string_view kLocalAsn = "local-asn"; +constexpr std::string_view kDescription = "description"; +constexpr std::string_view kPeerTag = "peer-tag"; +constexpr std::string_view kPeerGroup = "peer-group"; +constexpr std::string_view kIngressPolicy = "ingress-policy"; +constexpr std::string_view kEgressPolicy = "egress-policy"; +constexpr std::string_view kRrClient = "rr-client"; +constexpr std::string_view kConfedPeer = "confed-peer"; +constexpr std::string_view kRedistributePeer = "redistribute-peer"; +constexpr std::string_view kEnhancedRouteRefresh = "enhanced-route-refresh"; +constexpr std::string_view kPassive = "passive"; +constexpr std::string_view kPeerPort = "peer-port"; +constexpr std::string_view kAddPathSend = "add-path send"; +constexpr std::string_view kAddPathReceive = "add-path receive"; +constexpr std::string_view kAfiDisableIpv4Afi = "afi disable-ipv4-afi"; +constexpr std::string_view kAfiDisableIpv6Afi = "afi disable-ipv6-afi"; +constexpr std::string_view kAfiIpv4OverIpv6Nh = "afi ipv4-over-ipv6-nh"; +constexpr std::string_view kAfiIpv4LabeledUnicast = "afi ipv4-labeled-unicast"; +constexpr std::string_view kAfiIpv6LabeledUnicast = "afi ipv6-labeled-unicast"; +constexpr std::string_view kBindAddrAddress = "bind-addr address"; +constexpr std::string_view kBindAddrPort = "bind-addr port"; +constexpr std::string_view kGracefulRestartTime = + "graceful-restart restart-time"; +constexpr std::string_view kGracefulRestartStatefulHa = + "graceful-restart stateful-ha"; +constexpr std::string_view kMaxRoutePreFilter = "max-route pre-filter"; +constexpr std::string_view kMaxRoutePostFilter = "max-route post-filter"; +constexpr std::string_view kMaxRoutePostWarningThreshold = + "max-route post-warning-threshold"; +constexpr std::string_view kTimersHoldTime = "timers hold-time"; +constexpr std::string_view kTimersKeepalive = "timers keepalive"; +constexpr std::string_view kTimersOutDelay = "timers out-delay"; +// Parity attributes: not in the documented neighbor grammar, but supported by +// the per-attribute peer commands this dispatcher replaces (and by real +// BgpPeer thrift fields), so dropping them would regress existing usage. +constexpr std::string_view kNextHop4 = "next-hop4"; +constexpr std::string_view kNextHop6 = "next-hop6"; +constexpr std::string_view kNextHopSelf = "next-hop-self"; +constexpr std::string_view kPeerId = "peer-id"; +constexpr std::string_view kPeerType = "type"; +constexpr std::string_view kLinkBandwidth = "link-bandwidth"; +constexpr std::string_view kAdvertiseLbw = "advertise-lbw"; +constexpr std::string_view kReceiveLbw = "receive-lbw"; +constexpr std::string_view kTimersWithdrawUnprogDelay = + "timers withdraw-unprog-delay"; +constexpr std::string_view kMaxRoutePreWarningThreshold = + "max-route pre-warning-threshold"; +constexpr std::string_view kMaxRoutePreWarningOnly = + "max-route pre-warning-only"; +constexpr std::string_view kMaxRoutePostWarningOnly = + "max-route post-warning-only"; + +// Protocol ranges bgpd enforces (or the wire format silently truncates to): +// the OPEN hold time is 16 bits and bgpd refuses 1-2s; the graceful-restart +// restart time is the 12-bit field of RFC 4724. +constexpr int32_t kHoldTimeMinSeconds = 3; +constexpr int32_t kHoldTimeMaxSeconds = 65535; +constexpr int32_t kGracefulRestartMaxSeconds = 4095; + +// Value placeholders for the generated help. One per value shape, so the +// wording matches the factory that validates that shape. +constexpr std::string_view kUsageBool = ""; +constexpr std::string_view kUsageAsn = ""; +constexpr std::string_view kUsageName = ""; +constexpr std::string_view kUsageString = ""; +constexpr std::string_view kUsageText = ""; +constexpr std::string_view kUsagePolicy = ""; +constexpr std::string_view kUsageIp = ""; +constexpr std::string_view kUsageIpv4 = ""; +constexpr std::string_view kUsageIpv6 = ""; +constexpr std::string_view kUsageSeconds = ""; +constexpr std::string_view kUsageHoldTime = ""; +constexpr std::string_view kUsageGracefulRestart = ""; +constexpr std::string_view kUsageRouteCount = ""; +constexpr std::string_view kUsageBitRate = "[K|M|G] | auto"; + +using bgpcli::Tokens; +using BgpConfig = bgp::thrift::BgpConfig; +using BgpPeer = bgp::thrift::BgpPeer; +using PeerGroup = bgp::thrift::PeerGroup; +using bgpcli::asnAttr; +using bgpcli::AttrHandler; +using bgpcli::bitRateAttr; +using bgpcli::boolAttr; +using bgpcli::err; +using bgpcli::ipAttr; +using bgpcli::joinedStringAttr; +using bgpcli::ok; +using bgpcli::parseBool; +using bgpcli::rejectedAttr; +using bgpcli::Result; +using bgpcli::routeCountAttr; +using bgpcli::secondsAttr; +using bgpcli::SecondsRange; +using bgpcli::stringAttr; +using bgpcli::thriftEnumAttr; +using facebook::neteng::fboss::bgp_attr::AddPath; +using facebook::neteng::fboss::bgp_attr::AdvertiseLinkBandwidth; +using facebook::neteng::fboss::bgp_attr::ReceiveLinkBandwidth; + +// ---- per-attribute setters ------------------------------------------------- +// Pure thrift assignment: no validation, no messages. Parsing, bounds and the +// user-facing text all live in the shared factories in BgpCliAttrHandlers.h, +// so a setter is just the field this attribute writes. Field names map 1:1 to +// bgp_config.thrift, so the compiler validates them. + +void setRemoteAsn(BgpPeer& p, int64_t v) { + p.remote_as_4_byte() = v; +} + +void setLocalAsn(BgpPeer& p, int64_t v) { + p.local_as_4_byte() = v; +} + +void setDescription(BgpPeer& p, const std::string& v) { + p.description() = v; +} + +void setPeerTag(BgpPeer& p, const std::string& v) { + p.peer_tag() = v; +} + +void setPeerGroup(BgpPeer& p, const std::string& v) { + p.peer_group_name() = v; +} + +void setIngressPolicy(BgpPeer& p, const std::string& v) { + p.ingress_policy_name() = v; +} + +void setEgressPolicy(BgpPeer& p, const std::string& v) { + p.egress_policy_name() = v; +} + +void setRrClient(BgpPeer& p, bool v) { + p.is_rr_client() = v; +} + +void setConfedPeer(BgpPeer& p, bool v) { + p.is_confed_peer() = v; +} + +void setRedistributePeer(BgpPeer& p, bool v) { + p.is_redistribute_peer() = v; +} + +void setEnhancedRouteRefresh(BgpPeer& p, bool v) { + p.enhanced_route_refresh() = v; +} + +// bgpd reads is_passive=true as PASSIVE_ONLY (listen) and false as +// PASSIVE_ACTIVE (listen and connect); there is no active-only mode, so the +// CLI exposes the flag itself rather than inventing connect-mode names. +void setPassive(BgpPeer& p, bool v) { + p.is_passive() = v; +} + +void setDisableIpv4Afi(BgpPeer& p, bool v) { + p.disable_ipv4_afi() = v; +} + +void setDisableIpv6Afi(BgpPeer& p, bool v) { + p.disable_ipv6_afi() = v; +} + +void setIpv4OverIpv6Nh(BgpPeer& p, bool v) { + p.v4_over_v6_nexthop() = v; +} + +void setBindAddrAddress(BgpPeer& p, const std::string& v) { + p.local_addr() = v; +} + +void setGracefulRestartTime(BgpPeer& p, int32_t v) { + p.bgp_peer_timers().ensure().graceful_restart_seconds() = v; +} + +void setGracefulRestartStatefulHa(BgpPeer& p, bool v) { + p.enable_stateful_ha() = v; +} + +void setMaxRoutePreFilter(BgpPeer& p, int64_t v) { + p.pre_filter().ensure().max_routes() = v; +} + +void setMaxRoutePostFilter(BgpPeer& p, int64_t v) { + p.post_filter().ensure().max_routes() = v; +} + +// RouteLimit.warning_limit is an absolute route count in the thrift schema, +// not a percentage. +void setMaxRoutePreWarningThreshold(BgpPeer& p, int64_t v) { + p.pre_filter().ensure().warning_limit() = v; +} + +void setMaxRoutePostWarningThreshold(BgpPeer& p, int64_t v) { + p.post_filter().ensure().warning_limit() = v; +} + +void setMaxRoutePreWarningOnly(BgpPeer& p, bool v) { + p.pre_filter().ensure().warning_only() = v; +} + +void setMaxRoutePostWarningOnly(BgpPeer& p, bool v) { + p.post_filter().ensure().warning_only() = v; +} + +void setTimersHoldTime(BgpPeer& p, int32_t v) { + p.bgp_peer_timers().ensure().hold_time_seconds() = v; +} + +void setTimersKeepalive(BgpPeer& p, int32_t v) { + p.bgp_peer_timers().ensure().keep_alive_seconds() = v; +} + +void setTimersOutDelay(BgpPeer& p, int32_t v) { + p.bgp_peer_timers().ensure().out_delay_seconds() = v; +} + +void setTimersWithdrawUnprogDelay(BgpPeer& p, int32_t v) { + p.bgp_peer_timers().ensure().withdraw_unprog_delay_seconds() = v; +} + +void setNextHop4(BgpPeer& p, const std::string& v) { + p.next_hop4() = v; +} + +void setNextHop6(BgpPeer& p, const std::string& v) { + p.next_hop6() = v; +} + +void setNextHopSelf(BgpPeer& p, bool v) { + p.next_hop_self() = v; +} + +void setPeerId(BgpPeer& p, const std::string& v) { + p.peer_id() = v; +} + +void setPeerType(BgpPeer& p, const std::string& v) { + p.type() = v; +} + +void setLinkBandwidth(BgpPeer& p, const std::string& v) { + p.link_bandwidth_bps() = v; +} + +void setAdvertiseLbw(BgpPeer& p, AdvertiseLinkBandwidth v) { + p.advertise_link_bandwidth() = v; +} + +void setReceiveLbw(BgpPeer& p, ReceiveLinkBandwidth v) { + p.receive_link_bandwidth() = v; +} + +// ---- inherited values ------------------------------------------------------ +// bgpd resolves a peer against its peer group at STRUCT granularity: if the +// peer carries bgp_peer_timers (or pre_filter / post_filter) at all, every +// field of that struct comes from the peer and the group's copy is ignored; +// a timers struct whose other fields are thrift defaults therefore runs the +// session with hold time 0 (no keepalives). So before the first attribute of +// such a struct is set on a peer, the struct is seeded from what the peer +// inherits today -- the group's struct, else the global defaults -- and the +// one requested field is then changed on top of it. + +const PeerGroup* peerGroupOf(const BgpConfig& cfg, const BgpPeer& peer) { + if (!peer.peer_group_name() || !cfg.peer_groups()) { + return nullptr; + } + for (const auto& group : *cfg.peer_groups()) { + if (*group.name() == *peer.peer_group_name()) { + return &group; + } + } + return nullptr; +} + +void seedTimers(const BgpConfig& cfg, BgpPeer& peer) { + if (peer.bgp_peer_timers()) { + return; + } + if (const auto* group = peerGroupOf(cfg, peer); + group && group->bgp_peer_timers()) { + peer.bgp_peer_timers() = *group->bgp_peer_timers(); + return; + } + bgp::thrift::BgpPeerTimers timers; + timers.hold_time_seconds() = *cfg.hold_time(); + timers.keep_alive_seconds() = *cfg.hold_time() / 3; + peer.bgp_peer_timers() = std::move(timers); +} + +void seedRouteLimit(const BgpConfig& cfg, BgpPeer& peer, bool pre) { + auto field = [pre](auto& p) -> decltype(auto) { + return pre ? p.pre_filter() : p.post_filter(); + }; + if (field(peer)) { + return; + } + if (const auto* group = peerGroupOf(cfg, peer); group && field(*group)) { + field(peer) = *field(*group); + } + // Otherwise the setter's ensure() yields the RouteLimit thrift defaults, + // which is also what bgpd applies to a peer without one. +} + +// A neighbor handler sees the whole config (for the peer group lookup) plus +// the peer being edited. The shared factories only know the peer, so they are +// adapted; the adapters for struct-valued attributes seed first. +using NeighborHandler = + std::function; + +// One dispatch-table row: how the value is spelled in help, and the handler. +struct NeighborAttr { + std::string usage; + NeighborHandler handle; +}; + +// "" from a thrift enum's value names, for the generated help. +template +std::string enumUsage() { + std::string out = "<"; + for (auto n : apache::thrift::TEnumTraits::names) { + out += out.size() == 1 ? "" : "|"; + out += std::string(n); + } + return out + ">"; +} + +NeighborHandler peerOnly(AttrHandler handler) { + return [handler = std::move(handler)]( + const BgpConfig&, BgpPeer& peer, const Tokens& values) { + return handler(peer, values); + }; +} + +NeighborHandler withTimers(AttrHandler handler) { + return [handler = std::move(handler)]( + const BgpConfig& cfg, BgpPeer& peer, const Tokens& values) { + seedTimers(cfg, peer); + return handler(peer, values); + }; +} + +NeighborHandler withRouteLimit(bool pre, AttrHandler handler) { + return [pre, handler = std::move(handler)]( + const BgpConfig& cfg, BgpPeer& peer, const Tokens& values) { + seedRouteLimit(cfg, peer, pre); + return handler(peer, values); + }; +} + +// ---- hand-written handlers ------------------------------------------------- +// Only for value shapes no factory covers, because the shape is unique to this +// attribute rather than reusable. + +// add-path send/receive: a boolean at the CLI, but the two attributes share +// one thrift field -- add_path is a single enum whose values form a bitmask by +// design (RECEIVE=1, SEND=2, BOTH=3). Each takes the boolAttr text but merges +// its direction into the value in effect for the peer (its own, else the peer +// group's) instead of overwriting it. The enum has no "disabled" member and +// an unset peer field means "inherit", so a direction the group enables +// cannot be switched off per peer; that is refused rather than staged as a +// no-op the CLI would report as success. +Result applyAddPath( + const BgpConfig& cfg, + BgpPeer& peer, + bool send, + const Tokens& values) { + const std::string_view name = send ? kAddPathSend : kAddPathReceive; + if (values.size() != 1) { + return err(fmt::format("Error: {} requires ", name)); + } + auto enable = parseBool(values[0]); + if (!enable) { + return err( + fmt::format( + "Error: Invalid {} value '{}'; expected true or false", + name, + values[0])); + } + const auto* group = peerGroupOf(cfg, peer); + const std::optional inherited = group && group->add_path() + ? std::optional(*group->add_path()) + : std::nullopt; + const std::optional effective = + peer.add_path() ? std::optional(*peer.add_path()) : inherited; + int bits = effective ? static_cast(*effective) : 0; + const int bit = send ? static_cast(AddPath::SEND) + : static_cast(AddPath::RECEIVE); + bits = *enable ? (bits | bit) : (bits & ~bit); + if (bits == 0) { + if (inherited) { + return err( + fmt::format( + "Error: cannot disable {} on this neighbor: peer group {} " + "enables add-path {} and BgpPeer.add_path has no disabled " + "value to override it (an unset field inherits the group); " + "change it on the peer group instead", + name, + *group->name(), + apache::thrift::TEnumTraits::findName(*inherited))); + } + peer.add_path().reset(); + } else { + peer.add_path() = static_cast(bits); + } + return ok( + fmt::format( + "Successfully {} {}", *enable ? "enabled" : "disabled", name)); +} + +Result addPathSend(const BgpConfig& cfg, BgpPeer& peer, const Tokens& values) { + return applyAddPath(cfg, peer, /* send */ true, values); +} + +Result +addPathReceive(const BgpConfig& cfg, BgpPeer& peer, const Tokens& values) { + return applyAddPath(cfg, peer, /* send */ false, values); +} + +// ---- the dispatch table ---------------------------------------------------- +// One line per attribute: dispatch key, value shape, setter. There are no +// handler bodies here by design — if an attribute appears to need one, its +// value shape is missing a factory and the fix is to add the factory. + +constexpr std::string_view kNoPeerPortField = + "BgpPeer has no per-neighbor port field; the daemon listens on the global " + "listen_port"; +constexpr std::string_view kNoLabeledUnicastField = + "bgp_config.thrift has no labeled-unicast field on BgpPeer"; + +const std::map>& attrHandlers() { + auto plain = [](std::string_view usage, AttrHandler h) { + return NeighborAttr{std::string(usage), peerOnly(std::move(h))}; + }; + auto timers = [](std::string_view usage, AttrHandler h) { + return NeighborAttr{std::string(usage), withTimers(std::move(h))}; + }; + auto preLimit = [](std::string_view usage, AttrHandler h) { + return NeighborAttr{ + std::string(usage), withRouteLimit(/* pre */ true, std::move(h))}; + }; + auto postLimit = [](std::string_view usage, AttrHandler h) { + return NeighborAttr{ + std::string(usage), withRouteLimit(/* pre */ false, std::move(h))}; + }; + auto rejected = [](std::string_view name, std::string_view reason) { + return NeighborAttr{ + fmt::format("(not supported: {})", reason), + peerOnly(rejectedAttr(name, reason))}; + }; + static const std::map> kHandlers = { + {std::string(kRemoteAsn), + plain(kUsageAsn, asnAttr(kRemoteAsn, setRemoteAsn))}, + {std::string(kLocalAsn), + plain(kUsageAsn, asnAttr(kLocalAsn, setLocalAsn))}, + {std::string(kDescription), + plain( + kUsageText, + joinedStringAttr(kDescription, setDescription))}, + {std::string(kPeerTag), + plain( + kUsageString, stringAttr(kPeerTag, "string", setPeerTag))}, + {std::string(kPeerGroup), + plain( + kUsageName, stringAttr(kPeerGroup, "name", setPeerGroup))}, + {std::string(kIngressPolicy), + plain( + kUsagePolicy, + stringAttr( + kIngressPolicy, "policy-name", setIngressPolicy))}, + {std::string(kEgressPolicy), + plain( + kUsagePolicy, + stringAttr(kEgressPolicy, "policy-name", setEgressPolicy))}, + {std::string(kRrClient), + plain(kUsageBool, boolAttr(kRrClient, setRrClient))}, + {std::string(kConfedPeer), + plain(kUsageBool, boolAttr(kConfedPeer, setConfedPeer))}, + {std::string(kRedistributePeer), + plain( + kUsageBool, + boolAttr(kRedistributePeer, setRedistributePeer))}, + {std::string(kEnhancedRouteRefresh), + plain( + kUsageBool, + boolAttr(kEnhancedRouteRefresh, setEnhancedRouteRefresh))}, + {std::string(kPassive), + plain(kUsageBool, boolAttr(kPassive, setPassive))}, + {std::string(kPeerPort), rejected(kPeerPort, kNoPeerPortField)}, + {std::string(kAddPathSend), + NeighborAttr{std::string(kUsageBool), addPathSend}}, + {std::string(kAddPathReceive), + NeighborAttr{std::string(kUsageBool), addPathReceive}}, + {std::string(kAfiDisableIpv4Afi), + plain( + kUsageBool, + boolAttr(kAfiDisableIpv4Afi, setDisableIpv4Afi))}, + {std::string(kAfiDisableIpv6Afi), + plain( + kUsageBool, + boolAttr(kAfiDisableIpv6Afi, setDisableIpv6Afi))}, + {std::string(kAfiIpv4OverIpv6Nh), + plain( + kUsageBool, + boolAttr(kAfiIpv4OverIpv6Nh, setIpv4OverIpv6Nh))}, + {std::string(kAfiIpv4LabeledUnicast), + rejected(kAfiIpv4LabeledUnicast, kNoLabeledUnicastField)}, + {std::string(kAfiIpv6LabeledUnicast), + rejected(kAfiIpv6LabeledUnicast, kNoLabeledUnicastField)}, + {std::string(kBindAddrAddress), + plain(kUsageIp, ipAttr(kBindAddrAddress, setBindAddrAddress))}, + {std::string(kBindAddrPort), rejected(kBindAddrPort, kNoPeerPortField)}, + {std::string(kGracefulRestartTime), + timers( + kUsageGracefulRestart, + secondsAttr( + kGracefulRestartTime, + setGracefulRestartTime, + SecondsRange{0, kGracefulRestartMaxSeconds}))}, + {std::string(kGracefulRestartStatefulHa), + plain( + kUsageBool, + boolAttr( + kGracefulRestartStatefulHa, setGracefulRestartStatefulHa))}, + {std::string(kMaxRoutePreFilter), + preLimit( + kUsageRouteCount, + routeCountAttr(kMaxRoutePreFilter, setMaxRoutePreFilter))}, + {std::string(kMaxRoutePostFilter), + postLimit( + kUsageRouteCount, + routeCountAttr( + kMaxRoutePostFilter, setMaxRoutePostFilter))}, + {std::string(kMaxRoutePreWarningThreshold), + preLimit( + kUsageRouteCount, + routeCountAttr( + kMaxRoutePreWarningThreshold, setMaxRoutePreWarningThreshold))}, + {std::string(kMaxRoutePostWarningThreshold), + postLimit( + kUsageRouteCount, + routeCountAttr( + kMaxRoutePostWarningThreshold, + setMaxRoutePostWarningThreshold))}, + {std::string(kMaxRoutePreWarningOnly), + preLimit( + kUsageBool, + boolAttr( + kMaxRoutePreWarningOnly, setMaxRoutePreWarningOnly))}, + {std::string(kMaxRoutePostWarningOnly), + postLimit( + kUsageBool, + boolAttr( + kMaxRoutePostWarningOnly, setMaxRoutePostWarningOnly))}, + {std::string(kTimersHoldTime), + timers( + kUsageHoldTime, + secondsAttr( + kTimersHoldTime, + setTimersHoldTime, + SecondsRange{kHoldTimeMinSeconds, kHoldTimeMaxSeconds}))}, + {std::string(kTimersKeepalive), + timers( + kUsageSeconds, + secondsAttr(kTimersKeepalive, setTimersKeepalive))}, + {std::string(kTimersOutDelay), + timers( + kUsageSeconds, + secondsAttr(kTimersOutDelay, setTimersOutDelay))}, + {std::string(kTimersWithdrawUnprogDelay), + timers( + kUsageSeconds, + secondsAttr( + kTimersWithdrawUnprogDelay, setTimersWithdrawUnprogDelay))}, + {std::string(kNextHop4), + plain( + kUsageIpv4, + ipAttr(kNextHop4, setNextHop4, /* requireV6 */ false))}, + {std::string(kNextHop6), + plain( + kUsageIpv6, + ipAttr(kNextHop6, setNextHop6, /* requireV6 */ true))}, + {std::string(kNextHopSelf), + plain(kUsageBool, boolAttr(kNextHopSelf, setNextHopSelf))}, + {std::string(kPeerId), + plain(kUsageString, stringAttr(kPeerId, "string", setPeerId))}, + {std::string(kPeerType), + plain( + kUsageString, + stringAttr(kPeerType, "string", setPeerType))}, + {std::string(kLinkBandwidth), + plain( + kUsageBitRate, + bitRateAttr(kLinkBandwidth, setLinkBandwidth))}, + {std::string(kAdvertiseLbw), + plain( + enumUsage(), + thriftEnumAttr( + kAdvertiseLbw, setAdvertiseLbw))}, + {std::string(kReceiveLbw), + plain( + enumUsage(), + thriftEnumAttr( + kReceiveLbw, setReceiveLbw))}, + }; + return kHandlers; +} + +std::string validAttrList() { + std::string out; + for (const auto& [name, _] : attrHandlers()) { + if (!out.empty()) { + out += ", "; + } + out += name; + } + return out; +} + +// A fresh peer for peerAddr. Setting an attribute on a not-yet-created +// neighbor implicitly creates it, so command ordering stays forgiving; a bare +// `neighbor ` creates one explicitly. +BgpPeer newPeer(const std::string& peerAddr) { + BgpPeer peer; + peer.peer_addr() = peerAddr; + // local_addr / next_hop4 / next_hop6 are non-optional thrift fields that + // bgpd parses with folly::IPAddress at config load -- an empty string makes + // the daemon abort (IPAddressFormatException) before serving any config. + // Seed them with the unspecified address of the right family; the user + // overrides them via `bind-addr address` / `next-hop4` / `next-hop6`. + const bool isV6 = peerAddr.find(':') != std::string::npos; + peer.local_addr() = isV6 ? "::" : "0.0.0.0"; + peer.next_hop4() = "0.0.0.0"; + peer.next_hop6() = "::"; + return peer; +} + +} // namespace + +std::vector bgpNeighborAttributeNames() { + std::vector names; + for (const auto& [name, _] : attrHandlers()) { + names.push_back(name); + } + return names; +} + +std::string bgpNeighborAttributeHelp() { + size_t width = 0; + for (const auto& [name, _] : attrHandlers()) { + width = std::max(width, name.size()); + } + std::string out = "Attributes ( ):\n"; + for (const auto& [name, attr] : attrHandlers()) { + out += fmt::format(" {:<{}} {}\n", name, width, attr.usage); + } + return out; +} + +// Parse + validate at construction so queryClient stays a thin dispatch. +// Throwing std::invalid_argument is how the framework surfaces arg parse +// errors (same mechanism as BgpGlobalConfig). +BgpNeighborConfig::BgpNeighborConfig(std::vector v) + : utils::BaseObjectArgType(v) { + if (v.empty()) { + throw std::invalid_argument( + "Error: neighbor is required, followed by an optional " + " "); + } + auto normalized = bgpcli::normalizeBgpPeerAddr(v[0]); + if (!normalized) { + throw std::invalid_argument( + fmt::format("Error: Invalid neighbor address '{}'", v[0])); + } + peerAddr_ = std::move(*normalized); + + if (v.size() == 1) { + return; // bare `neighbor `: create the peer + } + + // Attributes may span two tokens (e.g. `timers hold-time`); match the + // longest prefix of the remaining tokens against the dispatch table. + const auto& handlers = attrHandlers(); + if (v.size() >= 3) { + std::string twoToken = v[1] + " " + v[2]; + if (handlers.find(twoToken) != handlers.end()) { + attr_ = std::move(twoToken); + values_.assign(v.begin() + 3, v.end()); + return; + } + } + if (handlers.find(v[1]) != handlers.end()) { + attr_ = v[1]; + values_.assign(v.begin() + 2, v.end()); + return; + } + throw std::invalid_argument( + fmt::format( + "Error: unknown neighbor attribute '{}'. Valid attributes: {}", + v[1], + validAttrList())); +} + +CmdConfigProtocolBgpNeighborTraits::RetType +CmdConfigProtocolBgpNeighbor::queryClient( + const HostInfo& /* hostInfo */, + const ObjectArgType& args) { + auto& session = ConfigSession::getInstance(); + auto& cfg = session.getBgpConfig(); + auto& peers = *cfg.peers(); + auto existing = bgpcli::findBgpPeer(cfg, args.peerAddr()); + + // Edit a copy: a rejected value (or a struct seeded for it) must leave the + // in-memory config exactly as it was, since later lookups in the same + // process see it whether or not it was saved. + BgpPeer peer = existing != peers.end() ? *existing : newPeer(args.peerAddr()); + Result result = args.attr().empty() + ? ok(fmt::format("Successfully created BGP neighbor {}", args.peerAddr())) + // The attribute is guaranteed valid: BgpNeighborConfig's constructor + // rejects an unknown attribute before we get here. + : attrHandlers() + .find(args.attr()) + ->second.handle(cfg, peer, args.values()); + if (!result.ok) { + return result.message; + } + if (existing != peers.end()) { + *existing = std::move(peer); + } else { + peers.push_back(std::move(peer)); + } + if (!args.attr().empty()) { + result.message += fmt::format(" for neighbor {}", args.peerAddr()); + } + session.saveBgpConfig(); + result.message += + fmt::format("\nConfig saved to: {}", session.getBgpSessionConfigPath()); + return result.message; +} + +void CmdConfigProtocolBgpNeighbor::printOutput(const RetType& output) { + std::cout << output << std::endl; +} + +// Explicit template instantiation +template void CmdHandler< + CmdConfigProtocolBgpNeighbor, + CmdConfigProtocolBgpNeighborTraits>::run(); + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h b/fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h new file mode 100644 index 0000000000000..22add751f7e68 --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h @@ -0,0 +1,97 @@ +/* + * 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 +#include +#include "CLI/App.hpp" +#include "fboss/cli/fboss2/CmdHandler.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/CmdConfigProtocolBgp.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +namespace facebook::fboss { + +// Parsed `config protocol bgp neighbor [ ...]`, +// validated at construction. The neighbor address is normalized via +// folly::IPAddress so different spellings of the same address (e.g. +// 2001:DB8::1 vs 2001:db8::1) key the same peer. Attributes may span two +// tokens (e.g. `timers hold-time`); the dispatch table is matched +// longest-prefix-first. A bare `neighbor ` (no attribute) creates the +// peer. +class BgpNeighborConfig : public utils::BaseObjectArgType { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + /* implicit */ BgpNeighborConfig(std::vector v); + const std::string& peerAddr() const { + return peerAddr_; + } + const std::string& attr() const { + return attr_; + } + const std::vector& values() const { + return values_; + } + const static utils::ObjectArgTypeId id = + utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_MESSAGE; + + private: + std::string peerAddr_; + std::string attr_; // matched dispatch key ("" = bare create) + std::vector values_; +}; + +// The ` ` list the neighbor command accepts, rendered one +// attribute per line for `neighbor --help`. Generated from the dispatch table +// in the .cpp, so it cannot drift from what the command actually parses. +std::string bgpNeighborAttributeHelp(); + +// The dispatch keys (e.g. "remote-asn", "timers hold-time"), sorted. +std::vector bgpNeighborAttributeNames(); + +// Single handler for the whole `config protocol bgp neighbor` family. The +// first positional token is the neighbor address, the next one or two tokens +// name the attribute, and the remaining tokens are its value(s); dispatch and +// per-attribute parsing live in the .cpp so adding a new neighbor tunable is +// a one-entry change rather than a new command class. Mirrors +// CmdConfigProtocolBgpGlobal. +struct CmdConfigProtocolBgpNeighborTraits : public WriteCommandTraits { + using ParentCmd = CmdConfigProtocolBgp; + static void addCliArg(CLI::App& cmd, std::vector& args) { + // positionals_at_end: once the first positional (the neighbor address, + // which never collides with a command name) is consumed, CLI11 stops + // classifying later tokens as subcommands. Without this, an attribute + // token that matches a sibling command name (e.g. `peer-group`) is + // stolen by CLI11's parent-chain subcommand fallthrough and the + // remaining tokens are misparsed. + cmd.positionals_at_end(); + cmd.add_option("args", args, " [ ...]"); + // The attribute list is long, so it lives in this subcommand's own help + // footer rather than in the one-line description shown by the parent. + cmd.footer(bgpNeighborAttributeHelp()); + } + using ObjectArgType = BgpNeighborConfig; + using RetType = std::string; +}; + +class CmdConfigProtocolBgpNeighbor : public CmdHandler< + CmdConfigProtocolBgpNeighbor, + CmdConfigProtocolBgpNeighborTraits> { + public: + using ObjectArgType = CmdConfigProtocolBgpNeighborTraits::ObjectArgType; + using RetType = CmdConfigProtocolBgpNeighborTraits::RetType; + + RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& args); + + void printOutput(const RetType& output); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.cpp deleted file mode 100644 index 756a1f4e958af..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerTraits::RetType CmdConfigProtocolBgpPeer::queryClient( - const HostInfo& /* hostInfo */, - const ObjectArgType& peerAddressList) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - - // Extract the first IP from the list as peer address - std::string peerAddress = peerAddressList.data()[0]; - - // Create peer entry in the peers list (translates to BGP++ thrift format) - session.createPeer(peerAddress); - session.saveConfig(); - - return fmt::format( - "BGP peer {} created.\n" - "Use subcommands: remote-asn, local-addr, next-hop4, next-hop6, peer-group\n" - "Config saved to: {}", - peerAddress, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeer::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void -CmdHandler::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h deleted file mode 100644 index eaee525fe41b3..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/CmdConfigProtocolBgp.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgp; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("ipAddrs", args, "IPv4 or IPv6 addr(s)"); - } - using ObjectArgType = utils::IPList; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeer : public CmdHandler< - CmdConfigProtocolBgpPeer, - CmdConfigProtocolBgpPeerTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpPeerTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const ObjectArgType& peerAddress); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerAdvertiseLbw.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerAdvertiseLbw.cpp deleted file mode 100644 index 2cc45ff10335e..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerAdvertiseLbw.cpp +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerAdvertiseLbw.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerAdvertiseLbwTraits::RetType -CmdConfigProtocolBgpPeerAdvertiseLbw::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& enableMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (enableMsg.data().empty()) { - return fmt::format( - "Error: enable value required for peer '{}'", peerAddress); - } - std::string enableStr = enableMsg.data()[0]; - bool enable = (enableStr == "true" || enableStr == "1" || enableStr == "yes"); - - session.createPeer(peerAddress); - session.setPeerAdvertiseLbw(peerAddress, enable); - session.saveConfig(); - - return fmt::format( - "Successfully {} advertise-link-bandwidth for peer '{}'\n" - "Config saved to: {}", - enable ? "enabled" : "disabled", - peerAddress, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerAdvertiseLbw::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerAdvertiseLbw, - CmdConfigProtocolBgpPeerAdvertiseLbwTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerAdvertiseLbw.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerAdvertiseLbw.h deleted file mode 100644 index 0b6de963abefc..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerAdvertiseLbw.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerAdvertiseLbwTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerAdvertiseLbw - : public CmdHandler< - CmdConfigProtocolBgpPeerAdvertiseLbw, - CmdConfigProtocolBgpPeerAdvertiseLbwTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerAdvertiseLbwTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerAdvertiseLbwTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& enable); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDescription.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDescription.cpp deleted file mode 100644 index 51f1fcab3265b..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDescription.cpp +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerDescription.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerDescriptionTraits::RetType -CmdConfigProtocolBgpPeerDescription::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& descriptionMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (descriptionMsg.data().empty()) { - return fmt::format( - "Error: description value is required for peer '{}'", peerAddress); - } - - session.createPeer(peerAddress); - session.setPeerDescription(peerAddress, descriptionMsg.data()[0]); - session.saveConfig(); - - return fmt::format( - "Successfully set description for peer '{}' to: '{}'\n" - "Config saved to: {}", - peerAddress, - descriptionMsg.data()[0], - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerDescription::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerDescription, - CmdConfigProtocolBgpPeerDescriptionTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDescription.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDescription.h deleted file mode 100644 index 7e689baf6f19e..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDescription.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerDescriptionTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerDescription - : public CmdHandler< - CmdConfigProtocolBgpPeerDescription, - CmdConfigProtocolBgpPeerDescriptionTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerDescriptionTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerDescriptionTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& description); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDisableIpv4Afi.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDisableIpv4Afi.cpp deleted file mode 100644 index ccbf99291fce6..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDisableIpv4Afi.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerDisableIpv4Afi.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerDisableIpv4AfiTraits::RetType -CmdConfigProtocolBgpPeerDisableIpv4Afi::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& disableMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (disableMsg.data().empty()) { - return fmt::format( - "Error: disable value required for peer '{}'", peerAddress); - } - std::string disableStr = disableMsg.data()[0]; - bool disable = - (disableStr == "true" || disableStr == "1" || disableStr == "yes"); - - session.createPeer(peerAddress); - session.setPeerDisableIpv4Afi(peerAddress, disable); - session.saveConfig(); - - return fmt::format( - "Successfully {} IPv4 AFI for peer '{}'\n" - "Config saved to: {}", - disable ? "disabled" : "enabled", - peerAddress, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerDisableIpv4Afi::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerDisableIpv4Afi, - CmdConfigProtocolBgpPeerDisableIpv4AfiTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDisableIpv4Afi.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDisableIpv4Afi.h deleted file mode 100644 index 285bc4fb56be1..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerDisableIpv4Afi.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerDisableIpv4AfiTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerDisableIpv4Afi - : public CmdHandler< - CmdConfigProtocolBgpPeerDisableIpv4Afi, - CmdConfigProtocolBgpPeerDisableIpv4AfiTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerDisableIpv4AfiTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerDisableIpv4AfiTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& disable); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerEgressPolicy.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerEgressPolicy.cpp deleted file mode 100644 index 816f2e529b030..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerEgressPolicy.cpp +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerEgressPolicy.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerEgressPolicyTraits::RetType -CmdConfigProtocolBgpPeerEgressPolicy::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& policyMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (policyMsg.data().empty()) { - return fmt::format( - "Error: egress policy name is required for peer '{}'", peerAddress); - } - std::string policyName = policyMsg.data()[0]; - - session.createPeer(peerAddress); - session.setPeerEgressPolicy(peerAddress, policyName); - session.saveConfig(); - - return fmt::format( - "Successfully set egress policy for peer '{}' to: '{}'\n" - "Config saved to: {}", - peerAddress, - policyName, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerEgressPolicy::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerEgressPolicy, - CmdConfigProtocolBgpPeerEgressPolicyTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerEgressPolicy.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerEgressPolicy.h deleted file mode 100644 index abf65d3c8a601..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerEgressPolicy.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerEgressPolicyTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerEgressPolicy - : public CmdHandler< - CmdConfigProtocolBgpPeerEgressPolicy, - CmdConfigProtocolBgpPeerEgressPolicyTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerEgressPolicyTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerEgressPolicyTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& policyName); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerHoldTime.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerHoldTime.cpp deleted file mode 100644 index 521aeb95a135b..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerHoldTime.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerHoldTime.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerHoldTimeTraits::RetType -CmdConfigProtocolBgpPeerHoldTime::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& holdTimeMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (holdTimeMsg.data().empty()) { - return fmt::format( - "Error: hold-time value is required for peer '{}'", peerAddress); - } - - int32_t holdTime; - try { - holdTime = folly::to(holdTimeMsg.data()[0]); - } catch (const std::exception& e) { - return fmt::format( - "Error: invalid hold-time value '{}': {}", - holdTimeMsg.data()[0], - e.what()); - } - - session.createPeer(peerAddress); - session.setPeerHoldTime(peerAddress, holdTime); - session.saveConfig(); - - return fmt::format( - "Successfully set hold-time for peer '{}' to: {} seconds\n" - "Config saved to: {}", - peerAddress, - holdTime, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerHoldTime::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerHoldTime, - CmdConfigProtocolBgpPeerHoldTimeTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerHoldTime.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerHoldTime.h deleted file mode 100644 index 22c2149ab7ebc..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerHoldTime.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerHoldTimeTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerHoldTime - : public CmdHandler< - CmdConfigProtocolBgpPeerHoldTime, - CmdConfigProtocolBgpPeerHoldTimeTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpPeerHoldTimeTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerHoldTimeTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& holdTime); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerIngressPolicy.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerIngressPolicy.cpp deleted file mode 100644 index 0f26e9045b9e2..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerIngressPolicy.cpp +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerIngressPolicy.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerIngressPolicyTraits::RetType -CmdConfigProtocolBgpPeerIngressPolicy::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& policyMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (policyMsg.data().empty()) { - return fmt::format( - "Error: ingress policy name is required for peer '{}'", peerAddress); - } - std::string policyName = policyMsg.data()[0]; - - session.createPeer(peerAddress); - session.setPeerIngressPolicy(peerAddress, policyName); - session.saveConfig(); - - return fmt::format( - "Successfully set ingress policy for peer '{}' to: '{}'\n" - "Config saved to: {}", - peerAddress, - policyName, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerIngressPolicy::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerIngressPolicy, - CmdConfigProtocolBgpPeerIngressPolicyTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerIngressPolicy.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerIngressPolicy.h deleted file mode 100644 index 57647d9acd537..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerIngressPolicy.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerIngressPolicyTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerIngressPolicy - : public CmdHandler< - CmdConfigProtocolBgpPeerIngressPolicy, - CmdConfigProtocolBgpPeerIngressPolicyTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerIngressPolicyTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerIngressPolicyTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& policyName); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLinkBandwidth.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLinkBandwidth.cpp deleted file mode 100644 index 7f42aecef6632..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLinkBandwidth.cpp +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerLinkBandwidth.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -namespace { -// Parse bandwidth string like "10G", "100M", "1000" to bps -int64_t parseBandwidth(const std::string& bwStr) { - std::string upper = bwStr; - for (auto& c : upper) { - c = std::toupper(c); - } - - int64_t multiplier = 1; - std::string numPart = upper; - - if (upper.back() == 'G') { - multiplier = 1000000000LL; - numPart = upper.substr(0, upper.length() - 1); - } else if (upper.back() == 'M') { - multiplier = 1000000LL; - numPart = upper.substr(0, upper.length() - 1); - } else if (upper.back() == 'K') { - multiplier = 1000LL; - numPart = upper.substr(0, upper.length() - 1); - } - - return folly::to(numPart) * multiplier; -} -} // namespace - -CmdConfigProtocolBgpPeerLinkBandwidthTraits::RetType -CmdConfigProtocolBgpPeerLinkBandwidth::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& bandwidthMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (bandwidthMsg.data().empty()) { - return fmt::format( - "Error: link-bandwidth value is required for peer '{}'", peerAddress); - } - - int64_t bps; - try { - bps = parseBandwidth(bandwidthMsg.data()[0]); - } catch (const std::exception& e) { - return fmt::format( - "Error: invalid link-bandwidth value '{}': {}", - bandwidthMsg.data()[0], - e.what()); - } - - session.createPeer(peerAddress); - session.setPeerLbwValue(peerAddress, bps); - session.saveConfig(); - - return fmt::format( - "Successfully set link-bandwidth for peer '{}' to: {} bps\n" - "Config saved to: {}", - peerAddress, - bps, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerLinkBandwidth::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerLinkBandwidth, - CmdConfigProtocolBgpPeerLinkBandwidthTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLinkBandwidth.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLinkBandwidth.h deleted file mode 100644 index 21efd752db201..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLinkBandwidth.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerLinkBandwidthTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerLinkBandwidth - : public CmdHandler< - CmdConfigProtocolBgpPeerLinkBandwidth, - CmdConfigProtocolBgpPeerLinkBandwidthTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerLinkBandwidthTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerLinkBandwidthTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& bandwidth); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLocalAddr.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLocalAddr.cpp deleted file mode 100644 index 16b702514cb68..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLocalAddr.cpp +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerLocalAddr.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerLocalAddrTraits::RetType -CmdConfigProtocolBgpPeerLocalAddr::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& localAddrMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (localAddrMsg.data().empty()) { - return fmt::format( - "Error: local-addr value required for peer '{}'", peerAddress); - } - std::string localAddr = localAddrMsg.data()[0]; - - session.createPeer(peerAddress); - session.setPeerLocalAddr(peerAddress, localAddr); - session.saveConfig(); - - return fmt::format( - "Successfully set local-addr for peer '{}' to: '{}'\n" - "Config saved to: {}", - peerAddress, - localAddr, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerLocalAddr::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerLocalAddr, - CmdConfigProtocolBgpPeerLocalAddrTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLocalAddr.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLocalAddr.h deleted file mode 100644 index e982e20e79d0b..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerLocalAddr.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerLocalAddrTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerLocalAddr - : public CmdHandler< - CmdConfigProtocolBgpPeerLocalAddr, - CmdConfigProtocolBgpPeerLocalAddrTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpPeerLocalAddrTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerLocalAddrTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& localAddr); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerMaxRoutes.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerMaxRoutes.cpp deleted file mode 100644 index 8f7171c157076..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerMaxRoutes.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerMaxRoutes.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerMaxRoutesTraits::RetType -CmdConfigProtocolBgpPeerMaxRoutes::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& maxRoutesMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (maxRoutesMsg.data().empty()) { - return fmt::format( - "Error: max-routes value is required for peer '{}'", peerAddress); - } - - int64_t maxRoutes; - try { - maxRoutes = folly::to(maxRoutesMsg.data()[0]); - } catch (const std::exception& e) { - return fmt::format( - "Error: invalid max-routes value '{}': {}", - maxRoutesMsg.data()[0], - e.what()); - } - - session.createPeer(peerAddress); - session.setPeerPreFilterMaxRoutes(peerAddress, maxRoutes); - session.saveConfig(); - - return fmt::format( - "Successfully set max-routes for peer '{}' to: {}\n" - "Config saved to: {}", - peerAddress, - maxRoutes, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerMaxRoutes::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerMaxRoutes, - CmdConfigProtocolBgpPeerMaxRoutesTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerMaxRoutes.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerMaxRoutes.h deleted file mode 100644 index 4cec005468d9b..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerMaxRoutes.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerMaxRoutesTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerMaxRoutes - : public CmdHandler< - CmdConfigProtocolBgpPeerMaxRoutes, - CmdConfigProtocolBgpPeerMaxRoutesTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpPeerMaxRoutesTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerMaxRoutesTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& maxRoutes); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop4.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop4.cpp deleted file mode 100644 index 2e0846c898931..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop4.cpp +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop4.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerNextHop4Traits::RetType -CmdConfigProtocolBgpPeerNextHop4::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& nextHop4Msg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (nextHop4Msg.data().empty()) { - return fmt::format( - "Error: next-hop4 value required for peer '{}'", peerAddress); - } - std::string nextHop4 = nextHop4Msg.data()[0]; - - session.createPeer(peerAddress); - session.setPeerNextHop4(peerAddress, nextHop4); - session.saveConfig(); - - return fmt::format( - "Successfully set next-hop4 for peer '{}' to: '{}'\n" - "Config saved to: {}", - peerAddress, - nextHop4, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerNextHop4::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerNextHop4, - CmdConfigProtocolBgpPeerNextHop4Traits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop4.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop4.h deleted file mode 100644 index 9f84c4b6813ab..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop4.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerNextHop4Traits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerNextHop4 - : public CmdHandler< - CmdConfigProtocolBgpPeerNextHop4, - CmdConfigProtocolBgpPeerNextHop4Traits> { - public: - using ObjectArgType = CmdConfigProtocolBgpPeerNextHop4Traits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerNextHop4Traits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& nextHop4); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop6.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop6.cpp deleted file mode 100644 index da3f6b4a57d9c..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop6.cpp +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop6.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerNextHop6Traits::RetType -CmdConfigProtocolBgpPeerNextHop6::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& nextHop6Msg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (nextHop6Msg.data().empty()) { - return fmt::format( - "Error: next-hop6 value required for peer '{}'", peerAddress); - } - std::string nextHop6 = nextHop6Msg.data()[0]; - - session.createPeer(peerAddress); - session.setPeerNextHop6(peerAddress, nextHop6); - session.saveConfig(); - - return fmt::format( - "Successfully set next-hop6 for peer '{}' to: '{}'\n" - "Config saved to: {}", - peerAddress, - nextHop6, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerNextHop6::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerNextHop6, - CmdConfigProtocolBgpPeerNextHop6Traits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop6.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop6.h deleted file mode 100644 index 6c7ba583d3743..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHop6.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerNextHop6Traits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerNextHop6 - : public CmdHandler< - CmdConfigProtocolBgpPeerNextHop6, - CmdConfigProtocolBgpPeerNextHop6Traits> { - public: - using ObjectArgType = CmdConfigProtocolBgpPeerNextHop6Traits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerNextHop6Traits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& nextHop6); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHopSelf.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHopSelf.cpp deleted file mode 100644 index 5e4f9d722770d..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHopSelf.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHopSelf.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerNextHopSelfTraits::RetType -CmdConfigProtocolBgpPeerNextHopSelf::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& enableMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (enableMsg.data().empty()) { - return fmt::format( - "Error: enable value (true/false) is required for peer '{}'", - peerAddress); - } - - std::string enableStr = enableMsg.data()[0]; - bool enable = (enableStr == "true" || enableStr == "1" || enableStr == "yes"); - - session.createPeer(peerAddress); - session.setPeerNextHopSelf(peerAddress, enable); - session.saveConfig(); - - return fmt::format( - "Successfully {} next-hop-self for peer '{}'\n" - "Config saved to: {}", - enable ? "enabled" : "disabled", - peerAddress, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerNextHopSelf::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerNextHopSelf, - CmdConfigProtocolBgpPeerNextHopSelfTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHopSelf.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHopSelf.h deleted file mode 100644 index 3fd450a707441..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerNextHopSelf.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerNextHopSelfTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerNextHopSelf - : public CmdHandler< - CmdConfigProtocolBgpPeerNextHopSelf, - CmdConfigProtocolBgpPeerNextHopSelfTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerNextHopSelfTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerNextHopSelfTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& enable); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPassive.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPassive.cpp deleted file mode 100644 index afdf292bac5d7..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPassive.cpp +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerPassive.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerPassiveTraits::RetType -CmdConfigProtocolBgpPeerPassive::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& enableMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (enableMsg.data().empty()) { - return fmt::format( - "Error: enable value required for peer '{}'", peerAddress); - } - std::string enableStr = enableMsg.data()[0]; - bool enable = (enableStr == "true" || enableStr == "1" || enableStr == "yes"); - - session.createPeer(peerAddress); - session.setPeerPassive(peerAddress, enable); - session.saveConfig(); - - return fmt::format( - "Successfully {} passive mode for peer '{}'\n" - "Config saved to: {}", - enable ? "enabled" : "disabled", - peerAddress, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerPassive::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerPassive, - CmdConfigProtocolBgpPeerPassiveTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPassive.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPassive.h deleted file mode 100644 index fc03203a62d81..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPassive.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerPassiveTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerPassive - : public CmdHandler< - CmdConfigProtocolBgpPeerPassive, - CmdConfigProtocolBgpPeerPassiveTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpPeerPassiveTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerPassiveTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& enable); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerGroup.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerGroup.cpp deleted file mode 100644 index 6d39e53a2ad90..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerGroup.cpp +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerGroup.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerPeerGroupTraits::RetType -CmdConfigProtocolBgpPeerPeerGroup::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& peerGroupNameMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - if (peerGroupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - - std::string peerAddress = peerAddressList.data()[0]; - std::string peerGroupName = peerGroupNameMsg.data()[0]; - - session.setPeerGroupName(peerAddress, peerGroupName); - session.saveConfig(); - - return fmt::format( - "Successfully set peer {} peer-group to: {}\nConfig saved to: {}", - peerAddress, - peerGroupName, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerPeerGroup::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerPeerGroup, - CmdConfigProtocolBgpPeerPeerGroupTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerGroup.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerGroup.h deleted file mode 100644 index 346af38f9ca42..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerGroup.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerPeerGroupTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerPeerGroup - : public CmdHandler< - CmdConfigProtocolBgpPeerPeerGroup, - CmdConfigProtocolBgpPeerPeerGroupTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpPeerPeerGroupTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerPeerGroupTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& peerGroupName); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerId.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerId.cpp deleted file mode 100644 index a4ac20d0e5ecd..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerId.cpp +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerId.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerPeerIdTraits::RetType -CmdConfigProtocolBgpPeerPeerId::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& peerIdMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (peerIdMsg.data().empty()) { - return fmt::format( - "Error: peer-id value is required for peer '{}'", peerAddress); - } - - session.createPeer(peerAddress); - session.setPeerPeerId(peerAddress, peerIdMsg.data()[0]); - session.saveConfig(); - - return fmt::format( - "Successfully set peer-id for peer '{}' to: '{}'\n" - "Config saved to: {}", - peerAddress, - peerIdMsg.data()[0], - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerPeerId::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerPeerId, - CmdConfigProtocolBgpPeerPeerIdTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerId.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerId.h deleted file mode 100644 index 2091ffe5a495a..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerPeerId.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerPeerIdTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerPeerId - : public CmdHandler< - CmdConfigProtocolBgpPeerPeerId, - CmdConfigProtocolBgpPeerPeerIdTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpPeerPeerIdTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerPeerIdTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& peerId); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRemoteAsn.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRemoteAsn.cpp deleted file mode 100644 index dbfb714a3b9e3..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRemoteAsn.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerRemoteAsn.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerRemoteAsnTraits::RetType -CmdConfigProtocolBgpPeerRemoteAsn::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& remoteAsnMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - - if (remoteAsnMsg.data().empty()) { - return "Error: remote-asn value is required"; - } - - // Extract the first IP from the list as peer address - std::string peerAddress = peerAddressList.data()[0]; - - // Extract string and parse to uint64_t (4-byte ASN per RFC 6793) - std::string asnStr = remoteAsnMsg.data()[0]; - uint64_t remoteAsn; - try { - remoteAsn = folly::to(asnStr); - } catch (const std::exception& e) { - return fmt::format( - "Error: Invalid remote-asn value '{}': {}", asnStr, e.what()); - } - - // Set the remote-asn for the peer (internally maps to remote_as_4_byte) - session.setPeerRemoteAsn(peerAddress, remoteAsn); - session.saveConfig(); - - return fmt::format( - "Successfully set BGP peer {} remote-asn to: {}\nConfig saved to: {}", - peerAddress, - remoteAsn, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerRemoteAsn::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerRemoteAsn, - CmdConfigProtocolBgpPeerRemoteAsnTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRemoteAsn.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRemoteAsn.h deleted file mode 100644 index 76059df98381a..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRemoteAsn.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerRemoteAsnTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerRemoteAsn - : public CmdHandler< - CmdConfigProtocolBgpPeerRemoteAsn, - CmdConfigProtocolBgpPeerRemoteAsnTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpPeerRemoteAsnTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerRemoteAsnTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& remoteAsn); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRrClient.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRrClient.cpp deleted file mode 100644 index 025e3a4d4c797..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRrClient.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerRrClient.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerRrClientTraits::RetType -CmdConfigProtocolBgpPeerRrClient::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& enableMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (enableMsg.data().empty()) { - return fmt::format( - "Error: enable value (true/false) is required for peer '{}'", - peerAddress); - } - - std::string enableStr = enableMsg.data()[0]; - bool enable = (enableStr == "true" || enableStr == "1" || enableStr == "yes"); - - session.createPeer(peerAddress); - session.setPeerRrClient(peerAddress, enable); - session.saveConfig(); - - return fmt::format( - "Successfully {} route-reflector-client for peer '{}'\n" - "Config saved to: {}", - enable ? "enabled" : "disabled", - peerAddress, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerRrClient::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerRrClient, - CmdConfigProtocolBgpPeerRrClientTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRrClient.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRrClient.h deleted file mode 100644 index 2eaba6e61b7c0..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerRrClient.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerRrClientTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerRrClient - : public CmdHandler< - CmdConfigProtocolBgpPeerRrClient, - CmdConfigProtocolBgpPeerRrClientTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpPeerRrClientTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerRrClientTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& enable); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.cpp deleted file mode 100644 index c4a43abf408b8..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.cpp +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerTimersTraits::RetType -CmdConfigProtocolBgpPeerTimers::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList) { - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - return fmt::format( - "BGP peer timers configuration for peer '{}'.\n" - "Available subcommands:\n" - " hold-time - Set hold time\n" - " keepalive - Set keepalive interval\n" - " out-delay - Set output delay\n" - " withdraw-unprog-delay - Set withdraw unprog delay", - peerAddress); -} - -void CmdConfigProtocolBgpPeerTimers::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerTimers, - CmdConfigProtocolBgpPeerTimersTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.h deleted file mode 100644 index a3fe71082ab57..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerTimersTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - using ObjectArgType = std::monostate; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerTimers - : public CmdHandler< - CmdConfigProtocolBgpPeerTimers, - CmdConfigProtocolBgpPeerTimersTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpPeerTimersTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerTimersTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersKeepalive.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersKeepalive.cpp deleted file mode 100644 index c6a582ebab8c9..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersKeepalive.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersKeepalive.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerTimersKeepaliveTraits::RetType -CmdConfigProtocolBgpPeerTimersKeepalive::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& keepaliveMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (keepaliveMsg.data().empty()) { - return fmt::format( - "Error: keepalive value (seconds) is required for peer '{}'", - peerAddress); - } - - int32_t keepalive; - try { - keepalive = std::stoi(keepaliveMsg.data()[0]); - } catch (const std::exception& e) { - return fmt::format( - "Error: invalid keepalive value '{}': {}", - keepaliveMsg.data()[0], - e.what()); - } - - session.createPeer(peerAddress); - session.setPeerKeepalive(peerAddress, keepalive); - session.saveConfig(); - - return fmt::format( - "Successfully set keepalive for peer '{}' to: {} seconds\n" - "Config saved to: {}", - peerAddress, - keepalive, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerTimersKeepalive::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerTimersKeepalive, - CmdConfigProtocolBgpPeerTimersKeepaliveTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersKeepalive.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersKeepalive.h deleted file mode 100644 index 8449f76d2d7fb..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersKeepalive.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerTimersKeepaliveTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerTimers; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerTimersKeepalive - : public CmdHandler< - CmdConfigProtocolBgpPeerTimersKeepalive, - CmdConfigProtocolBgpPeerTimersKeepaliveTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerTimersKeepaliveTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerTimersKeepaliveTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& keepalive); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersOutDelay.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersOutDelay.cpp deleted file mode 100644 index 46cb4cbe18168..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersOutDelay.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersOutDelay.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerTimersOutDelayTraits::RetType -CmdConfigProtocolBgpPeerTimersOutDelay::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& outDelayMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (outDelayMsg.data().empty()) { - return fmt::format( - "Error: out-delay value (seconds) is required for peer '{}'", - peerAddress); - } - - int32_t outDelay; - try { - outDelay = std::stoi(outDelayMsg.data()[0]); - } catch (const std::exception& e) { - return fmt::format( - "Error: invalid out-delay value '{}': {}", - outDelayMsg.data()[0], - e.what()); - } - - session.createPeer(peerAddress); - session.setPeerOutDelay(peerAddress, outDelay); - session.saveConfig(); - - return fmt::format( - "Successfully set out-delay for peer '{}' to: {} seconds\n" - "Config saved to: {}", - peerAddress, - outDelay, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerTimersOutDelay::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerTimersOutDelay, - CmdConfigProtocolBgpPeerTimersOutDelayTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersOutDelay.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersOutDelay.h deleted file mode 100644 index a4817268b16a1..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersOutDelay.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerTimersOutDelayTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerTimers; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerTimersOutDelay - : public CmdHandler< - CmdConfigProtocolBgpPeerTimersOutDelay, - CmdConfigProtocolBgpPeerTimersOutDelayTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerTimersOutDelayTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerTimersOutDelayTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& outDelay); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay.cpp deleted file mode 100644 index ff95a1dce0d78..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelayTraits::RetType -CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& delayMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (delayMsg.data().empty()) { - return fmt::format( - "Error: withdraw-unprog-delay value (seconds) is required for peer '{}'", - peerAddress); - } - - int32_t delay; - try { - delay = std::stoi(delayMsg.data()[0]); - } catch (const std::exception& e) { - return fmt::format( - "Error: invalid withdraw-unprog-delay value '{}': {}", - delayMsg.data()[0], - e.what()); - } - - session.createPeer(peerAddress); - session.setPeerWithdrawUnprogDelay(peerAddress, delay); - session.saveConfig(); - - return fmt::format( - "Successfully set withdraw-unprog-delay for peer '{}' to: {} seconds\n" - "Config saved to: {}", - peerAddress, - delay, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay, - CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelayTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay.h deleted file mode 100644 index 47b28cd791ce7..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay.h +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerTimers.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelayTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerTimers; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay - : public CmdHandler< - CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelay, - CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelayTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelayTraits::ObjectArgType; - using RetType = - CmdConfigProtocolBgpPeerTimersWithdrawUnprogDelayTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& withdrawUnprogDelay); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerType.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerType.cpp deleted file mode 100644 index af460feecbf90..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerType.cpp +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerType.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerTypeTraits::RetType -CmdConfigProtocolBgpPeerType::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& peerTypeMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (peerTypeMsg.data().empty()) { - return fmt::format( - "Error: type value is required for peer '{}'", peerAddress); - } - - session.createPeer(peerAddress); - session.setPeerType(peerAddress, peerTypeMsg.data()[0]); - session.saveConfig(); - - return fmt::format( - "Successfully set type for peer '{}' to: '{}'\n" - "Config saved to: {}", - peerAddress, - peerTypeMsg.data()[0], - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerType::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerType, - CmdConfigProtocolBgpPeerTypeTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerType.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerType.h deleted file mode 100644 index 8b5a593677bf1..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerType.h +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerTypeTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerType : public CmdHandler< - CmdConfigProtocolBgpPeerType, - CmdConfigProtocolBgpPeerTypeTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpPeerTypeTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerTypeTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& peerType); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerV4OverV6Nh.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerV4OverV6Nh.cpp deleted file mode 100644 index 94c3500946321..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerV4OverV6Nh.cpp +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerV4OverV6Nh.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerV4OverV6NhTraits::RetType -CmdConfigProtocolBgpPeerV4OverV6Nh::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& enableMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (enableMsg.data().empty()) { - return fmt::format( - "Error: enable value required for peer '{}'", peerAddress); - } - std::string enableStr = enableMsg.data()[0]; - bool enable = (enableStr == "true" || enableStr == "1" || enableStr == "yes"); - - session.createPeer(peerAddress); - session.setPeerIpv4OverIpv6Nh(peerAddress, enable); - session.saveConfig(); - - return fmt::format( - "Successfully {} v4-over-v6-nexthop for peer '{}'\n" - "Config saved to: {}", - enable ? "enabled" : "disabled", - peerAddress, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerV4OverV6Nh::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerV4OverV6Nh, - CmdConfigProtocolBgpPeerV4OverV6NhTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerV4OverV6Nh.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerV4OverV6Nh.h deleted file mode 100644 index af68ab9d07c62..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerV4OverV6Nh.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerV4OverV6NhTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerV4OverV6Nh - : public CmdHandler< - CmdConfigProtocolBgpPeerV4OverV6Nh, - CmdConfigProtocolBgpPeerV4OverV6NhTraits> { - public: - using ObjectArgType = CmdConfigProtocolBgpPeerV4OverV6NhTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerV4OverV6NhTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& enable); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningLimit.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningLimit.cpp deleted file mode 100644 index 6b361f89e0e25..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningLimit.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningLimit.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerWarningLimitTraits::RetType -CmdConfigProtocolBgpPeerWarningLimit::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& warningLimitMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (warningLimitMsg.data().empty()) { - return fmt::format( - "Error: warning-limit value is required for peer '{}'", peerAddress); - } - - int64_t warningLimit; - try { - warningLimit = std::stoll(warningLimitMsg.data()[0]); - } catch (const std::exception& e) { - return fmt::format( - "Error: invalid warning-limit value '{}': {}", - warningLimitMsg.data()[0], - e.what()); - } - - session.createPeer(peerAddress); - session.setPeerPreFilterWarningLimit(peerAddress, warningLimit); - session.saveConfig(); - - return fmt::format( - "Successfully set pre_filter warning_limit for peer '{}' to: {}\n" - "Config saved to: {}", - peerAddress, - warningLimit, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerWarningLimit::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerWarningLimit, - CmdConfigProtocolBgpPeerWarningLimitTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningLimit.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningLimit.h deleted file mode 100644 index 97dbae5059eab..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningLimit.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerWarningLimitTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerWarningLimit - : public CmdHandler< - CmdConfigProtocolBgpPeerWarningLimit, - CmdConfigProtocolBgpPeerWarningLimitTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerWarningLimitTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerWarningLimitTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& warningLimit); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningOnly.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningOnly.cpp deleted file mode 100644 index 2871824d4475c..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningOnly.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningOnly.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerWarningOnlyTraits::RetType -CmdConfigProtocolBgpPeerWarningOnly::queryClient( - const HostInfo& /* hostInfo */, - const utils::IPList& peerAddressList, - const ObjectArgType& warningOnlyMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerAddressList.data().empty()) { - return "Error: peer address is required"; - } - std::string peerAddress = peerAddressList.data()[0]; - - if (warningOnlyMsg.data().empty()) { - return fmt::format( - "Error: warning-only value (true|false) is required for peer '{}'", - peerAddress); - } - - std::string value = warningOnlyMsg.data()[0]; - bool warningOnly; - if (value == "true" || value == "1") { - warningOnly = true; - } else if (value == "false" || value == "0") { - warningOnly = false; - } else { - return fmt::format( - "Error: invalid warning-only value '{}', expected 'true' or 'false'", - value); - } - - session.createPeer(peerAddress); - session.setPeerPreFilterWarningOnly(peerAddress, warningOnly); - session.saveConfig(); - - return fmt::format( - "Successfully set pre_filter warning_only for peer '{}' to: {}\n" - "Config saved to: {}", - peerAddress, - warningOnly ? "true" : "false", - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerWarningOnly::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerWarningOnly, - CmdConfigProtocolBgpPeerWarningOnlyTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningOnly.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningOnly.h deleted file mode 100644 index 22f27198597dd..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeerWarningOnly.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer/CmdConfigProtocolBgpPeer.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerWarningOnlyTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeer; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerWarningOnly - : public CmdHandler< - CmdConfigProtocolBgpPeerWarningOnly, - CmdConfigProtocolBgpPeerWarningOnlyTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerWarningOnlyTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerWarningOnlyTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::IPList& peerAddress, - const ObjectArgType& warningOnly); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.cpp b/fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.cpp new file mode 100644 index 0000000000000..26fbfb3f972cb --- /dev/null +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.cpp @@ -0,0 +1,32 @@ +/* + * 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/delete/protocol/bgp/CmdDeleteProtocolBgp.h" +#include +#include + +#include "fboss/cli/fboss2/CmdHandler.cpp" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +namespace facebook::fboss { + +CmdDeleteProtocolBgpTraits::RetType CmdDeleteProtocolBgp::queryClient( + const HostInfo& /* hostInfo */) { + return "Delete BGP configuration objects. Use subcommands: neighbor"; +} + +void CmdDeleteProtocolBgp::printOutput(const RetType& output) { + std::cout << output << std::endl; +} + +template void +CmdHandler::run(); + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h b/fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h new file mode 100644 index 0000000000000..a79dfc13580fd --- /dev/null +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h @@ -0,0 +1,41 @@ +/* + * 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 +#include +#include "fboss/cli/fboss2/CmdHandler.h" +#include "fboss/cli/fboss2/commands/delete/protocol/CmdDeleteProtocol.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +namespace facebook::fboss { + +struct CmdDeleteProtocolBgpTraits : public WriteCommandTraits { + using ParentCmd = CmdDeleteProtocol; + static constexpr utils::ObjectArgTypeId ObjectArgTypeId = + utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_NONE; + using ObjectArgType = std::monostate; + using RetType = std::string; +}; + +class CmdDeleteProtocolBgp + : public CmdHandler { + public: + using ObjectArgType = CmdDeleteProtocolBgpTraits::ObjectArgType; + using RetType = CmdDeleteProtocolBgpTraits::RetType; + + RetType queryClient(const HostInfo& hostInfo); + + void printOutput(const RetType& output); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.cpp b/fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.cpp new file mode 100644 index 0000000000000..ff337a33e0aeb --- /dev/null +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.cpp @@ -0,0 +1,77 @@ +/* + * 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/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h" + +#include "fboss/cli/fboss2/CmdHandler.cpp" + +#include +#include +#include +#include +#include +#include +#include +#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpPeerAddr.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" +#include "fmt/format.h" + +namespace facebook::fboss { + +// Parse + validate at construction so queryClient stays a thin dispatch. +// Throwing std::invalid_argument is how the framework surfaces arg parse +// errors (same mechanism as BgpNeighborConfig). +BgpNeighborRef::BgpNeighborRef(std::vector v) + : utils::BaseObjectArgType(v) { + if (v.size() != 1) { + throw std::invalid_argument( + "Error: delete protocol bgp neighbor requires exactly one " + ""); + } + auto normalized = bgpcli::normalizeBgpPeerAddr(v[0]); + if (!normalized) { + throw std::invalid_argument( + fmt::format("Error: Invalid neighbor address '{}'", v[0])); + } + peerAddr_ = std::move(*normalized); +} + +CmdDeleteProtocolBgpNeighborTraits::RetType +CmdDeleteProtocolBgpNeighbor::queryClient( + const HostInfo& /* hostInfo */, + const ObjectArgType& args) { + auto& session = ConfigSession::getInstance(); + auto& cfg = session.getBgpConfig(); + auto it = bgpcli::findBgpPeer(cfg, args.peerAddr()); + if (it == cfg.peers()->end()) { + // Nothing is persisted for an unknown neighbor, so a typo'd delete can't + // stage an unrelated session change. + return fmt::format("Error: BGP neighbor {} not found", args.peerAddr()); + } + cfg.peers()->erase(it); + session.saveBgpConfig(); + return fmt::format( + "Successfully deleted BGP neighbor {}\nConfig saved to: {}", + args.peerAddr(), + session.getBgpSessionConfigPath()); +} + +void CmdDeleteProtocolBgpNeighbor::printOutput(const RetType& output) { + std::cout << output << std::endl; +} + +// Explicit template instantiation +template void CmdHandler< + CmdDeleteProtocolBgpNeighbor, + CmdDeleteProtocolBgpNeighborTraits>::run(); + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h b/fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h new file mode 100644 index 0000000000000..fa71727255c67 --- /dev/null +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h @@ -0,0 +1,61 @@ +/* + * 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 +#include +#include "CLI/App.hpp" +#include "fboss/cli/fboss2/CmdHandler.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +namespace facebook::fboss { + +// Parsed `delete protocol bgp neighbor `, validated at +// construction. The address is normalized the same way the config command +// normalizes it, so any spelling of a peer's address deletes that peer. +class BgpNeighborRef : public utils::BaseObjectArgType { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + /* implicit */ BgpNeighborRef(std::vector v); + const std::string& peerAddr() const { + return peerAddr_; + } + const static utils::ObjectArgTypeId id = + utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_MESSAGE; + + private: + std::string peerAddr_; +}; + +struct CmdDeleteProtocolBgpNeighborTraits : public WriteCommandTraits { + using ParentCmd = CmdDeleteProtocolBgp; + static void addCliArg(CLI::App& cmd, std::vector& args) { + cmd.add_option("args", args, ""); + } + using ObjectArgType = BgpNeighborRef; + using RetType = std::string; +}; + +class CmdDeleteProtocolBgpNeighbor : public CmdHandler< + CmdDeleteProtocolBgpNeighbor, + CmdDeleteProtocolBgpNeighborTraits> { + public: + using ObjectArgType = CmdDeleteProtocolBgpNeighborTraits::ObjectArgType; + using RetType = CmdDeleteProtocolBgpNeighborTraits::RetType; + + RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& args); + + void printOutput(const RetType& output); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/docs/BGP_CLI_TEST_PLAN.md b/fboss/cli/fboss2/docs/BGP_CLI_TEST_PLAN.md index 65cb71b350d0c..ebb8ad6486105 100644 --- a/fboss/cli/fboss2/docs/BGP_CLI_TEST_PLAN.md +++ b/fboss/cli/fboss2/docs/BGP_CLI_TEST_PLAN.md @@ -79,10 +79,10 @@ fboss2 config protocol bgp peer-group RSW-FSW-V6 timers keepalive 10 fboss2 config protocol bgp peer-group RSW-RTSW-V6 disable-ipv4-afi true # Peer configuration (with explicit disable_ipv4_afi: false preserved) -fboss2 config protocol bgp peer 2401:db00:501c::/64 remote-asn 65000 -fboss2 config protocol bgp peer 2401:db00:501c::/64 local-addr 2401:db00:501c::a -fboss2 config protocol bgp peer 2401:db00:501c::/64 passive true -fboss2 config protocol bgp peer 2401:db00:501c::/64 disable-ipv4-afi false +fboss2 config protocol bgp neighbor 2401:db00:501c::/64 remote-asn 65000 +fboss2 config protocol bgp neighbor 2401:db00:501c::/64 bind-addr address 2401:db00:501c::a +fboss2 config protocol bgp neighbor 2401:db00:501c::/64 passive true +fboss2 config protocol bgp neighbor 2401:db00:501c::/64 afi disable-ipv4-afi false ``` ### Step 5: Execute Script @@ -136,12 +136,13 @@ diff <(jq -S . /tmp/rsw_bgp_nao.txt) <(jq -S . ~/.fboss2/bgp_config.json) | Peer Group | `config protocol bgp peer-group timers hold-time ` | Set hold time | | Peer Group | `config protocol bgp peer-group timers keepalive ` | Set keepalive interval | | Peer Group | `config protocol bgp peer-group max-routes ` | Set max routes | -| Peer | `config protocol bgp peer remote-asn ` | Set remote AS | -| Peer | `config protocol bgp peer peer-group ` | Assign to peer group | -| Peer | `config protocol bgp peer local-addr ` | Set local address | -| Peer | `config protocol bgp peer passive ` | Set passive mode | -| Peer | `config protocol bgp peer disable-ipv4-afi ` | Disable IPv4 AFI | -| Peer | `config protocol bgp peer link-bandwidth ` | Set link bandwidth | +| Neighbor | `config protocol bgp neighbor remote-asn ` | Set remote AS | +| Neighbor | `config protocol bgp neighbor peer-group ` | Assign to peer group | +| Neighbor | `config protocol bgp neighbor bind-addr address ` | Set local address | +| Neighbor | `config protocol bgp neighbor passive ` | Listen-only (true) or listen-and-connect (false) session | +| Neighbor | `delete protocol bgp neighbor ` | Delete a neighbor | +| Neighbor | `config protocol bgp neighbor afi disable-ipv4-afi ` | Disable IPv4 AFI | +| Neighbor | `config protocol bgp neighbor link-bandwidth ` | Set link bandwidth | ## Automated Test Script diff --git a/fboss/cli/fboss2/test/config/BUCK b/fboss/cli/fboss2/test/config/BUCK index a1c201b2a3c40..e1991b9a16b35 100644 --- a/fboss/cli/fboss2/test/config/BUCK +++ b/fboss/cli/fboss2/test/config/BUCK @@ -12,6 +12,7 @@ cpp_unittest( "CmdConfigAppliedInfoTest.cpp", "CmdConfigArpTest.cpp", "CmdConfigBgpGlobalTest.cpp", + "CmdConfigBgpNeighborTest.cpp", "CmdConfigCoppTest.cpp", "CmdConfigDhcpTest.cpp", "CmdConfigHistoryTest.cpp", @@ -45,6 +46,7 @@ cpp_unittest( "CmdConfigVlanTest.cpp", "CmdDeleteAclRuleTest.cpp", "CmdDeleteArpTest.cpp", + "CmdDeleteBgpNeighborTest.cpp", "CmdDeleteConfigInterfaceTest.cpp", "CmdDeleteCoppQueueTest.cpp", "CmdDeleteCoppReasonTest.cpp", diff --git a/fboss/cli/fboss2/test/config/BgpConfigSessionTest.cpp b/fboss/cli/fboss2/test/config/BgpConfigSessionTest.cpp index da9d2698aa0b8..e2d6d848d3310 100644 --- a/fboss/cli/fboss2/test/config/BgpConfigSessionTest.cpp +++ b/fboss/cli/fboss2/test/config/BgpConfigSessionTest.cpp @@ -2,11 +2,11 @@ #include "fboss/cli/fboss2/test/config/CmdConfigTestBase.h" -#include // NOLINT(misc-include-cleaner) #include #include #include +#include #include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" @@ -199,160 +199,6 @@ TEST_F(BgpConfigSessionTest, addMultipleNetworks6) { EXPECT_FALSE(config["networks6"][1]["install_to_fib"].asBool()); } -// ==================== Peer Configuration Tests ==================== - -TEST_F(BgpConfigSessionTest, createPeerAndSetRemoteAsn) { - session_->createPeer("2001:db8::1"); - session_->setPeerRemoteAsn("2001:db8::1", 65001); - - auto& config = session_->getBgpConfig(); - ASSERT_TRUE(config.count("peers")); - ASSERT_EQ(config["peers"].size(), 1); - - auto& peer = config["peers"][0]; - EXPECT_EQ(peer["peer_addr"].asString(), "2001:db8::1"); - EXPECT_EQ(peer["remote_as_4_byte"].asInt(), 65001); -} - -TEST_F(BgpConfigSessionTest, setPeerDescription) { - session_->createPeer("10.0.0.1"); - session_->setPeerDescription("10.0.0.1", "Test peer description"); - - auto& config = session_->getBgpConfig(); - EXPECT_EQ( - config["peers"][0]["description"].asString(), "Test peer description"); -} - -TEST_F(BgpConfigSessionTest, setPeerPeerGroupName) { - session_->createPeer("2001:db8::1"); - session_->setPeerGroupName("2001:db8::1", "RSW-FSW-V6"); - - auto& config = session_->getBgpConfig(); - EXPECT_EQ(config["peers"][0]["peer_group_name"].asString(), "RSW-FSW-V6"); -} - -TEST_F(BgpConfigSessionTest, setPeerDisableIpv4AfiTrue) { - session_->createPeer("2001:db8::1"); - session_->setPeerDisableIpv4Afi("2001:db8::1", true); - - auto& config = session_->getBgpConfig(); - EXPECT_TRUE(config["peers"][0]["disable_ipv4_afi"].asBool()); -} - -TEST_F(BgpConfigSessionTest, setPeerDisableIpv4AfiFalse) { - // Critical test: Verify that disable_ipv4_afi: false is preserved - session_->createPeer("2001:db8::1"); - session_->setPeerDisableIpv4Afi("2001:db8::1", false); - - auto& config = session_->getBgpConfig(); - // Must be explicitly set to false, not omitted - ASSERT_TRUE(config["peers"][0].count("disable_ipv4_afi")); - EXPECT_FALSE(config["peers"][0]["disable_ipv4_afi"].asBool()); -} - -TEST_F(BgpConfigSessionTest, setPeerNextHopSelf) { - session_->createPeer("2001:db8::1"); - session_->setPeerNextHopSelf("2001:db8::1", true); - - auto& config = session_->getBgpConfig(); - EXPECT_TRUE(config["peers"][0]["next_hop_self"].asBool()); -} - -TEST_F(BgpConfigSessionTest, setPeerConfedPeer) { - session_->createPeer("2001:db8::1"); - session_->setPeerConfedPeer("2001:db8::1", true); - - auto& config = session_->getBgpConfig(); - EXPECT_TRUE(config["peers"][0]["is_confed_peer"].asBool()); -} - -TEST_F(BgpConfigSessionTest, setPeerPassive) { - session_->createPeer("2001:db8::1"); - session_->setPeerPassive("2001:db8::1", true); - - auto& config = session_->getBgpConfig(); - EXPECT_TRUE(config["peers"][0]["is_passive"].asBool()); -} - -TEST_F(BgpConfigSessionTest, setPeerRrClient) { - session_->createPeer("2001:db8::1"); - session_->setPeerRrClient("2001:db8::1", true); - - auto& config = session_->getBgpConfig(); - EXPECT_TRUE(config["peers"][0]["is_rr_client"].asBool()); -} - -TEST_F(BgpConfigSessionTest, setPeerTimers) { - session_->createPeer("2001:db8::1"); - session_->setPeerHoldTime("2001:db8::1", 90); - session_->setPeerKeepalive("2001:db8::1", 30); - session_->setPeerOutDelay("2001:db8::1", 5); - - auto& config = session_->getBgpConfig(); - ASSERT_TRUE(config["peers"][0].count("bgp_peer_timers")); - - auto& timers = config["peers"][0]["bgp_peer_timers"]; - EXPECT_EQ(timers["hold_time_seconds"].asInt(), 90); - EXPECT_EQ(timers["keep_alive_seconds"].asInt(), 30); - EXPECT_EQ(timers["out_delay_seconds"].asInt(), 5); -} - -TEST_F(BgpConfigSessionTest, setPeerPolicies) { - session_->createPeer("2001:db8::1"); - session_->setPeerIngressPolicy("2001:db8::1", "INGRESS_POLICY"); - session_->setPeerEgressPolicy("2001:db8::1", "EGRESS_POLICY"); - - auto& config = session_->getBgpConfig(); - EXPECT_EQ( - config["peers"][0]["ingress_policy_name"].asString(), "INGRESS_POLICY"); - EXPECT_EQ( - config["peers"][0]["egress_policy_name"].asString(), "EGRESS_POLICY"); -} - -TEST_F(BgpConfigSessionTest, setPeerPreFilterMaxRoutes) { - session_->createPeer("2001:db8::1"); - session_->setPeerPreFilterMaxRoutes("2001:db8::1", 1000); - - auto& config = session_->getBgpConfig(); - ASSERT_TRUE(config["peers"][0].count("pre_filter")); - EXPECT_EQ(config["peers"][0]["pre_filter"]["max_routes"].asInt(), 1000); -} - -TEST_F(BgpConfigSessionTest, setPeerLinkBandwidth) { - session_->createPeer("2001:db8::1"); - session_->setPeerAdvertiseLbw("2001:db8::1", true); - session_->setPeerLbwValue("2001:db8::1", 10000000000); - - auto& config = session_->getBgpConfig(); - EXPECT_TRUE(config["peers"][0]["advertise_link_bandwidth"].asBool()); - EXPECT_EQ(config["peers"][0]["link_bandwidth_bps"].asInt(), 10000000000); -} - -TEST_F(BgpConfigSessionTest, setPeerIpv4OverIpv6Nh) { - session_->createPeer("2001:db8::1"); - session_->setPeerIpv4OverIpv6Nh("2001:db8::1", true); - - auto& config = session_->getBgpConfig(); - EXPECT_TRUE(config["peers"][0]["v4_over_v6_nexthop"].asBool()); -} - -TEST_F(BgpConfigSessionTest, setPeerPeerId) { - session_->createPeer("2001:db8::1"); - session_->setPeerPeerId("2001:db8::1", "fsw001.p001.f01.qzd1:v6:1"); - - auto& config = session_->getBgpConfig(); - EXPECT_EQ( - config["peers"][0]["peer_id"].asString(), "fsw001.p001.f01.qzd1:v6:1"); -} - -TEST_F(BgpConfigSessionTest, setPeerType) { - session_->createPeer("2001:db8::1"); - session_->setPeerType("2001:db8::1", "FSW"); - - auto& config = session_->getBgpConfig(); - EXPECT_EQ(config["peers"][0]["type"].asString(), "FSW"); -} - // ==================== Peer Group Configuration Tests ==================== TEST_F(BgpConfigSessionTest, createPeerGroupAndSetDescription) { @@ -480,14 +326,6 @@ TEST_F(BgpConfigSessionTest, fullRswConfigurationWorkflow) { session_->createPeerGroup("RSW-RTSW-V6"); session_->setPeerGroupDisableIpv4Afi("RSW-RTSW-V6", true); - // Create peer with peer_group_name - session_->createPeer("2401:db00:e50e:1300::20"); - session_->setPeerRemoteAsn("2401:db00:e50e:1300::20", 65000); - session_->setPeerGroupName("2401:db00:e50e:1300::20", "RSW-FSW-V6"); - session_->setPeerDisableIpv4Afi("2401:db00:e50e:1300::20", false); - session_->setPeerPeerId( - "2401:db00:e50e:1300::20", "fsw001.p001.f01.qzd1:v6:1"); - // Verify the complete configuration auto& config = session_->getBgpConfig(); @@ -533,17 +371,6 @@ TEST_F(BgpConfigSessionTest, fullRswConfigurationWorkflow) { } EXPECT_TRUE(foundFswGroup); EXPECT_TRUE(foundRtswGroup); - - // Verify peer - ASSERT_EQ(config["peers"].size(), 1); - auto& peer = config["peers"][0]; - EXPECT_EQ(peer["peer_addr"].asString(), "2401:db00:e50e:1300::20"); - EXPECT_EQ(peer["remote_as_4_byte"].asInt(), 65000); - EXPECT_EQ(peer["peer_group_name"].asString(), "RSW-FSW-V6"); - // Critical: disable_ipv4_afi: false must be preserved - ASSERT_TRUE(peer.count("disable_ipv4_afi")); - EXPECT_FALSE(peer["disable_ipv4_afi"].asBool()); - EXPECT_EQ(peer["peer_id"].asString(), "fsw001.p001.f01.qzd1:v6:1"); } TEST_F(BgpConfigSessionTest, exportConfigProducesValidJson) { @@ -551,8 +378,6 @@ TEST_F(BgpConfigSessionTest, exportConfigProducesValidJson) { session_->setLocalAsn(65000); session_->addNetwork6("2001:db8::/32", "TEST_POLICY", true, 0); session_->createPeerGroup("TEST-GROUP"); - session_->createPeer("2001:db8::1"); - session_->setPeerRemoteAsn("2001:db8::1", 65001); std::string jsonStr = session_->exportConfig(); @@ -563,41 +388,6 @@ TEST_F(BgpConfigSessionTest, exportConfigProducesValidJson) { EXPECT_EQ(parsed["local_as_4_byte"].asInt(), 65000); EXPECT_EQ(parsed["networks6"].size(), 1); EXPECT_EQ(parsed["peer_groups"].size(), 1); - EXPECT_EQ(parsed["peers"].size(), 1); -} - -TEST_F(BgpConfigSessionTest, multiplePeersWithDifferentConfigs) { - // Create multiple peers with different configurations - session_->createPeer("2001:db8::1"); - session_->setPeerRemoteAsn("2001:db8::1", 65001); - session_->setPeerDisableIpv4Afi("2001:db8::1", true); - session_->setPeerNextHopSelf("2001:db8::1", true); - - session_->createPeer("2001:db8::2"); - session_->setPeerRemoteAsn("2001:db8::2", 65002); - session_->setPeerDisableIpv4Afi("2001:db8::2", false); - session_->setPeerPassive("2001:db8::2", true); - - session_->createPeer("10.0.0.1"); - session_->setPeerRemoteAsn("10.0.0.1", 65003); - session_->setPeerRrClient("10.0.0.1", true); - - auto& config = session_->getBgpConfig(); - ASSERT_EQ(config["peers"].size(), 3); - - // Find each peer and verify its config - for (const auto& peer : config["peers"]) { - std::string addr = peer["peer_addr"].asString(); - if (addr == "2001:db8::1") { - EXPECT_TRUE(peer["disable_ipv4_afi"].asBool()); - EXPECT_TRUE(peer["next_hop_self"].asBool()); - } else if (addr == "2001:db8::2") { - EXPECT_FALSE(peer["disable_ipv4_afi"].asBool()); - EXPECT_TRUE(peer["is_passive"].asBool()); - } else if (addr == "10.0.0.1") { - EXPECT_TRUE(peer["is_rr_client"].asBool()); - } - } } TEST_F(BgpConfigSessionTest, stubModePreventsSave) { @@ -615,7 +405,6 @@ TEST_F(BgpConfigSessionTest, stubModeCanClearSession) { // In stub mode, clearSession() should not throw // The actual clearing behavior depends on implementation details session_->setRouterId("10.0.0.1"); - session_->createPeer("2001:db8::1"); EXPECT_EQ(session_->getRouterId(), "10.0.0.1"); @@ -625,24 +414,6 @@ TEST_F(BgpConfigSessionTest, stubModeCanClearSession) { // ==================== Edge Case Tests ==================== -TEST_F(BgpConfigSessionTest, updateExistingPeer) { - // Create peer and set initial values - session_->createPeer("2001:db8::1"); - session_->setPeerRemoteAsn("2001:db8::1", 65001); - session_->setPeerDescription("2001:db8::1", "Initial description"); - - // Update the same peer - session_->setPeerRemoteAsn("2001:db8::1", 65002); - session_->setPeerDescription("2001:db8::1", "Updated description"); - - auto& config = session_->getBgpConfig(); - // Should still only have one peer - ASSERT_EQ(config["peers"].size(), 1); - EXPECT_EQ(config["peers"][0]["remote_as_4_byte"].asInt(), 65002); - EXPECT_EQ( - config["peers"][0]["description"].asString(), "Updated description"); -} - TEST_F(BgpConfigSessionTest, updateExistingPeerGroup) { session_->createPeerGroup("TEST-GROUP"); session_->setPeerGroupDescription("TEST-GROUP", "Initial"); @@ -654,39 +425,10 @@ TEST_F(BgpConfigSessionTest, updateExistingPeerGroup) { EXPECT_EQ(config["peer_groups"][0]["description"].asString(), "Updated"); } -TEST_F(BgpConfigSessionTest, ipv4PeerAddress) { - // Test with IPv4 peer address - session_->createPeer("192.168.1.1"); - session_->setPeerRemoteAsn("192.168.1.1", 65000); - - auto& config = session_->getBgpConfig(); - EXPECT_EQ(config["peers"][0]["peer_addr"].asString(), "192.168.1.1"); -} - -TEST_F(BgpConfigSessionTest, specialCharactersInDescription) { - session_->createPeer("2001:db8::1"); - session_->setPeerDescription( - "2001:db8::1", "Test \"quotes\" and 'apostrophes' and special: !@#$%"); - - auto& config = session_->getBgpConfig(); - EXPECT_EQ( - config["peers"][0]["description"].asString(), - "Test \"quotes\" and 'apostrophes' and special: !@#$%"); -} - TEST_F(BgpConfigSessionTest, largeAsnNumber) { // Test with 4-byte ASN session_->setLocalAsn(4200000000); EXPECT_EQ(session_->getLocalAsn(), 4200000000); } -TEST_F(BgpConfigSessionTest, largeLinkBandwidth) { - // Test with 100G bandwidth - session_->createPeer("2001:db8::1"); - session_->setPeerLbwValue("2001:db8::1", 100000000000); - - auto& config = session_->getBgpConfig(); - EXPECT_EQ(config["peers"][0]["link_bandwidth_bps"].asInt(), 100000000000); -} - } // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/config/CmdConfigBgpNeighborTest.cpp b/fboss/cli/fboss2/test/config/CmdConfigBgpNeighborTest.cpp new file mode 100644 index 0000000000000..00322dec76dcd --- /dev/null +++ b/fboss/cli/fboss2/test/config/CmdConfigBgpNeighborTest.cpp @@ -0,0 +1,540 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "configerator/structs/neteng/fboss/bgp/if/gen-cpp2/bgp_attr_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/test/config/CmdConfigTestBase.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +using namespace ::testing; +using facebook::neteng::fboss::bgp_attr::AddPath; + +namespace facebook::fboss { + +// The neighbor dispatcher only touches the BGP side of ConfigSession, which +// seeds from thrift schema defaults when neither a staged session nor a +// system bgpcpp.conf exists — so no seed agent config is needed (mirrors +// BgpConfigSessionTest). +class CmdConfigBgpNeighborTestFixture : public CmdConfigTestBase { + public: + CmdConfigBgpNeighborTestFixture() + : CmdConfigTestBase("bgp_neighbor_test_%%%%-%%%%-%%%%", "") {} + + void SetUp() override { + CmdConfigTestBase::SetUp(); + setupTestableConfigSession(); + } + + std::string run(const std::vector& tokens) { + CmdConfigProtocolBgpNeighbor cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpNeighborConfig(tokens)); + } + + const std::vector& peers() { + return *ConfigSession::getInstance().getBgpConfig().peers(); + } + + bgp::thrift::BgpConfig& config() { + return ConfigSession::getInstance().getBgpConfig(); + } + + // Stage a peer group directly in the session config (the peer-group command + // is out of scope here); returns a reference valid until the next insert. + bgp::thrift::PeerGroup& addPeerGroup(const std::string& name) { + auto& groups = config().peer_groups().ensure(); + groups.emplace_back(); + groups.back().name() = name; + return groups.back(); + } + + bool sessionFileExists() { + return std::filesystem::exists( + ConfigSession::getInstance().getBgpSessionConfigPath()); + } +}; + +// ============================================================================== +// BgpNeighborConfig (arg) validation +// ============================================================================== + +TEST_F(CmdConfigBgpNeighborTestFixture, argValidation) { + // Bare create. + EXPECT_EQ(BgpNeighborConfig({"10.0.0.1"}).peerAddr(), "10.0.0.1"); + EXPECT_TRUE(BgpNeighborConfig({"10.0.0.1"}).attr().empty()); + + // Address normalization. + EXPECT_EQ(BgpNeighborConfig({"2001:DB8::1"}).peerAddr(), "2001:db8::1"); + + // Prefix notation (passive listening sessions); host bits are cleared the + // way bgpd keys dynamic peers, so spellings that differ only there agree. + EXPECT_EQ(BgpNeighborConfig({"2001:db8::/64"}).peerAddr(), "2001:db8::/64"); + EXPECT_EQ(BgpNeighborConfig({"10.0.0.1/24"}).peerAddr(), "10.0.0.0/24"); + EXPECT_EQ( + BgpNeighborConfig({"2001:db8::1234/64"}).peerAddr(), "2001:db8::/64"); + + // Single-token attribute. + auto singleTok = BgpNeighborConfig({"10.0.0.1", "remote-asn", "65000"}); + EXPECT_EQ(singleTok.attr(), "remote-asn"); + EXPECT_EQ(singleTok.values(), std::vector({"65000"})); + + // Two-token attribute wins longest-prefix match. + auto twoTok = BgpNeighborConfig({"10.0.0.1", "timers", "hold-time", "30"}); + EXPECT_EQ(twoTok.attr(), "timers hold-time"); + EXPECT_EQ(twoTok.values(), std::vector({"30"})); + + // Invalid: empty, bad address, unknown attribute, incomplete group. + EXPECT_THROW(BgpNeighborConfig({}), std::invalid_argument); + EXPECT_THROW(BgpNeighborConfig({"not-an-ip"}), std::invalid_argument); + EXPECT_THROW( + BgpNeighborConfig({"10.0.0.1", "no-such-attr", "1"}), + std::invalid_argument); + EXPECT_THROW( + BgpNeighborConfig({"10.0.0.1", "timers"}), std::invalid_argument); +} + +// ============================================================================== +// Attribute handlers +// ============================================================================== + +TEST_F(CmdConfigBgpNeighborTestFixture, bareCreate) { + auto result = run({"10.0.0.1"}); + EXPECT_THAT(result, HasSubstr("Successfully created BGP neighbor 10.0.0.1")); + ASSERT_EQ(peers().size(), 1); + EXPECT_EQ(*peers()[0].peer_addr(), "10.0.0.1"); + // bgpd aborts at config load on unparseable (empty) address fields, so a + // new peer must be born with parseable any-addresses of the right family. + EXPECT_EQ(*peers()[0].local_addr(), "0.0.0.0"); + EXPECT_EQ(*peers()[0].next_hop4(), "0.0.0.0"); + EXPECT_EQ(*peers()[0].next_hop6(), "::"); + EXPECT_TRUE(sessionFileExists()); + + run({"2001:db8::1"}); + ASSERT_EQ(peers().size(), 2); + EXPECT_EQ(*peers()[1].local_addr(), "::"); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, setRemoteAsn) { + auto result = run({"10.0.0.1", "remote-asn", "65000"}); + EXPECT_THAT(result, HasSubstr("65000")); + ASSERT_EQ(peers().size(), 1); + EXPECT_EQ(peers()[0].remote_as_4_byte().value_or(0), 65000); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, timersAccumulate) { + run({"10.0.0.1", "timers", "hold-time", "90"}); + run({"10.0.0.1", "timers", "keepalive", "30"}); + run({"10.0.0.1", "graceful-restart", "restart-time", "120"}); + ASSERT_EQ(peers().size(), 1); + const auto& timers = *peers()[0].bgp_peer_timers(); + EXPECT_EQ(*timers.hold_time_seconds(), 90); + EXPECT_EQ(*timers.keep_alive_seconds(), 30); + EXPECT_EQ(timers.graceful_restart_seconds().value_or(0), 120); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, addPathMerge) { + run({"10.0.0.1", "add-path", "receive", "true"}); + EXPECT_EQ(peers()[0].add_path().value_or(AddPath(0)), AddPath::RECEIVE); + + run({"10.0.0.1", "add-path", "send", "true"}); + EXPECT_EQ(peers()[0].add_path().value_or(AddPath(0)), AddPath::BOTH); + + run({"10.0.0.1", "add-path", "receive", "false"}); + EXPECT_EQ(peers()[0].add_path().value_or(AddPath(0)), AddPath::SEND); + + run({"10.0.0.1", "add-path", "send", "false"}); + EXPECT_FALSE(peers()[0].add_path().has_value()); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, passive) { + // is_passive is the only session-mode knob bgpd has: true = listen only, + // false = listen and connect. The CLI exposes the flag as-is. + run({"10.0.0.1", "passive", "true"}); + EXPECT_TRUE(peers()[0].is_passive().value_or(false)); + + run({"10.0.0.1", "passive", "false"}); + EXPECT_FALSE(peers()[0].is_passive().value_or(true)); + + EXPECT_THAT(run({"10.0.0.1", "passive", "BOTH"}), HasSubstr("Invalid")); + EXPECT_FALSE(peers()[0].is_passive().value_or(true)); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, boolAndStringAttributes) { + run({"10.0.0.1", "rr-client", "true"}); + run({"10.0.0.1", "confed-peer", "false"}); + run({"10.0.0.1", "redistribute-peer", "true"}); + run({"10.0.0.1", "enhanced-route-refresh", "true"}); + run({"10.0.0.1", "afi", "disable-ipv4-afi", "true"}); + run({"10.0.0.1", "afi", "disable-ipv6-afi", "false"}); + run({"10.0.0.1", "afi", "ipv4-over-ipv6-nh", "true"}); + run({"10.0.0.1", "graceful-restart", "stateful-ha", "true"}); + run({"10.0.0.1", "peer-group", "MY-GROUP"}); + run({"10.0.0.1", "peer-tag", "FSW"}); + run({"10.0.0.1", "ingress-policy", "IN_POLICY"}); + run({"10.0.0.1", "egress-policy", "OUT_POLICY"}); + run({"10.0.0.1", "bind-addr", "address", "10.0.0.2"}); + run({"10.0.0.1", "local-asn", "64512"}); + run({"10.0.0.1", "description", "spine", "peer"}); + + ASSERT_EQ(peers().size(), 1); + const auto& peer = peers()[0]; + EXPECT_TRUE(peer.is_rr_client().value_or(false)); + EXPECT_FALSE(peer.is_confed_peer().value_or(true)); + EXPECT_TRUE(peer.is_redistribute_peer().value_or(false)); + EXPECT_TRUE(peer.enhanced_route_refresh().value_or(false)); + EXPECT_TRUE(peer.disable_ipv4_afi().value_or(false)); + EXPECT_FALSE(peer.disable_ipv6_afi().value_or(true)); + EXPECT_TRUE(peer.v4_over_v6_nexthop().value_or(false)); + EXPECT_TRUE(peer.enable_stateful_ha().value_or(false)); + EXPECT_EQ(peer.peer_group_name().value_or(""), "MY-GROUP"); + EXPECT_EQ(peer.peer_tag().value_or(""), "FSW"); + EXPECT_EQ(peer.ingress_policy_name().value_or(""), "IN_POLICY"); + EXPECT_EQ(peer.egress_policy_name().value_or(""), "OUT_POLICY"); + EXPECT_EQ(*peer.local_addr(), "10.0.0.2"); + EXPECT_EQ(peer.local_as_4_byte().value_or(0), 64512); + // Multi-token description is re-joined. + EXPECT_EQ(peer.description().value_or(""), "spine peer"); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, parityAttributes) { + // Attributes carried over from the deleted per-attribute peer commands. + run({"10.0.0.1", "next-hop4", "10.1.1.1"}); + run({"10.0.0.1", "next-hop6", "2001:db8::a"}); + run({"10.0.0.1", "next-hop-self", "true"}); + run({"10.0.0.1", "peer-id", "test-peer:v6:1"}); + run({"10.0.0.1", "type", "BGP_MONITOR"}); + run({"10.0.0.1", "link-bandwidth", "100G"}); + EXPECT_EQ(peers()[0].link_bandwidth_bps().value_or(""), "100G"); + // Every spelling bgpd's own parser takes must be accepted, or an existing + // config could not be recreated through the CLI. + run({"10.0.0.1", "link-bandwidth", "1.5G"}); + EXPECT_EQ(peers()[0].link_bandwidth_bps().value_or(""), "1.5G"); + run({"10.0.0.1", "link-bandwidth", "150g"}); + EXPECT_EQ(peers()[0].link_bandwidth_bps().value_or(""), "150g"); + run({"10.0.0.1", "link-bandwidth", "auto"}); + EXPECT_EQ(peers()[0].link_bandwidth_bps().value_or(""), "auto"); + run({"10.0.0.1", "link-bandwidth", "100G"}); + run({"10.0.0.1", "advertise-lbw", "1"}); + run({"10.0.0.1", "receive-lbw", "ACCEPT"}); // enum value name accepted + run( + {"10.0.0.1", + "advertise-lbw", + "true"}); // bool alias (old peer cmd compat) + run({"10.0.0.1", "timers", "withdraw-unprog-delay", "5"}); + run({"10.0.0.1", "max-route", "pre-warning-threshold", "100"}); + run({"10.0.0.1", "max-route", "pre-warning-only", "true"}); + run({"10.0.0.1", "max-route", "post-warning-only", "false"}); + + ASSERT_EQ(peers().size(), 1); + const auto& peer = peers()[0]; + EXPECT_EQ(*peer.next_hop4(), "10.1.1.1"); + EXPECT_EQ(*peer.next_hop6(), "2001:db8::a"); + EXPECT_TRUE(peer.next_hop_self().value_or(false)); + EXPECT_EQ(peer.peer_id().value_or(""), "test-peer:v6:1"); + EXPECT_EQ(peer.type().value_or(""), "BGP_MONITOR"); + EXPECT_EQ(peer.link_bandwidth_bps().value_or(""), "100G"); + EXPECT_EQ( + static_cast(peer.advertise_link_bandwidth().value_or( + facebook::neteng::fboss::bgp_attr::AdvertiseLinkBandwidth::DISABLE)), + 1); + EXPECT_EQ( + static_cast(peer.receive_link_bandwidth().value_or( + facebook::neteng::fboss::bgp_attr::ReceiveLinkBandwidth::DISABLE)), + 1); + EXPECT_EQ(*peer.bgp_peer_timers()->withdraw_unprog_delay_seconds(), 5); + EXPECT_EQ(*peer.pre_filter()->warning_limit(), 100); + EXPECT_TRUE(*peer.pre_filter()->warning_only()); + EXPECT_FALSE(*peer.post_filter()->warning_only()); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, valueValidation) { + // ASN must fit in 4 bytes (RFC 6793). + EXPECT_THAT( + run({"10.0.0.1", "remote-asn", "4294967296"}), HasSubstr("Invalid")); + EXPECT_THAT( + run({"10.0.0.1", "local-asn", "18446744073709551615"}), + HasSubstr("Invalid")); + // IP-valued attributes validate the address and its family. + EXPECT_THAT( + run({"10.0.0.1", "bind-addr", "address", "not-an-ip"}), + HasSubstr("Invalid")); + EXPECT_THAT(run({"10.0.0.1", "next-hop4", "2001:db8::a"}), HasSubstr("IPv4")); + EXPECT_THAT(run({"10.0.0.1", "next-hop6", "10.1.1.1"}), HasSubstr("IPv6")); + // Enum-valued attributes reject non-member modes but accept value names + // and true/false aliases (compat with the replaced boolean peer commands). + EXPECT_THAT( + run({"10.0.0.1", "advertise-lbw", "99"}), HasSubstr("not a valid mode")); + // link-bandwidth rejects exactly what bgpd's parser rejects. + EXPECT_THAT(run({"10.0.0.1", "link-bandwidth", "10Q"}), HasSubstr("Invalid")); + EXPECT_THAT(run({"10.0.0.1", "link-bandwidth", "G10"}), HasSubstr("Invalid")); + EXPECT_THAT( + run({"10.0.0.1", "link-bandwidth", "1.2.3G"}), HasSubstr("Invalid")); + EXPECT_THAT(run({"10.0.0.1", "link-bandwidth", "G"}), HasSubstr("Invalid")); + EXPECT_THAT(run({"10.0.0.1", "link-bandwidth", "10T"}), HasSubstr("Invalid")); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +TEST_F(CmdConfigBgpNeighborTestFixture, routeLimits) { + run({"10.0.0.1", "max-route", "pre-filter", "45000"}); + run({"10.0.0.1", "max-route", "post-filter", "50000"}); + run({"10.0.0.1", "max-route", "post-warning-threshold", "48000"}); + const auto& peer = peers()[0]; + EXPECT_EQ(*peer.pre_filter()->max_routes(), 45000); + EXPECT_EQ(*peer.post_filter()->max_routes(), 50000); + EXPECT_EQ(*peer.post_filter()->warning_limit(), 48000); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, normalizedAddressesShareOnePeer) { + run({"2001:DB8::1", "remote-asn", "65000"}); + run({"2001:db8:0:0::1", "timers", "hold-time", "30"}); + ASSERT_EQ(peers().size(), 1); + EXPECT_EQ(*peers()[0].peer_addr(), "2001:db8::1"); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, prefixSpellingsShareOnePeer) { + // bgpd masks a dynamic peer's prefix at load, so these are one neighbor to + // it; a second CLI spelling must update that peer, not create a twin. + run({"10.0.0.1/24", "remote-asn", "65000"}); + run({"10.0.0.2/24", "passive", "true"}); + ASSERT_EQ(peers().size(), 1); + EXPECT_EQ(*peers()[0].peer_addr(), "10.0.0.0/24"); + EXPECT_EQ(peers()[0].remote_as_4_byte().value_or(0), 65000); + EXPECT_TRUE(peers()[0].is_passive().value_or(false)); +} + +// ============================================================================== +// Inherited values: bgpd resolves timers / route limits per STRUCT, so the +// first field set on a peer must not silently zero the rest of the struct. +// ============================================================================== + +TEST_F(CmdConfigBgpNeighborTestFixture, timersSeededFromPeerGroup) { + auto& group = addPeerGroup("SPINE"); + bgp::thrift::BgpPeerTimers groupTimers; + groupTimers.hold_time_seconds() = 90; + groupTimers.keep_alive_seconds() = 30; + groupTimers.out_delay_seconds() = 5; + groupTimers.graceful_restart_seconds() = 120; + group.bgp_peer_timers() = groupTimers; + + run({"10.0.0.1", "peer-group", "SPINE"}); + run({"10.0.0.1", "graceful-restart", "restart-time", "60"}); + + ASSERT_EQ(peers().size(), 1); + const auto& timers = *peers()[0].bgp_peer_timers(); + EXPECT_EQ(*timers.hold_time_seconds(), 90); + EXPECT_EQ(*timers.keep_alive_seconds(), 30); + EXPECT_EQ(*timers.out_delay_seconds(), 5); + EXPECT_EQ(timers.graceful_restart_seconds().value_or(0), 60); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, timersSeededFromGlobalWithoutGroup) { + config().hold_time() = 180; + run({"10.0.0.1", "graceful-restart", "restart-time", "60"}); + + const auto& timers = *peers()[0].bgp_peer_timers(); + EXPECT_EQ(*timers.hold_time_seconds(), 180); + EXPECT_EQ(*timers.keep_alive_seconds(), 60); + EXPECT_EQ(*timers.out_delay_seconds(), 0); + EXPECT_EQ(timers.graceful_restart_seconds().value_or(0), 60); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, timersNotReseededOncePresent) { + auto& group = addPeerGroup("SPINE"); + bgp::thrift::BgpPeerTimers groupTimers; + groupTimers.hold_time_seconds() = 90; + groupTimers.keep_alive_seconds() = 30; + group.bgp_peer_timers() = groupTimers; + + run({"10.0.0.1", "peer-group", "SPINE"}); + run({"10.0.0.1", "timers", "hold-time", "45"}); + run({"10.0.0.1", "timers", "out-delay", "2"}); + + const auto& timers = *peers()[0].bgp_peer_timers(); + EXPECT_EQ(*timers.hold_time_seconds(), 45); + EXPECT_EQ(*timers.keep_alive_seconds(), 30); + EXPECT_EQ(*timers.out_delay_seconds(), 2); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, routeLimitSeededFromPeerGroup) { + auto& group = addPeerGroup("SPINE"); + bgp::thrift::RouteLimit limit; + limit.max_routes() = 45000; + limit.warning_limit() = 40000; + group.pre_filter() = limit; + + run({"10.0.0.1", "peer-group", "SPINE"}); + run({"10.0.0.1", "max-route", "pre-warning-only", "true"}); + + const auto& pre = *peers()[0].pre_filter(); + EXPECT_EQ(*pre.max_routes(), 45000); + EXPECT_EQ(*pre.warning_limit(), 40000); + EXPECT_TRUE(*pre.warning_only()); + // post_filter was not touched and stays inherited. + EXPECT_FALSE(peers()[0].post_filter().has_value()); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, addPathMergesWithPeerGroupValue) { + addPeerGroup("SPINE").add_path() = AddPath::SEND; + run({"10.0.0.1", "peer-group", "SPINE"}); + + // Enabling the other direction keeps the group's SEND. + run({"10.0.0.1", "add-path", "receive", "true"}); + EXPECT_EQ(peers()[0].add_path().value_or(AddPath(0)), AddPath::BOTH); + + // Turning receive back off leaves the peer at the group's value explicitly. + run({"10.0.0.1", "add-path", "receive", "false"}); + EXPECT_EQ(peers()[0].add_path().value_or(AddPath(0)), AddPath::SEND); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, addPathCannotDisableGroupDirection) { + // The enum has no disabled value and an unset peer field inherits the + // group, so "send false" here would be a no-op reported as success. + addPeerGroup("SPINE").add_path() = AddPath::SEND; + run({"10.0.0.1", "peer-group", "SPINE"}); + + auto result = run({"10.0.0.1", "add-path", "send", "false"}); + EXPECT_THAT(result, HasSubstr("Error")); + EXPECT_THAT(result, HasSubstr("SPINE")); + EXPECT_FALSE(peers()[0].add_path().has_value()); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, addPathNarrowsGroupBothToOneDirection) { + addPeerGroup("SPINE").add_path() = AddPath::BOTH; + run({"10.0.0.1", "peer-group", "SPINE"}); + + // Dropping one direction of BOTH is representable: the peer gets an + // explicit RECEIVE that overrides the group. + run({"10.0.0.1", "add-path", "send", "false"}); + EXPECT_EQ(peers()[0].add_path().value_or(AddPath(0)), AddPath::RECEIVE); + // ... but the remaining direction cannot be switched off (see above). + EXPECT_THAT( + run({"10.0.0.1", "add-path", "receive", "false"}), HasSubstr("Error")); + EXPECT_EQ(peers()[0].add_path().value_or(AddPath(0)), AddPath::RECEIVE); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, timerRanges) { + // Hold time: 0 or 3..65535 (bgpd refuses 1-2; the OPEN field is 16 bits). + EXPECT_THAT( + run({"10.0.0.1", "timers", "hold-time", "1"}), HasSubstr("Error")); + EXPECT_THAT( + run({"10.0.0.1", "timers", "hold-time", "2"}), HasSubstr("Error")); + EXPECT_THAT( + run({"10.0.0.1", "timers", "hold-time", "65536"}), HasSubstr("Error")); + EXPECT_FALSE(sessionFileExists()); + run({"10.0.0.1", "timers", "hold-time", "0"}); + EXPECT_EQ(*peers()[0].bgp_peer_timers()->hold_time_seconds(), 0); + run({"10.0.0.1", "timers", "hold-time", "3"}); + EXPECT_EQ(*peers()[0].bgp_peer_timers()->hold_time_seconds(), 3); + run({"10.0.0.1", "timers", "hold-time", "65535"}); + EXPECT_EQ(*peers()[0].bgp_peer_timers()->hold_time_seconds(), 65535); + + // Graceful restart: 0..4095 (12-bit RFC 4724 field). + EXPECT_THAT( + run({"10.0.0.1", "graceful-restart", "restart-time", "4096"}), + HasSubstr("Error")); + run({"10.0.0.1", "graceful-restart", "restart-time", "4095"}); + EXPECT_EQ( + peers()[0].bgp_peer_timers()->graceful_restart_seconds().value_or(0), + 4095); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, rejectedValueLeavesPeerUntouched) { + run({"10.0.0.1", "remote-asn", "65000"}); + // A rejected timer must not leave a seeded timers struct behind. + EXPECT_THAT( + run({"10.0.0.1", "timers", "hold-time", "1"}), HasSubstr("Error")); + EXPECT_FALSE(peers()[0].bgp_peer_timers().has_value()); + EXPECT_EQ(peers().size(), 1); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, distinctAddressesCreateDistinctPeers) { + run({"10.0.0.1", "remote-asn", "65000"}); + run({"10.0.0.2", "remote-asn", "65001"}); + EXPECT_EQ(peers().size(), 2); +} + +// ============================================================================== +// Help text — generated from the dispatch table and wired into `--help` +// ============================================================================== + +TEST_F(CmdConfigBgpNeighborTestFixture, attributeHelpListsEveryAttribute) { + const auto help = bgpNeighborAttributeHelp(); + const auto names = bgpNeighborAttributeNames(); + ASSERT_FALSE(names.empty()); + for (const auto& name : names) { + // One line per attribute: indented name, then its value placeholder. + EXPECT_THAT(help, HasSubstr("\n " + name + " ")) << name; + // ... and every listed spelling is one the parser accepts. + std::vector tokens = {"10.0.0.1"}; + folly::split(' ', name, tokens); + tokens.emplace_back("x"); + EXPECT_EQ(BgpNeighborConfig(tokens).attr(), name); + } + // Spot-check the value shapes users most need to see. + EXPECT_THAT(help, HasSubstr("passive")); + EXPECT_THAT(help, HasSubstr("")); + EXPECT_THAT(help, HasSubstr("timers hold-time")); + EXPECT_THAT(help, HasSubstr("0 or 3-65535")); + EXPECT_THAT(help, HasSubstr("graceful-restart restart-time")); + EXPECT_THAT(help, HasSubstr("0-4095")); + EXPECT_THAT(help, HasSubstr("| auto")); + EXPECT_THAT(help, HasSubstr("not supported")); + EXPECT_THAT(help, Not(HasSubstr("connect-mode"))); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, cliHelpFooterCarriesAttributeList) { + CLI::App app; + std::vector args; + CmdConfigProtocolBgpNeighborTraits::addCliArg(app, args); + EXPECT_EQ(app.get_footer(), bgpNeighborAttributeHelp()); + EXPECT_THAT(app.help(), HasSubstr("timers hold-time")); +} + +// ============================================================================== +// Reject paths — the error is surfaced and nothing is persisted +// ============================================================================== + +TEST_F(CmdConfigBgpNeighborTestFixture, invalidValuesRejected) { + EXPECT_THAT(run({"10.0.0.1", "rr-client", "maybe"}), HasSubstr("Invalid")); + EXPECT_THAT( + run({"10.0.0.1", "timers", "hold-time", "-1"}), + HasSubstr("non-negative")); + EXPECT_THAT( + run({"10.0.0.1", "remote-asn", "not-a-number"}), HasSubstr("Invalid")); + EXPECT_THAT( + run({"10.0.0.1", "max-route", "pre-filter", "-5"}), + HasSubstr("non-negative")); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +TEST_F(CmdConfigBgpNeighborTestFixture, unsupportedAttributesRejected) { + // No thrift representation (or not per-neighbor) — refuse instead of + // persisting dead config. + EXPECT_THAT( + run({"10.0.0.1", "peer-port", "1179"}), HasSubstr("not supported")); + EXPECT_THAT( + run({"10.0.0.1", "bind-addr", "port", "1179"}), + HasSubstr("not supported")); + EXPECT_THAT( + run({"10.0.0.1", "afi", "ipv4-labeled-unicast", "true"}), + HasSubstr("not supported")); + EXPECT_THAT( + run({"10.0.0.1", "afi", "ipv6-labeled-unicast", "true"}), + HasSubstr("not supported")); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/config/CmdDeleteBgpNeighborTest.cpp b/fboss/cli/fboss2/test/config/CmdDeleteBgpNeighborTest.cpp new file mode 100644 index 0000000000000..7d59a8541e6ab --- /dev/null +++ b/fboss/cli/fboss2/test/config/CmdDeleteBgpNeighborTest.cpp @@ -0,0 +1,128 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include +#include +#include + +#include +#include +#include +#include + +#include "fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/test/config/CmdConfigTestBase.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +using namespace ::testing; + +namespace facebook::fboss { + +// Deleting only touches the BGP side of ConfigSession, which seeds from +// thrift schema defaults when neither a staged session nor a system +// bgpcpp.conf exists — so no seed agent config is needed (mirrors +// CmdConfigBgpNeighborTest). +class CmdDeleteBgpNeighborTestFixture : public CmdConfigTestBase { + public: + CmdDeleteBgpNeighborTestFixture() + : CmdConfigTestBase("bgp_neighbor_delete_test_%%%%-%%%%-%%%%", "") {} + + void SetUp() override { + CmdConfigTestBase::SetUp(); + setupTestableConfigSession(); + } + + std::string configure(const std::vector& tokens) { + CmdConfigProtocolBgpNeighbor cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpNeighborConfig(tokens)); + } + + std::string del(const std::vector& tokens) { + CmdDeleteProtocolBgpNeighbor cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpNeighborRef(tokens)); + } + + const std::vector& peers() { + return *ConfigSession::getInstance().getBgpConfig().peers(); + } + + bool sessionFileExists() { + return std::filesystem::exists( + ConfigSession::getInstance().getBgpSessionConfigPath()); + } +}; + +// ============================================================================== +// BgpNeighborRef (arg) validation +// ============================================================================== + +TEST_F(CmdDeleteBgpNeighborTestFixture, argValidation) { + // Address normalization mirrors the config command. + EXPECT_EQ(BgpNeighborRef({"2001:DB8::1"}).peerAddr(), "2001:db8::1"); + EXPECT_EQ(BgpNeighborRef({"2001:db8::/64"}).peerAddr(), "2001:db8::/64"); + EXPECT_EQ(BgpNeighborRef({"10.0.0.1/24"}).peerAddr(), "10.0.0.0/24"); + + // Invalid: empty, extra tokens, bad address. + EXPECT_THROW(BgpNeighborRef({}), std::invalid_argument); + EXPECT_THROW(BgpNeighborRef({"10.0.0.1", "10.0.0.2"}), std::invalid_argument); + EXPECT_THROW(BgpNeighborRef({"not-an-ip"}), std::invalid_argument); +} + +// ============================================================================== +// queryClient +// ============================================================================== + +TEST_F(CmdDeleteBgpNeighborTestFixture, deleteExistingNeighbor) { + configure({"10.0.0.1", "remote-asn", "65000"}); + configure({"10.0.0.2", "remote-asn", "65001"}); + ASSERT_EQ(peers().size(), 2); + + auto result = del({"10.0.0.1"}); + EXPECT_THAT(result, HasSubstr("Successfully deleted BGP neighbor 10.0.0.1")); + // The other neighbor survives untouched. + ASSERT_EQ(peers().size(), 1); + EXPECT_EQ(*peers()[0].peer_addr(), "10.0.0.2"); + EXPECT_EQ(peers()[0].remote_as_4_byte().value_or(0), 65001); + EXPECT_TRUE(sessionFileExists()); +} + +TEST_F(CmdDeleteBgpNeighborTestFixture, deleteByAnySpelling) { + configure({"2001:db8::1", "remote-asn", "65000"}); + ASSERT_EQ(peers().size(), 1); + // A different spelling of the same address deletes the same peer. + auto result = del({"2001:DB8:0:0::1"}); + EXPECT_THAT(result, HasSubstr("Successfully deleted")); + EXPECT_TRUE(peers().empty()); +} + +TEST_F(CmdDeleteBgpNeighborTestFixture, deleteByEquivalentPrefixSpelling) { + // bgpd masks dynamic-peer prefixes, so 10.0.0.1/24 and 10.0.0.2/24 name the + // same neighbor; deleting via either spelling must find it. + configure({"10.0.0.1/24", "remote-asn", "65000"}); + ASSERT_EQ(peers().size(), 1); + EXPECT_EQ(*peers()[0].peer_addr(), "10.0.0.0/24"); + auto result = del({"10.0.0.2/24"}); + EXPECT_THAT(result, HasSubstr("Successfully deleted")); + EXPECT_TRUE(peers().empty()); +} + +TEST_F(CmdDeleteBgpNeighborTestFixture, deleteUnknownNeighborRejected) { + auto result = del({"192.0.2.99"}); + EXPECT_THAT(result, HasSubstr("Error: BGP neighbor 192.0.2.99 not found")); + // Nothing was persisted for the failed delete. + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected delete"; +} + +TEST_F(CmdDeleteBgpNeighborTestFixture, deleteUnknownLeavesOthersIntact) { + configure({"10.0.0.1", "remote-asn", "65000"}); + auto result = del({"10.0.0.99"}); + EXPECT_THAT(result, HasSubstr("not found")); + ASSERT_EQ(peers().size(), 1); + EXPECT_EQ(*peers()[0].peer_addr(), "10.0.0.1"); +} + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/integration_test/BUCK b/fboss/cli/fboss2/test/integration_test/BUCK index e1af58457345c..9d37e37383c40 100644 --- a/fboss/cli/fboss2/test/integration_test/BUCK +++ b/fboss/cli/fboss2/test/integration_test/BUCK @@ -21,6 +21,7 @@ cpp_binary( "ConfigAdminDistanceTest.cpp", "ConfigArpTest.cpp", "ConfigBgpGlobalTest.cpp", + "ConfigBgpNeighborTest.cpp", "ConfigBgpSessionTest.cpp", "ConfigConcurrentSessionsTest.cpp", "ConfigCoppTest.cpp", @@ -68,10 +69,14 @@ cpp_binary( "TunnelIpInIpTest.cpp", "//fboss/cli/fboss2:cmd-list-config-impl", ], + headers = [ + "ConfigBgpTestBase.h", + ], deps = [ "fbsource//third-party/fmt:fmt", "fbsource//third-party/googletest:gmock", "fbsource//third-party/googletest:gtest", + "//configerator/structs/neteng/fboss/bgp:bgp_config-cpp2-types", "//fboss/agent:fboss-types", "//fboss/agent:platform_config-cpp2-types", "//fboss/agent:switch_config-cpp2-types", @@ -97,6 +102,7 @@ cpp_binary( "//folly/json:dynamic", "//folly/logging:init", "//folly/logging:logging", + "//neteng/fboss/bgp/if:bgp_thrift-cpp2-services", "//thrift/lib/cpp/util:enum_utils", ], external_deps = [ diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpNeighborTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpNeighborTest.cpp new file mode 100644 index 0000000000000..be697eb6fb137 --- /dev/null +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpNeighborTest.cpp @@ -0,0 +1,153 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +/** + * End-to-end tests for `fboss2-dev config protocol bgp neighbor + * [ ...]`. + * + * Scope: the per-neighbor attributes. Every test stages the change AND + * commits it, then asserts the value landed at the correct thrift field path + * inside the matching .peers[] entry of the promoted system config + * (/etc/coop/bgpcpp/bgpcpp.conf) and of bgpd's own running config. Stage-only + * behavior (attribute parsing, validation, rejection) is covered by the unit + * tests; an integration test that never commits exercises no daemon. + * + * Requirements: + * - The fboss2-dev binary under test (config subcommand tree). + * - HOME is set (the session file lives under $HOME/.fboss2). + * - bgpd is installed/active (commit restarts it). + */ + +#include +#include +#include + +#include "fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h" +#include "folly/json/dynamic.h" +#include "gmock/gmock.h" + +using namespace facebook::fboss; +using ::testing::HasSubstr; +using ::testing::Not; + +namespace { +// Test-only neighbor addresses, from the documentation range (RFC 3849) so +// they never collide with a real peer in the device's running BGP config. +const std::string kNeighbor = "2001:db8::1234"; +} // namespace + +class ConfigBgpNeighborTest : public ConfigBgpTestBase { + protected: + // Stage `config protocol bgp neighbor ` WITHOUT clearing the + // staged session, so attributes can accumulate across invocations. Returns + // the staged session JSON. + folly::dynamic stageNeighbor(const std::vector& tokens) { + std::vector args = {"config", "protocol", "bgp", "neighbor"}; + args.insert(args.end(), tokens.begin(), tokens.end()); + auto result = runCli(args); + EXPECT_EQ(result.exitCode, 0) + << "stdout=" << result.stdout << " stderr=" << result.stderr; + EXPECT_THAT(result.stdout, Not(HasSubstr("Error:"))); + return readBgpSessionConfig(); + } + + // The .peers[] entry for addr, or nullptr. + static const folly::dynamic* findPeer( + const folly::dynamic& config, + const std::string& addr) { + if (config.count("peers") == 0) { + return nullptr; + } + for (const auto& peer : config["peers"]) { + if (peer.count("peer_addr") && peer["peer_addr"].asString() == addr) { + return &peer; + } + } + return nullptr; + } +}; + +TEST_F(ConfigBgpNeighborTest, SetRemoteAsnAndCommit) { + discardSession(); + clearBgpSession(); + stageNeighbor({kNeighbor, "remote-asn", "65000"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + // Two layers: the promoted file proves what the commit wrote; the daemon's + // getRunningConfig RPC proves bgpd parsed and adopted it after the restart. + auto config = readSystemBgpConfig(); + const auto* peer = findPeer(config, kNeighbor); + ASSERT_NE(peer, nullptr) << "committed config has no peer " << kNeighbor; + ASSERT_TRUE(peer->count("remote_as_4_byte")); + EXPECT_EQ((*peer)["remote_as_4_byte"].asInt(), 65000); + + auto running = readRunningBgpConfigViaRpc(); + const auto* runningPeer = findPeer(running, kNeighbor); + ASSERT_NE(runningPeer, nullptr) + << "bgpd's running config has no peer " << kNeighbor; + EXPECT_EQ((*runningPeer)["remote_as_4_byte"].asInt(), 65000); +} + +TEST_F(ConfigBgpNeighborTest, SetTimersHoldTimeAndCommit) { + discardSession(); + clearBgpSession(); + stageNeighbor({kNeighbor, "timers", "hold-time", "90"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + // Verify nested timer fields through the daemon's own view of its config. + auto running = readRunningBgpConfigViaRpc(); + const auto* peer = findPeer(running, kNeighbor); + ASSERT_NE(peer, nullptr) << "bgpd's running config has no peer " << kNeighbor; + ASSERT_TRUE(peer->count("bgp_peer_timers")); + EXPECT_EQ((*peer)["bgp_peer_timers"]["hold_time_seconds"].asInt(), 90); +} + +TEST_F(ConfigBgpNeighborTest, GracefulRestartKeepsInheritedHoldTime) { + // bgpd takes a peer's whole timers struct once any field of it is set, so + // the first per-neighbor timer must carry the inherited hold time along + // instead of the thrift default (0 = no keepalives on the session). + discardSession(); + clearBgpSession(); + auto global = + runCli({"config", "protocol", "bgp", "global", "hold-time", "180"}); + ASSERT_EQ(global.exitCode, 0) << global.stdout << global.stderr; + stageNeighbor({kNeighbor, "graceful-restart", "restart-time", "60"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + auto running = readRunningBgpConfigViaRpc(); + const auto* peer = findPeer(running, kNeighbor); + ASSERT_NE(peer, nullptr) << "bgpd's running config has no peer " << kNeighbor; + ASSERT_TRUE(peer->count("bgp_peer_timers")); + EXPECT_EQ((*peer)["bgp_peer_timers"]["hold_time_seconds"].asInt(), 180); + EXPECT_EQ((*peer)["bgp_peer_timers"]["keep_alive_seconds"].asInt(), 60); + EXPECT_EQ((*peer)["bgp_peer_timers"]["graceful_restart_seconds"].asInt(), 60); +} + +TEST_F(ConfigBgpNeighborTest, DeleteNeighborAndCommit) { + // Land a neighbor in the system config, then delete it through a second + // commit and verify it is gone from the promoted config. + discardSession(); + clearBgpSession(); + stageNeighbor({kNeighbor, "remote-asn", "65000"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + ASSERT_NE(findPeer(readRunningBgpConfigViaRpc(), kNeighbor), nullptr) + << "setup commit did not land the neighbor in bgpd's running config"; + + clearBgpSession(); + auto result = runCli({"delete", "protocol", "bgp", "neighbor", kNeighbor}); + EXPECT_THAT(result.stdout, HasSubstr("Successfully deleted")); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after delete commit; state=" + << bgpDaemonActiveState(); + EXPECT_EQ(findPeer(readRunningBgpConfigViaRpc(), kNeighbor), nullptr) + << "deleted neighbor still present in bgpd's running config"; +} diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h b/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h index 48d722a4ff6ab..83e3077963dc6 100644 --- a/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h @@ -15,18 +15,26 @@ */ #include +#include #include #include #include #include #include +#include +#include +#include #include #include #include +#include +#include +#include #include #include #include +#include #include "fboss/cli/fboss2/test/integration_test/Fboss2IntegrationTest.h" #include "fboss/cli/fboss2/utils/CmdClientUtilsCommon.h" #include "neteng/fboss/bgp/if/gen-cpp2/TBgpService.h" @@ -40,6 +48,17 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { if (bgpDaemonActiveState() != "active") { GTEST_SKIP() << "skipping because bgp is not active"; } + // Snapshot the config bgpd runs with; TearDown restores it, so committed + // test objects never outlive the test regardless of where it failed. + haveBgpSnapshot_ = folly::readFile(kBgpDaemonConfigPath, bgpSnapshot_); + if (haveBgpSnapshot_) { + bgpSnapshotConfig_ = parseBgpConfig(bgpSnapshot_); + } + } + + void TearDown() override { + restoreBgpConfigSnapshot(); + Fboss2IntegrationTest::TearDown(); } // Staged session file written by the CLI (~/.fboss2/bgp_config.json). @@ -79,8 +98,22 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { folly::dynamic readSystemBgpConfig() const { std::string content; if (!folly::readFile(systemBgpConfigPath().c_str(), content)) { - throw std::runtime_error( - "Failed to read system BGP config at " + systemBgpConfigPath()); + const int promotedErrno = errno; + // On a box where no BGP commit ever happened, the promoted + // bgpcpp/bgpcpp.conf doesn't exist yet; the effective config is the + // plain file the RPM installed at /etc/coop/bgpcpp.conf (a symlink to + // the promoted file afterwards, so it is always safe to read). + if (promotedErrno != ENOENT) { + throw std::runtime_error( + "Failed to read system BGP config at " + systemBgpConfigPath() + + ": " + folly::errnoStr(promotedErrno)); + } + if (!folly::readFile(kBgpDaemonConfigPath, content)) { + throw std::runtime_error( + "Failed to read system BGP config: " + systemBgpConfigPath() + + " does not exist and reading " + kBgpDaemonConfigPath + + " failed: " + folly::errnoStr(errno)); + } } return folly::parseJson(content); } @@ -96,23 +129,40 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { // seconds later — an RPC issued right after a commit-triggered restart // would otherwise race that window and get ECONNREFUSED. folly::dynamic readRunningBgpConfigViaRpc( - std::chrono::seconds timeout = std::chrono::seconds(30)) const { + std::chrono::seconds timeout = std::chrono::seconds(90)) const { auto deadline = std::chrono::steady_clock::now() + timeout; + HostInfo hostInfo("localhost"); + std::string configStr; while (true) { try { - HostInfo hostInfo("localhost"); auto client = utils::createClient>(hostInfo); - std::string configStr; client->sync_getRunningConfig(configStr); - return folly::parseJson(configStr); - } catch (const std::exception&) { + break; + } catch (const std::exception& ex) { + // A NOT_OPEN transport exception (connect refused / connection + // closed) is the expected bind-window race; any other RPC error is + // worth surfacing even though we keep retrying until the deadline. + // The refused connect reaches us as a channel-level + // TTransportException wrapping the socket error, so the inner errno + // is gone but the NOT_OPEN type survives. + const auto* tex = + dynamic_cast( + &ex); + if (tex == nullptr || + tex->getType() != + apache::thrift::transport::TTransportException::NOT_OPEN) { + XLOG(WARN) << "BGP getRunningConfig failed with an unexpected " + "error (not a connection refused error): " + << ex.what(); + } if (std::chrono::steady_clock::now() >= deadline) { throw; } std::this_thread::sleep_for(std::chrono::seconds(1)); } } + return folly::parseJson(configStr); } // Root of the git repo that versions both the agent config (cli/agent.conf) @@ -264,6 +314,64 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { } return bgpDaemonActiveState() == "active"; } + + private: + // The stable path bgpd reads (--config): a plain file on a device the CLI + // never committed on, a symlink into bgpcpp/ afterwards. Reading follows + // the symlink, so the snapshot is always bgpd's effective config. + static constexpr auto kBgpDaemonConfigPath = "/etc/coop/bgpcpp.conf"; + + // Deserialized view of a serialized BgpConfig; nullopt if unparseable. + static std::optional parseBgpConfig( + const std::string& content) { + try { + return apache::thrift::SimpleJSONSerializer::deserialize< + bgp::thrift::BgpConfig>(content); + } catch (const std::exception&) { + return std::nullopt; + } + } + + // True when `content` matches the snapshot: by thrift struct equality when + // both sides parse (formatting-only differences from a commit round-trip + // don't count), else by bytes. + bool matchesBgpSnapshot(const std::string& content) const { + auto current = parseBgpConfig(content); + if (bgpSnapshotConfig_ && current) { + return *current == *bgpSnapshotConfig_; + } + return content == bgpSnapshot_; + } + + void restoreBgpConfigSnapshot() { + if (!haveBgpSnapshot_) { + return; // SetUp skipped or there was no config to protect + } + discardSession(); + clearBgpSession(); + std::string current; + if (folly::readFile(kBgpDaemonConfigPath, current) && + matchesBgpSnapshot(current)) { + return; // semantically unchanged; skip the restart + } + // Write the real file, never through the bgpd.conf symlink. + std::string target = std::filesystem::exists(systemBgpConfigPath()) + ? systemBgpConfigPath() + : std::string(kBgpDaemonConfigPath); + EXPECT_TRUE(folly::writeFile(bgpSnapshot_, target.c_str())) + << "failed to restore BGP config snapshot to " << target; + resetBgpDaemonLimit(); + runCmd({"/usr/bin/systemctl", "restart", "bgpd"}); + EXPECT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after snapshot restore; state=" + << bgpDaemonActiveState(); + } + + bool haveBgpSnapshot_{false}; + // Raw bytes, written back verbatim on restore; the typed view drives the + // changed/unchanged decision. + std::string bgpSnapshot_; + std::optional bgpSnapshotConfig_; }; } // namespace facebook::fboss diff --git a/fboss/cli/fboss2/tools/bgp_json_to_cli.py b/fboss/cli/fboss2/tools/bgp_json_to_cli.py index cbc096e7a0bcd..05136ee8ec7bb 100644 --- a/fboss/cli/fboss2/tools/bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/bgp_json_to_cli.py @@ -10,10 +10,10 @@ This enables round-trip conversion: JSON -> CLI -> JSON (via fboss2). Usage: - python3 bgp_json_to_cli.py [--output output.sh] [--stub] + python3 bgp_json_to_cli.py [--output output.sh] Example: - python3 bgp_json_to_cli.py rsw_bgp_config.json --stub > configure_bgp.sh + python3 bgp_json_to_cli.py rsw_bgp_config.json > configure_bgp.sh """ import argparse @@ -33,19 +33,17 @@ def _generate_global_basic_commands(config: dict[str, Any]) -> list[str]: commands = [] if "router_id" in config: commands.append( - f"config protocol bgp global router-id {escape_shell_arg(config['router_id'])}" + f"config protocol bgp global router-id {escape_shell_arg(str(config['router_id']))}" ) if "local_as_4_byte" in config: commands.append( - f"config protocol bgp global local-asn {escape_shell_arg(str(config['local_as_4_byte']))}" + f"config protocol bgp global local-asn {config['local_as_4_byte']}" ) if "hold_time" in config: - commands.append( - f"config protocol bgp global hold-time {escape_shell_arg(str(config['hold_time']))}" - ) + commands.append(f"config protocol bgp global hold-time {config['hold_time']}") if "local_confed_as_4_byte" in config: commands.append( - f"config protocol bgp global confed-asn {escape_shell_arg(str(config['local_confed_as_4_byte']))}" + f"config protocol bgp global confed-asn {config['local_confed_as_4_byte']}" ) return commands @@ -57,17 +55,13 @@ def _generate_network6_commands(networks: list[dict[str, Any]]) -> list[str]: prefix = network.get("prefix", "") if not prefix: continue - cmd_parts = [ - f"config protocol bgp global network6 add {escape_shell_arg(prefix)}" - ] + cmd_parts = [f"config protocol bgp global network6 add {prefix}"] if "policy_name" in network: cmd_parts.append(f"policy {escape_shell_arg(network['policy_name'])}") if "install_to_fib" in network: cmd_parts.append(f"install-to-fib {str(network['install_to_fib']).lower()}") if "minimum_supporting_routes" in network: - cmd_parts.append( - f"min-routes {escape_shell_arg(str(network['minimum_supporting_routes']))}" - ) + cmd_parts.append(f"min-routes {network['minimum_supporting_routes']}") commands.append(" ".join(cmd_parts)) return commands @@ -77,19 +71,19 @@ def _generate_switch_limit_commands(switch_limit: dict[str, Any]) -> list[str]: commands = [] if "prefix_limit" in switch_limit: commands.append( - f"config protocol bgp global switch-limit {escape_shell_arg(str(switch_limit['prefix_limit']))}" + f"config protocol bgp global switch-limit {switch_limit['prefix_limit']}" ) if "total_path_limit" in switch_limit: commands.append( - f"config protocol bgp global switch-limit-total-path {escape_shell_arg(str(switch_limit['total_path_limit']))}" + f"config protocol bgp global switch-limit-total-path {switch_limit['total_path_limit']}" ) if "max_golden_vips" in switch_limit: commands.append( - f"config protocol bgp global switch-limit-max-golden-vips {escape_shell_arg(str(switch_limit['max_golden_vips']))}" + f"config protocol bgp global switch-limit-max-golden-vips {switch_limit['max_golden_vips']}" ) if "overload_protection_mode" in switch_limit: commands.append( - f"config protocol bgp global switch-limit-overload-protection-mode {escape_shell_arg(str(switch_limit['overload_protection_mode']))}" + f"config protocol bgp global switch-limit-overload-protection-mode {switch_limit['overload_protection_mode']}" ) return commands @@ -112,7 +106,7 @@ def _generate_peer_group_basic_commands( commands = [] if "remote_as_4_byte" in peer_group: commands.append( - f"config protocol bgp peer-group {escaped_name} remote-asn {escape_shell_arg(str(peer_group['remote_as_4_byte']))}" + f"config protocol bgp peer-group {escaped_name} remote-asn {peer_group['remote_as_4_byte']}" ) if "description" in peer_group: commands.append( @@ -160,19 +154,19 @@ def _generate_peer_group_timer_commands( commands = [] if timers.get("hold_time_seconds"): commands.append( - f"config protocol bgp peer-group {escaped_name} timers hold-time {escape_shell_arg(str(timers['hold_time_seconds']))}" + f"config protocol bgp peer-group {escaped_name} timers hold-time {timers['hold_time_seconds']}" ) if timers.get("keep_alive_seconds"): commands.append( - f"config protocol bgp peer-group {escaped_name} timers keepalive {escape_shell_arg(str(timers['keep_alive_seconds']))}" + f"config protocol bgp peer-group {escaped_name} timers keepalive {timers['keep_alive_seconds']}" ) if "out_delay_seconds" in timers: commands.append( - f"config protocol bgp peer-group {escaped_name} timers out-delay {escape_shell_arg(str(timers['out_delay_seconds']))}" + f"config protocol bgp peer-group {escaped_name} timers out-delay {timers['out_delay_seconds']}" ) if "withdraw_unprog_delay_seconds" in timers: commands.append( - f"config protocol bgp peer-group {escaped_name} timers withdraw-unprog-delay {escape_shell_arg(str(timers['withdraw_unprog_delay_seconds']))}" + f"config protocol bgp peer-group {escaped_name} timers withdraw-unprog-delay {timers['withdraw_unprog_delay_seconds']}" ) return commands @@ -184,11 +178,11 @@ def _generate_peer_group_prefilter_commands( commands = [] if pre_filter.get("max_routes"): commands.append( - f"config protocol bgp peer-group {escaped_name} max-routes {escape_shell_arg(str(pre_filter['max_routes']))}" + f"config protocol bgp peer-group {escaped_name} max-routes {pre_filter['max_routes']}" ) if "warning_limit" in pre_filter: commands.append( - f"config protocol bgp peer-group {escaped_name} warning-limit {escape_shell_arg(str(pre_filter['warning_limit']))}" + f"config protocol bgp peer-group {escaped_name} warning-limit {pre_filter['warning_limit']}" ) if "warning_only" in pre_filter: commands.append( @@ -221,9 +215,11 @@ def generate_peer_group_commands(peer_group: dict[str, Any]) -> list[str]: def format_bandwidth(bps: int) -> str: - """Format bandwidth in bps to human-readable format (e.g., 10G).""" - if bps >= 1_000_000_000_000 and bps % 1_000_000_000_000 == 0: - return f"{bps // 1_000_000_000_000}T" + """Format bandwidth in bps with the largest exact K/M/G suffix. + + Only K, M and G are emitted: those are the multipliers bgpd's + link-bandwidth parser accepts, so a "T" spelling would not round-trip. + """ if bps >= 1_000_000_000 and bps % 1_000_000_000 == 0: return f"{bps // 1_000_000_000}G" if bps >= 1_000_000 and bps % 1_000_000 == 0: @@ -233,169 +229,153 @@ def format_bandwidth(bps: int) -> str: return str(bps) -def _generate_peer_basic_commands(peer: dict[str, Any], peer_addr: str) -> list[str]: - """Generate basic peer commands (remote-asn, peer-group, description, etc.).""" +# (json field, CLI attribute, escape as free-form shell arg) — fields whose +# value maps 1:1 onto a neighbor attribute token. +_NEIGHBOR_SCALAR_FIELDS = [ + ("remote_as_4_byte", "remote-asn", False), + ("local_as_4_byte", "local-asn", False), + ("peer_group_name", "peer-group", True), + ("description", "description", True), + ("peer_tag", "peer-tag", True), + ("local_addr", "bind-addr address", True), + ("ingress_policy_name", "ingress-policy", True), + ("egress_policy_name", "egress-policy", True), + ("next_hop4", "next-hop4", True), + ("next_hop6", "next-hop6", True), + ("peer_id", "peer-id", True), + ("type", "type", True), +] + + +def _generate_neighbor_basic_commands(peer: dict[str, Any], prefix: str) -> list[str]: + """Generate scalar neighbor commands (ASNs, names, policies, addresses).""" commands = [] - if "remote_as_4_byte" in peer: - commands.append( - f"config protocol bgp peer {peer_addr} remote-asn {escape_shell_arg(str(peer['remote_as_4_byte']))}" - ) - if "peer_group_name" in peer: - commands.append( - f"config protocol bgp peer {peer_addr} peer-group {escape_shell_arg(peer['peer_group_name'])}" - ) - if "description" in peer: - commands.append( - f"config protocol bgp peer {peer_addr} description {escape_shell_arg(peer['description'])}" - ) - if "local_addr" in peer: - commands.append( - f"config protocol bgp peer {peer_addr} local-addr {escape_shell_arg(peer['local_addr'])}" - ) - if "ingress_policy_name" in peer: - commands.append( - f"config protocol bgp peer {peer_addr} ingress-policy {escape_shell_arg(peer['ingress_policy_name'])}" - ) - if "egress_policy_name" in peer: - commands.append( - f"config protocol bgp peer {peer_addr} egress-policy {escape_shell_arg(peer['egress_policy_name'])}" - ) + for field, cli_name, escape in _NEIGHBOR_SCALAR_FIELDS: + if field in peer: + value = escape_shell_arg(str(peer[field])) if escape else peer[field] + commands.append(f"{prefix} {cli_name} {value}") + if "is_passive" in peer: + commands.append(f"{prefix} passive {str(peer['is_passive']).lower()}") + if "link_bandwidth_bps" in peer: + bw = peer["link_bandwidth_bps"] + bw_str = format_bandwidth(bw) if isinstance(bw, int) else str(bw) + commands.append(f"{prefix} link-bandwidth {bw_str}") + if "advertise_link_bandwidth" in peer: + commands.append(f"{prefix} advertise-lbw {peer['advertise_link_bandwidth']}") + if "receive_link_bandwidth" in peer: + commands.append(f"{prefix} receive-lbw {peer['receive_link_bandwidth']}") return commands -def _generate_peer_bool_commands(peer: dict[str, Any], peer_addr: str) -> list[str]: - """Generate boolean flag commands for peer.""" +def _generate_neighbor_bool_commands(peer: dict[str, Any], prefix: str) -> list[str]: + """Generate boolean flag commands for a neighbor.""" commands = [] bool_fields = [ - ("is_passive", "passive"), ("is_rr_client", "rr-client"), + ("is_confed_peer", "confed-peer"), + ("is_redistribute_peer", "redistribute-peer"), + ("enhanced_route_refresh", "enhanced-route-refresh"), + ("disable_ipv4_afi", "afi disable-ipv4-afi"), + ("disable_ipv6_afi", "afi disable-ipv6-afi"), + ("v4_over_v6_nexthop", "afi ipv4-over-ipv6-nh"), + ("enable_stateful_ha", "graceful-restart stateful-ha"), ("next_hop_self", "next-hop-self"), - ("v4_over_v6_nexthop", "v4-over-v6-nh"), - ("disable_ipv4_afi", "disable-ipv4-afi"), - ("advertise_link_bandwidth", "advertise-lbw"), ] for field, cli_name in bool_fields: if field in peer: - commands.append( - f"config protocol bgp peer {peer_addr} {cli_name} {str(peer[field]).lower()}" - ) + commands.append(f"{prefix} {cli_name} {str(peer[field]).lower()}") return commands -def _generate_peer_nexthop_commands(peer: dict[str, Any], peer_addr: str) -> list[str]: - """Generate next-hop and link-bandwidth commands for peer.""" +def _generate_neighbor_add_path_commands( + peer: dict[str, Any], prefix: str +) -> list[str]: + """Generate add-path commands from the AddPath enum (RECEIVE=1, SEND=2, BOTH=3).""" + if "add_path" not in peer: + return [] + raw = peer["add_path"] + value = ( + {"RECEIVE": 1, "SEND": 2, "BOTH": 3}.get(raw, raw) + if isinstance(raw, str) + else raw + ) + value = int(value) commands = [] - if "next_hop4" in peer: - commands.append( - f"config protocol bgp peer {peer_addr} next-hop4 {escape_shell_arg(peer['next_hop4'])}" - ) - if "next_hop6" in peer: - commands.append( - f"config protocol bgp peer {peer_addr} next-hop6 {escape_shell_arg(peer['next_hop6'])}" - ) - if "link_bandwidth_bps" in peer: - bw = peer["link_bandwidth_bps"] - bw_str = format_bandwidth(bw) if isinstance(bw, int) else str(bw) - commands.append( - f"config protocol bgp peer {peer_addr} link-bandwidth {escape_shell_arg(bw_str)}" - ) + if value & 1: + commands.append(f"{prefix} add-path receive true") + if value & 2: + commands.append(f"{prefix} add-path send true") return commands -def _generate_peer_timer_commands(timers: dict[str, Any], peer_addr: str) -> list[str]: - """Generate timer commands for peer.""" +def _generate_neighbor_timer_commands(timers: dict[str, Any], prefix: str) -> list[str]: + """Generate timer and graceful-restart commands for a neighbor.""" commands = [] - if timers.get("hold_time_seconds"): - commands.append( - f"config protocol bgp peer {peer_addr} hold-time {escape_shell_arg(str(timers['hold_time_seconds']))}" - ) - if timers.get("keep_alive_seconds"): - commands.append( - f"config protocol bgp peer {peer_addr} timers keepalive {escape_shell_arg(str(timers['keep_alive_seconds']))}" - ) + if "hold_time_seconds" in timers: + commands.append(f"{prefix} timers hold-time {timers['hold_time_seconds']}") + if "keep_alive_seconds" in timers: + commands.append(f"{prefix} timers keepalive {timers['keep_alive_seconds']}") if "out_delay_seconds" in timers: + commands.append(f"{prefix} timers out-delay {timers['out_delay_seconds']}") + if "withdraw_unprog_delay_seconds" in timers: commands.append( - f"config protocol bgp peer {peer_addr} timers out-delay {escape_shell_arg(str(timers['out_delay_seconds']))}" + f"{prefix} timers withdraw-unprog-delay {timers['withdraw_unprog_delay_seconds']}" ) - if "withdraw_unprog_delay_seconds" in timers: + if "graceful_restart_seconds" in timers: commands.append( - f"config protocol bgp peer {peer_addr} timers withdraw-unprog-delay {escape_shell_arg(str(timers['withdraw_unprog_delay_seconds']))}" + f"{prefix} graceful-restart restart-time {timers['graceful_restart_seconds']}" ) return commands -def _generate_peer_prefilter_commands( - pre_filter: dict[str, Any], peer_addr: str +def _generate_neighbor_route_limit_commands( + peer: dict[str, Any], prefix: str ) -> list[str]: - """Generate pre_filter commands for peer.""" + """Generate max-route commands from pre_filter/post_filter.""" commands = [] - if pre_filter.get("max_routes"): - commands.append( - f"config protocol bgp peer {peer_addr} max-routes {escape_shell_arg(str(pre_filter['max_routes']))}" - ) + pre_filter = peer.get("pre_filter", {}) + if "max_routes" in pre_filter: + commands.append(f"{prefix} max-route pre-filter {pre_filter['max_routes']}") if "warning_limit" in pre_filter: commands.append( - f"config protocol bgp peer {peer_addr} warning-limit {escape_shell_arg(str(pre_filter['warning_limit']))}" + f"{prefix} max-route pre-warning-threshold {pre_filter['warning_limit']}" ) if "warning_only" in pre_filter: commands.append( - f"config protocol bgp peer {peer_addr} warning-only {str(pre_filter['warning_only']).lower()}" + f"{prefix} max-route pre-warning-only {str(pre_filter['warning_only']).lower()}" ) - return commands - - -def _generate_peer_id_commands(peer: dict[str, Any], peer_addr: str) -> list[str]: - """Generate peer identification commands.""" - commands = [] - if "peer_id" in peer: + post_filter = peer.get("post_filter", {}) + if "max_routes" in post_filter: + commands.append(f"{prefix} max-route post-filter {post_filter['max_routes']}") + if "warning_limit" in post_filter: commands.append( - f"config protocol bgp peer {peer_addr} peer-id {escape_shell_arg(peer['peer_id'])}" + f"{prefix} max-route post-warning-threshold {post_filter['warning_limit']}" ) - if "type" in peer: + if "warning_only" in post_filter: commands.append( - f"config protocol bgp peer {peer_addr} type {escape_shell_arg(peer['type'])}" + f"{prefix} max-route post-warning-only {str(post_filter['warning_only']).lower()}" ) return commands def generate_peer_commands(peer: dict[str, Any]) -> list[str]: - """Generate CLI commands for a peer.""" + """Generate `config protocol bgp neighbor` CLI commands for a peer.""" peer_addr = peer.get("peer_addr", "") if not peer_addr: return [] - escaped_peer_addr = escape_shell_arg(peer_addr) + prefix = f"config protocol bgp neighbor {escape_shell_arg(peer_addr)}" commands = [] - commands.extend(_generate_peer_basic_commands(peer, escaped_peer_addr)) - commands.extend(_generate_peer_bool_commands(peer, escaped_peer_addr)) - commands.extend(_generate_peer_nexthop_commands(peer, escaped_peer_addr)) + commands.extend(_generate_neighbor_basic_commands(peer, prefix)) + commands.extend(_generate_neighbor_bool_commands(peer, prefix)) + commands.extend(_generate_neighbor_add_path_commands(peer, prefix)) commands.extend( - _generate_peer_timer_commands( - peer.get("bgp_peer_timers", {}), escaped_peer_addr - ) + _generate_neighbor_timer_commands(peer.get("bgp_peer_timers", {}), prefix) ) - commands.extend( - _generate_peer_prefilter_commands(peer.get("pre_filter", {}), escaped_peer_addr) - ) - commands.extend(_generate_peer_id_commands(peer, escaped_peer_addr)) + commands.extend(_generate_neighbor_route_limit_commands(peer, prefix)) return commands -def generate_stub_commands(commands: list[str], binary: str = "fboss2") -> list[str]: - """Wrap commands for stub mode execution.""" - stub_commands = [ - "#!/bin/bash", - "# BGP configuration script generated by j2c", - "# Run with FBOSS_BGP_STUB_MODE=1 for testing", - "", - "export FBOSS_BGP_STUB_MODE=1", - "", - ] - for cmd in commands: - stub_commands.append(f"{binary} {cmd}") - return stub_commands - - def generate_exec_commands(commands: list[str], binary: str = "fboss2") -> list[str]: """Generate executable commands with custom binary.""" exec_commands = [ @@ -420,9 +400,7 @@ def generate_exec_commands(commands: list[str], binary: str = "fboss2") -> list[ return exec_commands -def json_to_cli( - config: dict[str, Any], stub: bool = False, binary: str = "fboss2" -) -> list[str]: +def json_to_cli(config: dict[str, Any], binary: str = "fboss2") -> list[str]: """Convert BGP++ JSON config to CLI commands.""" commands = [] @@ -437,9 +415,6 @@ def json_to_cli( for peer in config.get("peers", []): commands.extend(generate_peer_commands(peer)) - if stub: - return generate_stub_commands(commands, binary) - return generate_exec_commands(commands, binary) @@ -451,11 +426,6 @@ def main() -> int: parser.add_argument( "--output", "-o", help="Output file (default: stdout)", default=None ) - parser.add_argument( - "--stub", - action="store_true", - help="Generate stub mode script with FBOSS_BGP_STUB_MODE=1", - ) parser.add_argument( "--binary", "-b", @@ -489,7 +459,7 @@ def main() -> int: commands.extend(generate_peer_commands(peer)) output = "\n".join(commands) else: - commands = json_to_cli(config, stub=args.stub, binary=args.binary) + commands = json_to_cli(config, binary=args.binary) output = "\n".join(commands) if args.output: diff --git a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py index 265e9db61af00..59067c7df667b 100644 --- a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py @@ -24,7 +24,6 @@ generate_global_commands, generate_peer_commands, generate_peer_group_commands, - generate_stub_commands, json_to_cli, ) @@ -72,14 +71,19 @@ def test_backtick_injection_neutralized(self) -> None: result = escape_shell_arg("`reboot`") self.assertEqual(result, "'`reboot`'") + def test_command_separator_neutralized(self) -> None: + """Separators like ; & | must not split the generated command.""" + self.assertEqual(escape_shell_arg("x;id"), "'x;id'") + self.assertEqual(escape_shell_arg("a&&b|c"), "'a&&b|c'") + class FormatBandwidthTest(unittest.TestCase): """Tests for format_bandwidth function.""" - def test_terabits(self) -> None: - """Bandwidth in terabits should format as 'T'.""" + def test_terabits_stay_in_gigabits(self) -> None: + """bgpd's parser knows only K/M/G, so 1 Tbps must be emitted as 1000G.""" result = format_bandwidth(1_000_000_000_000) - self.assertEqual(result, "1T") + self.assertEqual(result, "1000G") def test_gigabits(self) -> None: """Bandwidth in gigabits should format as 'G'.""" @@ -353,7 +357,7 @@ def test_peer_tag(self) -> None: class GeneratePeerCommandsTest(unittest.TestCase): - """Tests for generate_peer_commands function.""" + """Tests for generate_peer_commands (neighbor grammar).""" def test_empty_peer_addr_returns_empty(self) -> None: """Peer without peer_addr should return empty list.""" @@ -365,36 +369,53 @@ def test_remote_asn(self) -> None: """remote_as_4_byte should generate correct command.""" peer = {"peer_addr": "2001:db8::1", "remote_as_4_byte": 65000} commands = generate_peer_commands(peer) - self.assertIn("config protocol bgp peer 2001:db8::1 remote-asn 65000", commands) + self.assertIn( + "config protocol bgp neighbor 2001:db8::1 remote-asn 65000", commands + ) + + def test_local_asn(self) -> None: + """local_as_4_byte should generate local-asn command.""" + peer = {"peer_addr": "2001:db8::1", "local_as_4_byte": 64512} + commands = generate_peer_commands(peer) + self.assertIn( + "config protocol bgp neighbor 2001:db8::1 local-asn 64512", commands + ) def test_peer_group_name(self) -> None: """peer_group_name should generate peer-group command.""" peer = {"peer_addr": "2001:db8::1", "peer_group_name": "MY-GROUP"} commands = generate_peer_commands(peer) self.assertIn( - "config protocol bgp peer 2001:db8::1 peer-group MY-GROUP", commands + "config protocol bgp neighbor 2001:db8::1 peer-group MY-GROUP", commands ) def test_local_addr(self) -> None: - """local_addr should generate local-addr command.""" + """local_addr should generate bind-addr address command.""" peer = {"peer_addr": "2001:db8::1", "local_addr": "2001:db8::a"} commands = generate_peer_commands(peer) self.assertIn( - "config protocol bgp peer 2001:db8::1 local-addr 2001:db8::a", commands + "config protocol bgp neighbor 2001:db8::1 bind-addr address 2001:db8::a", + commands, ) def test_is_passive(self) -> None: - """is_passive should generate passive command.""" + """is_passive should generate the passive flag.""" peer = {"peer_addr": "2001:db8::1", "is_passive": True} commands = generate_peer_commands(peer) - self.assertIn("config protocol bgp peer 2001:db8::1 passive true", commands) + self.assertIn("config protocol bgp neighbor 2001:db8::1 passive true", commands) + peer["is_passive"] = False + commands = generate_peer_commands(peer) + self.assertIn( + "config protocol bgp neighbor 2001:db8::1 passive false", commands + ) def test_disable_ipv4_afi_false_preserved(self) -> None: """disable_ipv4_afi: false should NOT be omitted.""" peer = {"peer_addr": "2001:db8::1", "disable_ipv4_afi": False} commands = generate_peer_commands(peer) self.assertIn( - "config protocol bgp peer 2001:db8::1 disable-ipv4-afi false", commands + "config protocol bgp neighbor 2001:db8::1 afi disable-ipv4-afi false", + commands, ) def test_disable_ipv4_afi_true(self) -> None: @@ -402,58 +423,40 @@ def test_disable_ipv4_afi_true(self) -> None: peer = {"peer_addr": "2001:db8::1", "disable_ipv4_afi": True} commands = generate_peer_commands(peer) self.assertIn( - "config protocol bgp peer 2001:db8::1 disable-ipv4-afi true", commands + "config protocol bgp neighbor 2001:db8::1 afi disable-ipv4-afi true", + commands, ) - def test_next_hop4_and_next_hop6(self) -> None: - """next_hop4 and next_hop6 should generate correct commands.""" - peer = { - "peer_addr": "2001:db8::1", - "next_hop4": "0.0.0.0", - "next_hop6": "2001:db8::a", - } + def test_stateful_ha(self) -> None: + """enable_stateful_ha should generate graceful-restart stateful-ha.""" + peer = {"peer_addr": "2001:db8::1", "enable_stateful_ha": True} commands = generate_peer_commands(peer) self.assertIn( - "config protocol bgp peer 2001:db8::1 next-hop4 0.0.0.0", commands - ) - self.assertIn( - "config protocol bgp peer 2001:db8::1 next-hop6 2001:db8::a", commands + "config protocol bgp neighbor 2001:db8::1 graceful-restart stateful-ha true", + commands, ) - def test_link_bandwidth_string(self) -> None: - """link_bandwidth_bps as string should be used directly.""" - peer = {"peer_addr": "2001:db8::1", "link_bandwidth_bps": "10G"} + def test_add_path_both(self) -> None: + """add_path BOTH (3) should generate both directions.""" + peer = {"peer_addr": "2001:db8::1", "add_path": 3} commands = generate_peer_commands(peer) self.assertIn( - "config protocol bgp peer 2001:db8::1 link-bandwidth 10G", commands + "config protocol bgp neighbor 2001:db8::1 add-path receive true", commands ) - - def test_link_bandwidth_int(self) -> None: - """link_bandwidth_bps as int should be formatted.""" - peer = {"peer_addr": "2001:db8::1", "link_bandwidth_bps": 10_000_000_000} - commands = generate_peer_commands(peer) self.assertIn( - "config protocol bgp peer 2001:db8::1 link-bandwidth 10G", commands + "config protocol bgp neighbor 2001:db8::1 add-path send true", commands ) - def test_advertise_link_bandwidth(self) -> None: - """advertise_link_bandwidth should generate advertise-lbw command.""" - peer = {"peer_addr": "2001:db8::1", "advertise_link_bandwidth": 1} - commands = generate_peer_commands(peer) - self.assertIn("config protocol bgp peer 2001:db8::1 advertise-lbw 1", commands) - - def test_peer_id_and_type(self) -> None: - """peer_id and type should generate correct commands.""" - peer = { - "peer_addr": "2001:db8::1", - "peer_id": "test-peer:v6:1", - "type": "BGP_MONITOR", - } + def test_add_path_receive_only(self) -> None: + """add_path RECEIVE (1) should generate only the receive direction.""" + peer = {"peer_addr": "2001:db8::1", "add_path": 1} commands = generate_peer_commands(peer) self.assertIn( - "config protocol bgp peer 2001:db8::1 peer-id test-peer:v6:1", commands + "config protocol bgp neighbor 2001:db8::1 add-path receive true", commands + ) + self.assertNotIn( + "config protocol bgp neighbor 2001:db8::1 add-path send true", commands ) - self.assertIn("config protocol bgp peer 2001:db8::1 type BGP_MONITOR", commands) def test_timers(self) -> None: """Timer fields should generate correct commands.""" @@ -463,35 +466,101 @@ def test_timers(self) -> None: "hold_time_seconds": 120, "keep_alive_seconds": 40, "out_delay_seconds": 0, - "withdraw_unprog_delay_seconds": 0, + "graceful_restart_seconds": 60, }, } commands = generate_peer_commands(peer) - self.assertIn("config protocol bgp peer 2001:db8::1 hold-time 120", commands) self.assertIn( - "config protocol bgp peer 2001:db8::1 timers keepalive 40", commands + "config protocol bgp neighbor 2001:db8::1 timers hold-time 120", commands + ) + self.assertIn( + "config protocol bgp neighbor 2001:db8::1 timers keepalive 40", commands + ) + self.assertIn( + "config protocol bgp neighbor 2001:db8::1 timers out-delay 0", commands + ) + self.assertIn( + "config protocol bgp neighbor 2001:db8::1 graceful-restart restart-time 60", + commands, + ) + + def test_route_limits(self) -> None: + """pre_filter/post_filter should generate max-route commands.""" + peer = { + "peer_addr": "2001:db8::1", + "pre_filter": {"max_routes": 45000}, + "post_filter": {"max_routes": 50000, "warning_limit": 48000}, + } + commands = generate_peer_commands(peer) + self.assertIn( + "config protocol bgp neighbor 2001:db8::1 max-route pre-filter 45000", + commands, + ) + self.assertIn( + "config protocol bgp neighbor 2001:db8::1 max-route post-filter 50000", + commands, + ) + self.assertIn( + "config protocol bgp neighbor 2001:db8::1 max-route post-warning-threshold 48000", + commands, ) - def test_pre_filter(self) -> None: - """pre_filter fields should generate correct commands.""" + def test_parity_fields(self) -> None: + """Fields carried over from the old peer commands map to the new grammar.""" peer = { "peer_addr": "2001:db8::1", - "pre_filter": { - "max_routes": 45000, - "warning_limit": 0, - "warning_only": False, - }, + "next_hop4": "0.0.0.0", + "next_hop6": "2001:db8::a", + "next_hop_self": True, + "link_bandwidth_bps": "10G", + "advertise_link_bandwidth": 1, + "receive_link_bandwidth": 1, + "peer_id": "test-peer:v6:1", + "type": "BGP_MONITOR", + "bgp_peer_timers": {"withdraw_unprog_delay_seconds": 5}, + "pre_filter": {"warning_limit": 100, "warning_only": True}, } commands = generate_peer_commands(peer) - self.assertIn("config protocol bgp peer 2001:db8::1 max-routes 45000", commands) - self.assertIn("config protocol bgp peer 2001:db8::1 warning-limit 0", commands) + prefix = "config protocol bgp neighbor 2001:db8::1" + self.assertIn(f"{prefix} next-hop4 0.0.0.0", commands) + self.assertIn(f"{prefix} next-hop6 2001:db8::a", commands) + self.assertIn(f"{prefix} next-hop-self true", commands) + self.assertIn(f"{prefix} link-bandwidth 10G", commands) + self.assertIn(f"{prefix} advertise-lbw 1", commands) + self.assertIn(f"{prefix} receive-lbw 1", commands) + self.assertIn(f"{prefix} peer-id test-peer:v6:1", commands) + self.assertIn(f"{prefix} type BGP_MONITOR", commands) + self.assertIn(f"{prefix} timers withdraw-unprog-delay 5", commands) + self.assertIn(f"{prefix} max-route pre-warning-threshold 100", commands) + self.assertIn(f"{prefix} max-route pre-warning-only true", commands) + + def test_zero_values_preserved(self) -> None: + """Explicit zero timers/limits must not be dropped.""" + peer = { + "peer_addr": "2001:db8::1", + "bgp_peer_timers": {"hold_time_seconds": 0, "keep_alive_seconds": 0}, + "pre_filter": {"max_routes": 0}, + } + commands = generate_peer_commands(peer) + prefix = "config protocol bgp neighbor 2001:db8::1" + self.assertIn(f"{prefix} timers hold-time 0", commands) + self.assertIn(f"{prefix} timers keepalive 0", commands) + self.assertIn(f"{prefix} max-route pre-filter 0", commands) + + def test_add_path_symbolic(self) -> None: + """add_path serialized as an enum name must not crash the tool.""" + peer = {"peer_addr": "2001:db8::1", "add_path": "BOTH"} + commands = generate_peer_commands(peer) + self.assertIn( + "config protocol bgp neighbor 2001:db8::1 add-path receive true", commands + ) self.assertIn( - "config protocol bgp peer 2001:db8::1 warning-only false", commands + "config protocol bgp neighbor 2001:db8::1 add-path send true", commands ) class GenerateScriptCommandsTest(unittest.TestCase): - """Tests for generate_exec_commands and generate_stub_commands.""" + """Tests for generate_exec_commands.""" def test_exec_commands_contains_header(self) -> None: """Exec commands should contain shebang and header.""" @@ -509,12 +578,6 @@ def test_exec_commands_prepends_binary(self) -> None: "/path/to/fboss2 config protocol bgp global router-id 10.0.0.1", result ) - def test_stub_commands_contains_stub_env(self) -> None: - """Stub commands should set FBOSS_BGP_STUB_MODE.""" - commands = ["test command"] - result = generate_stub_commands(commands, "fboss2") - self.assertIn("export FBOSS_BGP_STUB_MODE=1", result) - class JsonToCliIntegrationTest(unittest.TestCase): """Integration tests for json_to_cli function.""" @@ -522,7 +585,7 @@ class JsonToCliIntegrationTest(unittest.TestCase): def test_minimal_config(self) -> None: """Minimal config with just router_id should work.""" config = {"router_id": "10.0.0.1"} - commands = json_to_cli(config, stub=False, binary="fboss2") + commands = json_to_cli(config, binary="fboss2") self.assertTrue(any("router-id 10.0.0.1" in cmd for cmd in commands)) def test_full_rsw_config(self) -> None: @@ -571,7 +634,7 @@ def test_full_rsw_config(self) -> None: ], } - commands = json_to_cli(config, stub=False, binary="fboss2") + commands = json_to_cli(config, binary="fboss2") joined = "\n".join(commands) # Verify global commands @@ -587,44 +650,38 @@ def test_full_rsw_config(self) -> None: self.assertIn("peer-group RSW-RTSW-V6 disable-ipv4-afi true", joined) # Verify peer commands - self.assertIn("peer 2401:db00:501c::/64 remote-asn 65000", joined) - self.assertIn("peer 2401:db00:501c::/64 disable-ipv4-afi false", joined) - self.assertIn("peer 2401:db00:e50e:1000:: peer-group RSW-FSW-V6", joined) - - def test_stub_mode(self) -> None: - """Stub mode should set FBOSS_BGP_STUB_MODE environment variable.""" - config = {"router_id": "10.0.0.1"} - commands = json_to_cli(config, stub=True, binary="fboss2") - self.assertIn("export FBOSS_BGP_STUB_MODE=1", commands) + self.assertIn("neighbor 2401:db00:501c::/64 remote-asn 65000", joined) + self.assertIn("neighbor 2401:db00:501c::/64 afi disable-ipv4-afi false", joined) + self.assertIn("neighbor 2401:db00:e50e:1000:: peer-group RSW-FSW-V6", joined) def test_empty_config(self) -> None: """Empty config should produce script with no commands.""" config = {} - commands = json_to_cli(config, stub=False, binary="fboss2") + commands = json_to_cli(config, binary="fboss2") # Should only have header and footer, no actual BGP commands self.assertTrue(any("#!/bin/bash" in cmd for cmd in commands)) def test_custom_binary(self) -> None: """Custom binary path should be used in commands.""" config = {"router_id": "10.0.0.1"} - commands = json_to_cli(config, stub=False, binary="/custom/path/fboss2") + commands = json_to_cli(config, binary="/custom/path/fboss2") self.assertTrue(any("/custom/path/fboss2" in cmd for cmd in commands)) def test_injection_in_router_id_neutralized(self) -> None: - """Command injection in router_id should be single-quoted.""" + """Command injection in router_id should be escaped.""" config = {"router_id": "$(reboot)"} - commands = json_to_cli(config, stub=False, binary="fboss2") + commands = json_to_cli(config, binary="fboss2") joined = "\n".join(commands) self.assertIn("router-id '$(reboot)'", joined) self.assertNotIn("router-id $(reboot)", joined) def test_injection_in_peer_addr_neutralized(self) -> None: - """Command injection in peer_addr should be single-quoted.""" + """Command injection in peer_addr should be escaped.""" config = {"peers": [{"peer_addr": "$(malicious)", "remote_as_4_byte": 65000}]} - commands = json_to_cli(config, stub=False, binary="fboss2") + commands = json_to_cli(config, binary="fboss2") joined = "\n".join(commands) - self.assertIn("peer '$(malicious)'", joined) - self.assertNotIn("peer $(malicious)", joined) + self.assertIn("neighbor '$(malicious)'", joined) + self.assertNotIn("neighbor $(malicious)", joined) if __name__ == "__main__": From b1d05fb9038be05e29ba85b43e712b6e369bacfe Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Wed, 9 Sep 2026 15:01:10 +0000 Subject: [PATCH 07/22] [fboss2] bgp j2c: shell-quote every JSON-sourced value Review feedback on #1391: the generator quoted free-form strings but emitted numeric/boolean fields raw. The input JSON is not type-checked, so a string in an ASN or timer field reached the generated script unquoted. Quote every value uniformly; shlex.quote is a no-op for valid integers and booleans. --- fboss/cli/fboss2/tools/bgp_json_to_cli.py | 138 +++++++++++------- .../tools/tests/test_bgp_json_to_cli.py | 50 +++++++ 2 files changed, 133 insertions(+), 55 deletions(-) diff --git a/fboss/cli/fboss2/tools/bgp_json_to_cli.py b/fboss/cli/fboss2/tools/bgp_json_to_cli.py index 05136ee8ec7bb..c9e7cd7e27708 100644 --- a/fboss/cli/fboss2/tools/bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/bgp_json_to_cli.py @@ -23,27 +23,38 @@ from typing import Any -def escape_shell_arg(arg: str) -> str: - """Escape argument for shell usage.""" +def escape_shell_arg(arg: Any) -> str: + """Quote a JSON-sourced value for the generated shell script. + + Applied to every value taken from the input JSON, numeric fields included: + the script does not validate types, and shlex.quote leaves plain digits + and identifiers unchanged, so valid input produces identical output. + """ return shlex.quote(str(arg)) +def _shell_bool(value: Any) -> str: + return escape_shell_arg(str(value).lower()) + + def _generate_global_basic_commands(config: dict[str, Any]) -> list[str]: """Generate basic global BGP commands.""" commands = [] if "router_id" in config: commands.append( - f"config protocol bgp global router-id {escape_shell_arg(str(config['router_id']))}" + f"config protocol bgp global router-id {escape_shell_arg(config['router_id'])}" ) if "local_as_4_byte" in config: commands.append( - f"config protocol bgp global local-asn {config['local_as_4_byte']}" + f"config protocol bgp global local-asn {escape_shell_arg(config['local_as_4_byte'])}" ) if "hold_time" in config: - commands.append(f"config protocol bgp global hold-time {config['hold_time']}") + commands.append( + f"config protocol bgp global hold-time {escape_shell_arg(config['hold_time'])}" + ) if "local_confed_as_4_byte" in config: commands.append( - f"config protocol bgp global confed-asn {config['local_confed_as_4_byte']}" + f"config protocol bgp global confed-asn {escape_shell_arg(config['local_confed_as_4_byte'])}" ) return commands @@ -55,13 +66,17 @@ def _generate_network6_commands(networks: list[dict[str, Any]]) -> list[str]: prefix = network.get("prefix", "") if not prefix: continue - cmd_parts = [f"config protocol bgp global network6 add {prefix}"] + cmd_parts = [ + f"config protocol bgp global network6 add {escape_shell_arg(prefix)}" + ] if "policy_name" in network: cmd_parts.append(f"policy {escape_shell_arg(network['policy_name'])}") if "install_to_fib" in network: - cmd_parts.append(f"install-to-fib {str(network['install_to_fib']).lower()}") + cmd_parts.append(f"install-to-fib {_shell_bool(network['install_to_fib'])}") if "minimum_supporting_routes" in network: - cmd_parts.append(f"min-routes {network['minimum_supporting_routes']}") + cmd_parts.append( + f"min-routes {escape_shell_arg(network['minimum_supporting_routes'])}" + ) commands.append(" ".join(cmd_parts)) return commands @@ -71,19 +86,19 @@ def _generate_switch_limit_commands(switch_limit: dict[str, Any]) -> list[str]: commands = [] if "prefix_limit" in switch_limit: commands.append( - f"config protocol bgp global switch-limit {switch_limit['prefix_limit']}" + f"config protocol bgp global switch-limit {escape_shell_arg(switch_limit['prefix_limit'])}" ) if "total_path_limit" in switch_limit: commands.append( - f"config protocol bgp global switch-limit-total-path {switch_limit['total_path_limit']}" + f"config protocol bgp global switch-limit-total-path {escape_shell_arg(switch_limit['total_path_limit'])}" ) if "max_golden_vips" in switch_limit: commands.append( - f"config protocol bgp global switch-limit-max-golden-vips {switch_limit['max_golden_vips']}" + f"config protocol bgp global switch-limit-max-golden-vips {escape_shell_arg(switch_limit['max_golden_vips'])}" ) if "overload_protection_mode" in switch_limit: commands.append( - f"config protocol bgp global switch-limit-overload-protection-mode {switch_limit['overload_protection_mode']}" + f"config protocol bgp global switch-limit-overload-protection-mode {escape_shell_arg(switch_limit['overload_protection_mode'])}" ) return commands @@ -106,7 +121,7 @@ def _generate_peer_group_basic_commands( commands = [] if "remote_as_4_byte" in peer_group: commands.append( - f"config protocol bgp peer-group {escaped_name} remote-asn {peer_group['remote_as_4_byte']}" + f"config protocol bgp peer-group {escaped_name} remote-asn {escape_shell_arg(peer_group['remote_as_4_byte'])}" ) if "description" in peer_group: commands.append( @@ -142,7 +157,7 @@ def _generate_peer_group_bool_commands( for field, cli_name in bool_fields: if field in peer_group: commands.append( - f"config protocol bgp peer-group {escaped_name} {cli_name} {str(peer_group[field]).lower()}" + f"config protocol bgp peer-group {escaped_name} {cli_name} {_shell_bool(peer_group[field])}" ) return commands @@ -154,19 +169,19 @@ def _generate_peer_group_timer_commands( commands = [] if timers.get("hold_time_seconds"): commands.append( - f"config protocol bgp peer-group {escaped_name} timers hold-time {timers['hold_time_seconds']}" + f"config protocol bgp peer-group {escaped_name} timers hold-time {escape_shell_arg(timers['hold_time_seconds'])}" ) if timers.get("keep_alive_seconds"): commands.append( - f"config protocol bgp peer-group {escaped_name} timers keepalive {timers['keep_alive_seconds']}" + f"config protocol bgp peer-group {escaped_name} timers keepalive {escape_shell_arg(timers['keep_alive_seconds'])}" ) if "out_delay_seconds" in timers: commands.append( - f"config protocol bgp peer-group {escaped_name} timers out-delay {timers['out_delay_seconds']}" + f"config protocol bgp peer-group {escaped_name} timers out-delay {escape_shell_arg(timers['out_delay_seconds'])}" ) if "withdraw_unprog_delay_seconds" in timers: commands.append( - f"config protocol bgp peer-group {escaped_name} timers withdraw-unprog-delay {timers['withdraw_unprog_delay_seconds']}" + f"config protocol bgp peer-group {escaped_name} timers withdraw-unprog-delay {escape_shell_arg(timers['withdraw_unprog_delay_seconds'])}" ) return commands @@ -178,15 +193,15 @@ def _generate_peer_group_prefilter_commands( commands = [] if pre_filter.get("max_routes"): commands.append( - f"config protocol bgp peer-group {escaped_name} max-routes {pre_filter['max_routes']}" + f"config protocol bgp peer-group {escaped_name} max-routes {escape_shell_arg(pre_filter['max_routes'])}" ) if "warning_limit" in pre_filter: commands.append( - f"config protocol bgp peer-group {escaped_name} warning-limit {pre_filter['warning_limit']}" + f"config protocol bgp peer-group {escaped_name} warning-limit {escape_shell_arg(pre_filter['warning_limit'])}" ) if "warning_only" in pre_filter: commands.append( - f"config protocol bgp peer-group {escaped_name} warning-only {str(pre_filter['warning_only']).lower()}" + f"config protocol bgp peer-group {escaped_name} warning-only {_shell_bool(pre_filter['warning_only'])}" ) return commands @@ -229,41 +244,44 @@ def format_bandwidth(bps: int) -> str: return str(bps) -# (json field, CLI attribute, escape as free-form shell arg) — fields whose -# value maps 1:1 onto a neighbor attribute token. +# (json field, CLI attribute) — fields whose value maps 1:1 onto a neighbor +# attribute token. _NEIGHBOR_SCALAR_FIELDS = [ - ("remote_as_4_byte", "remote-asn", False), - ("local_as_4_byte", "local-asn", False), - ("peer_group_name", "peer-group", True), - ("description", "description", True), - ("peer_tag", "peer-tag", True), - ("local_addr", "bind-addr address", True), - ("ingress_policy_name", "ingress-policy", True), - ("egress_policy_name", "egress-policy", True), - ("next_hop4", "next-hop4", True), - ("next_hop6", "next-hop6", True), - ("peer_id", "peer-id", True), - ("type", "type", True), + ("remote_as_4_byte", "remote-asn"), + ("local_as_4_byte", "local-asn"), + ("peer_group_name", "peer-group"), + ("description", "description"), + ("peer_tag", "peer-tag"), + ("local_addr", "bind-addr address"), + ("ingress_policy_name", "ingress-policy"), + ("egress_policy_name", "egress-policy"), + ("next_hop4", "next-hop4"), + ("next_hop6", "next-hop6"), + ("peer_id", "peer-id"), + ("type", "type"), ] def _generate_neighbor_basic_commands(peer: dict[str, Any], prefix: str) -> list[str]: """Generate scalar neighbor commands (ASNs, names, policies, addresses).""" commands = [] - for field, cli_name, escape in _NEIGHBOR_SCALAR_FIELDS: + for field, cli_name in _NEIGHBOR_SCALAR_FIELDS: if field in peer: - value = escape_shell_arg(str(peer[field])) if escape else peer[field] - commands.append(f"{prefix} {cli_name} {value}") + commands.append(f"{prefix} {cli_name} {escape_shell_arg(peer[field])}") if "is_passive" in peer: - commands.append(f"{prefix} passive {str(peer['is_passive']).lower()}") + commands.append(f"{prefix} passive {_shell_bool(peer['is_passive'])}") if "link_bandwidth_bps" in peer: bw = peer["link_bandwidth_bps"] bw_str = format_bandwidth(bw) if isinstance(bw, int) else str(bw) - commands.append(f"{prefix} link-bandwidth {bw_str}") + commands.append(f"{prefix} link-bandwidth {escape_shell_arg(bw_str)}") if "advertise_link_bandwidth" in peer: - commands.append(f"{prefix} advertise-lbw {peer['advertise_link_bandwidth']}") + commands.append( + f"{prefix} advertise-lbw {escape_shell_arg(peer['advertise_link_bandwidth'])}" + ) if "receive_link_bandwidth" in peer: - commands.append(f"{prefix} receive-lbw {peer['receive_link_bandwidth']}") + commands.append( + f"{prefix} receive-lbw {escape_shell_arg(peer['receive_link_bandwidth'])}" + ) return commands @@ -283,7 +301,7 @@ def _generate_neighbor_bool_commands(peer: dict[str, Any], prefix: str) -> list[ ] for field, cli_name in bool_fields: if field in peer: - commands.append(f"{prefix} {cli_name} {str(peer[field]).lower()}") + commands.append(f"{prefix} {cli_name} {_shell_bool(peer[field])}") return commands @@ -312,18 +330,24 @@ def _generate_neighbor_timer_commands(timers: dict[str, Any], prefix: str) -> li """Generate timer and graceful-restart commands for a neighbor.""" commands = [] if "hold_time_seconds" in timers: - commands.append(f"{prefix} timers hold-time {timers['hold_time_seconds']}") + commands.append( + f"{prefix} timers hold-time {escape_shell_arg(timers['hold_time_seconds'])}" + ) if "keep_alive_seconds" in timers: - commands.append(f"{prefix} timers keepalive {timers['keep_alive_seconds']}") + commands.append( + f"{prefix} timers keepalive {escape_shell_arg(timers['keep_alive_seconds'])}" + ) if "out_delay_seconds" in timers: - commands.append(f"{prefix} timers out-delay {timers['out_delay_seconds']}") + commands.append( + f"{prefix} timers out-delay {escape_shell_arg(timers['out_delay_seconds'])}" + ) if "withdraw_unprog_delay_seconds" in timers: commands.append( - f"{prefix} timers withdraw-unprog-delay {timers['withdraw_unprog_delay_seconds']}" + f"{prefix} timers withdraw-unprog-delay {escape_shell_arg(timers['withdraw_unprog_delay_seconds'])}" ) if "graceful_restart_seconds" in timers: commands.append( - f"{prefix} graceful-restart restart-time {timers['graceful_restart_seconds']}" + f"{prefix} graceful-restart restart-time {escape_shell_arg(timers['graceful_restart_seconds'])}" ) return commands @@ -335,25 +359,29 @@ def _generate_neighbor_route_limit_commands( commands = [] pre_filter = peer.get("pre_filter", {}) if "max_routes" in pre_filter: - commands.append(f"{prefix} max-route pre-filter {pre_filter['max_routes']}") + commands.append( + f"{prefix} max-route pre-filter {escape_shell_arg(pre_filter['max_routes'])}" + ) if "warning_limit" in pre_filter: commands.append( - f"{prefix} max-route pre-warning-threshold {pre_filter['warning_limit']}" + f"{prefix} max-route pre-warning-threshold {escape_shell_arg(pre_filter['warning_limit'])}" ) if "warning_only" in pre_filter: commands.append( - f"{prefix} max-route pre-warning-only {str(pre_filter['warning_only']).lower()}" + f"{prefix} max-route pre-warning-only {_shell_bool(pre_filter['warning_only'])}" ) post_filter = peer.get("post_filter", {}) if "max_routes" in post_filter: - commands.append(f"{prefix} max-route post-filter {post_filter['max_routes']}") + commands.append( + f"{prefix} max-route post-filter {escape_shell_arg(post_filter['max_routes'])}" + ) if "warning_limit" in post_filter: commands.append( - f"{prefix} max-route post-warning-threshold {post_filter['warning_limit']}" + f"{prefix} max-route post-warning-threshold {escape_shell_arg(post_filter['warning_limit'])}" ) if "warning_only" in post_filter: commands.append( - f"{prefix} max-route post-warning-only {str(post_filter['warning_only']).lower()}" + f"{prefix} max-route post-warning-only {_shell_bool(post_filter['warning_only'])}" ) return commands diff --git a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py index 59067c7df667b..d029545228924 100644 --- a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py @@ -683,6 +683,56 @@ def test_injection_in_peer_addr_neutralized(self) -> None: self.assertIn("neighbor '$(malicious)'", joined) self.assertNotIn("neighbor $(malicious)", joined) + def test_injection_in_numeric_and_bool_fields_neutralized(self) -> None: + """Fields expected to be numeric or boolean are quoted like any other.""" + config = { + "local_as_4_byte": "65000;id", + "networks6": [{"prefix": "2401::/64;id", "install_to_fib": "true|id"}], + "peer_groups": [{"name": "g", "remote_as_4_byte": "1&&id"}], + "peers": [ + { + "peer_addr": "10.0.0.1", + "remote_as_4_byte": "2;id", + "is_rr_client": "false;id", + "bgp_peer_timers": {"hold_time_seconds": "90;id"}, + "pre_filter": {"max_routes": "5|id"}, + } + ], + } + joined = "\n".join(json_to_cli(config, binary="fboss2")) + for payload in ( + "local-asn '65000;id'", + "network6 add '2401::/64;id' install-to-fib 'true|id'", + "remote-asn '1&&id'", + "remote-asn '2;id'", + "rr-client 'false;id'", + "hold-time '90;id'", + "pre-filter '5|id'", + ): + self.assertIn(payload, joined) + for line in joined.splitlines(): + self.assertNotRegex(line, r"[;|&]id\b(?!')") + + def test_valid_numeric_fields_render_unquoted(self) -> None: + """Quoting is a no-op for well-formed integers and booleans.""" + config = { + "local_as_4_byte": 65000, + "peers": [ + { + "peer_addr": "10.0.0.1", + "remote_as_4_byte": 65001, + "is_rr_client": True, + "bgp_peer_timers": {"hold_time_seconds": 90}, + } + ], + } + joined = "\n".join(json_to_cli(config, binary="fboss2")) + self.assertIn("local-asn 65000", joined) + self.assertIn("remote-asn 65001", joined) + self.assertIn("rr-client true", joined) + self.assertIn("hold-time 90", joined) + self.assertNotIn("'", joined.split("set -e")[1].split("echo")[0]) + if __name__ == "__main__": unittest.main() From 2fdb6673eb2e83e31318df2a0179767d7d279b29 Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Thu, 10 Sep 2026 07:35:34 +0000 Subject: [PATCH 08/22] [fboss2] bgp neighbor: seed unlimited route limit, add session knobs, flag unconverted j2c fields Route limits: when neither the peer nor its group carries a RouteLimit, the first warning-only/threshold edit seeded the thrift default (max_routes 12000, a hard cap with session teardown) where bgpd enforced nothing. Seed {max_routes 0, warning_only false, warning_limit 0} instead: bgpd reads 0 as unlimited, so only the requested field changes. Attributes: add remove-private-as, enforce-first-as, route-refresh and ttl-security-hops (1..255, bgpd's kMin/kMaxTtlSecurityHops), the remaining BgpPeer fields bgpd resolves peer > group. Document the fields deliberately not exposed (enabled, router_port_id, graceful_restart_end_of_rib_seconds: nothing in bgpd reads them). bgp_json_to_cli.py: shared bool/int/link-bandwidth tables for the fields BgpPeer and PeerGroup have in common; deprecated i32 ASNs map to the 4-byte attribute; any JSON field with no CLI equivalent emits a `# WARNING` comment line in the script and on stderr instead of being dropped silently. --- .../neighbor/CmdConfigProtocolBgpNeighbor.cpp | 68 +++- .../test/config/CmdConfigBgpNeighborTest.cpp | 57 +++ .../ConfigBgpNeighborTest.cpp | 39 ++ fboss/cli/fboss2/tools/bgp_json_to_cli.py | 340 +++++++++++------- .../tools/tests/test_bgp_json_to_cli.py | 201 +++++++++++ 5 files changed, 566 insertions(+), 139 deletions(-) diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.cpp index 1df7813f49e63..4a5799148cf0a 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.cpp +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.cpp @@ -53,6 +53,10 @@ constexpr std::string_view kRrClient = "rr-client"; constexpr std::string_view kConfedPeer = "confed-peer"; constexpr std::string_view kRedistributePeer = "redistribute-peer"; constexpr std::string_view kEnhancedRouteRefresh = "enhanced-route-refresh"; +constexpr std::string_view kRouteRefresh = "route-refresh"; +constexpr std::string_view kRemovePrivateAs = "remove-private-as"; +constexpr std::string_view kEnforceFirstAs = "enforce-first-as"; +constexpr std::string_view kTtlSecurityHops = "ttl-security-hops"; constexpr std::string_view kPassive = "passive"; constexpr std::string_view kPeerPort = "peer-port"; constexpr std::string_view kAddPathSend = "add-path send"; @@ -94,13 +98,20 @@ constexpr std::string_view kMaxRoutePreWarningOnly = "max-route pre-warning-only"; constexpr std::string_view kMaxRoutePostWarningOnly = "max-route post-warning-only"; +// BgpPeer fields deliberately NOT exposed: `enabled` and `router_port_id` +// (nothing in bgpd reads them) and +// bgp_peer_timers.graceful_restart_end_of_rib_seconds (same). Staging them +// would persist config the daemon ignores. // Protocol ranges bgpd enforces (or the wire format silently truncates to): // the OPEN hold time is 16 bits and bgpd refuses 1-2s; the graceful-restart -// restart time is the 12-bit field of RFC 4724. +// restart time is the 12-bit field of RFC 4724; TTL security hops are +// bgpd's kMin/kMaxTtlSecurityHops (it throws on anything else at load). constexpr int32_t kHoldTimeMinSeconds = 3; constexpr int32_t kHoldTimeMaxSeconds = 65535; constexpr int32_t kGracefulRestartMaxSeconds = 4095; +constexpr int32_t kTtlSecurityHopsMin = 1; +constexpr int32_t kTtlSecurityHopsMax = 255; // Value placeholders for the generated help. One per value shape, so the // wording matches the factory that validates that shape. @@ -118,6 +129,7 @@ constexpr std::string_view kUsageHoldTime = ""; constexpr std::string_view kUsageGracefulRestart = ""; constexpr std::string_view kUsageRouteCount = ""; constexpr std::string_view kUsageBitRate = "[K|M|G] | auto"; +constexpr std::string_view kUsageTtlHops = ""; using bgpcli::Tokens; using BgpConfig = bgp::thrift::BgpConfig; @@ -128,6 +140,7 @@ using bgpcli::AttrHandler; using bgpcli::bitRateAttr; using bgpcli::boolAttr; using bgpcli::err; +using bgpcli::intAttr; using bgpcli::ipAttr; using bgpcli::joinedStringAttr; using bgpcli::ok; @@ -193,9 +206,27 @@ void setEnhancedRouteRefresh(BgpPeer& p, bool v) { p.enhanced_route_refresh() = v; } +void setRouteRefresh(BgpPeer& p, bool v) { + p.route_refresh() = v; +} + +void setRemovePrivateAs(BgpPeer& p, bool v) { + p.remove_private_as() = v; +} + +void setEnforceFirstAs(BgpPeer& p, bool v) { + p.enforce_first_as() = v; +} + +void setTtlSecurityHops(BgpPeer& p, int32_t v) { + p.ttl_security_hops() = v; +} + // bgpd reads is_passive=true as PASSIVE_ONLY (listen) and false as -// PASSIVE_ACTIVE (listen and connect); there is no active-only mode, so the -// CLI exposes the flag itself rather than inventing connect-mode names. +// PASSIVE_ACTIVE (listen and connect). Its session code also knows an +// ACTIVE_ONLY mode, but the config model has no way to request it (the mode +// is derived from is_passive alone), so the CLI exposes the flag itself +// rather than inventing connect-mode names. void setPassive(BgpPeer& p, bool v) { p.is_passive() = v; } @@ -344,9 +375,18 @@ void seedRouteLimit(const BgpConfig& cfg, BgpPeer& peer, bool pre) { } if (const auto* group = peerGroupOf(cfg, peer); group && field(*group)) { field(peer) = *field(*group); + return; } - // Otherwise the setter's ensure() yields the RouteLimit thrift defaults, - // which is also what bgpd applies to a peer without one. + // No limit anywhere above: bgpd applies none to a peer without a struct + // (capRoutesPerPeer returns early) and reads max_routes == 0 as unlimited, + // so seed that. The thrift default the setter's ensure() would leave behind + // is a 12000-route hard cap with session teardown -- a warning-only or + // threshold edit must not introduce one. + bgp::thrift::RouteLimit unlimited; + unlimited.max_routes() = 0; + unlimited.warning_only() = false; + unlimited.warning_limit() = 0; + field(peer) = std::move(unlimited); } // A neighbor handler sees the whole config (for the peer group lookup) plus @@ -531,6 +571,24 @@ const std::map>& attrHandlers() { plain( kUsageBool, boolAttr(kEnhancedRouteRefresh, setEnhancedRouteRefresh))}, + {std::string(kRouteRefresh), + plain(kUsageBool, boolAttr(kRouteRefresh, setRouteRefresh))}, + {std::string(kRemovePrivateAs), + plain( + kUsageBool, + boolAttr(kRemovePrivateAs, setRemovePrivateAs))}, + {std::string(kEnforceFirstAs), + plain( + kUsageBool, boolAttr(kEnforceFirstAs, setEnforceFirstAs))}, + {std::string(kTtlSecurityHops), + plain( + kUsageTtlHops, + intAttr( + kTtlSecurityHops, + "1-255", + kTtlSecurityHopsMin, + kTtlSecurityHopsMax, + setTtlSecurityHops))}, {std::string(kPassive), plain(kUsageBool, boolAttr(kPassive, setPassive))}, {std::string(kPeerPort), rejected(kPeerPort, kNoPeerPortField)}, diff --git a/fboss/cli/fboss2/test/config/CmdConfigBgpNeighborTest.cpp b/fboss/cli/fboss2/test/config/CmdConfigBgpNeighborTest.cpp index 00322dec76dcd..141e879b7f48f 100644 --- a/fboss/cli/fboss2/test/config/CmdConfigBgpNeighborTest.cpp +++ b/fboss/cli/fboss2/test/config/CmdConfigBgpNeighborTest.cpp @@ -174,6 +174,9 @@ TEST_F(CmdConfigBgpNeighborTestFixture, boolAndStringAttributes) { run({"10.0.0.1", "confed-peer", "false"}); run({"10.0.0.1", "redistribute-peer", "true"}); run({"10.0.0.1", "enhanced-route-refresh", "true"}); + run({"10.0.0.1", "route-refresh", "true"}); + run({"10.0.0.1", "remove-private-as", "true"}); + run({"10.0.0.1", "enforce-first-as", "false"}); run({"10.0.0.1", "afi", "disable-ipv4-afi", "true"}); run({"10.0.0.1", "afi", "disable-ipv6-afi", "false"}); run({"10.0.0.1", "afi", "ipv4-over-ipv6-nh", "true"}); @@ -192,6 +195,9 @@ TEST_F(CmdConfigBgpNeighborTestFixture, boolAndStringAttributes) { EXPECT_FALSE(peer.is_confed_peer().value_or(true)); EXPECT_TRUE(peer.is_redistribute_peer().value_or(false)); EXPECT_TRUE(peer.enhanced_route_refresh().value_or(false)); + EXPECT_TRUE(peer.route_refresh().value_or(false)); + EXPECT_TRUE(peer.remove_private_as().value_or(false)); + EXPECT_FALSE(peer.enforce_first_as().value_or(true)); EXPECT_TRUE(peer.disable_ipv4_afi().value_or(false)); EXPECT_FALSE(peer.disable_ipv6_afi().value_or(true)); EXPECT_TRUE(peer.v4_over_v6_nexthop().value_or(false)); @@ -384,6 +390,52 @@ TEST_F(CmdConfigBgpNeighborTestFixture, routeLimitSeededFromPeerGroup) { EXPECT_FALSE(peers()[0].post_filter().has_value()); } +TEST_F(CmdConfigBgpNeighborTestFixture, routeLimitSeededUnlimitedWithoutGroup) { + // Neither the peer nor its group carries a RouteLimit: bgpd applies no cap + // at all, so the struct created for a warning-only/threshold edit must keep + // that (max_routes 0 = unlimited) rather than the 12000 thrift default. + run({"10.0.0.1", "max-route", "pre-warning-only", "true"}); + const auto& pre = *peers()[0].pre_filter(); + EXPECT_EQ(*pre.max_routes(), 0); + EXPECT_EQ(*pre.warning_limit(), 0); + EXPECT_TRUE(*pre.warning_only()); + EXPECT_FALSE(peers()[0].post_filter().has_value()); + + run({"10.0.0.1", "max-route", "post-warning-threshold", "30000"}); + const auto& post = *peers()[0].post_filter(); + EXPECT_EQ(*post.max_routes(), 0); + EXPECT_EQ(*post.warning_limit(), 30000); + EXPECT_FALSE(*post.warning_only()); + + // A group with no struct of its own is the same as no group. + addPeerGroup("SPINE"); + run({"10.0.0.2", "peer-group", "SPINE"}); + run({"10.0.0.2", "max-route", "pre-warning-threshold", "30000"}); + EXPECT_EQ(*peers()[1].pre_filter()->max_routes(), 0); + EXPECT_EQ(*peers()[1].pre_filter()->warning_limit(), 30000); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, routeLimitNotReseededOncePresent) { + run({"10.0.0.1", "max-route", "pre-filter", "45000"}); + run({"10.0.0.1", "max-route", "pre-warning-threshold", "30000"}); + EXPECT_EQ(*peers()[0].pre_filter()->max_routes(), 45000); + EXPECT_EQ(*peers()[0].pre_filter()->warning_limit(), 30000); +} + +TEST_F(CmdConfigBgpNeighborTestFixture, ttlSecurityHopsRange) { + // bgpd throws at config load outside kMin/kMaxTtlSecurityHops (1..255). + EXPECT_THAT(run({"10.0.0.1", "ttl-security-hops", "0"}), HasSubstr("Error")); + EXPECT_THAT( + run({"10.0.0.1", "ttl-security-hops", "256"}), HasSubstr("Error")); + EXPECT_THAT(run({"10.0.0.1", "ttl-security-hops", "x"}), HasSubstr("Error")); + EXPECT_TRUE(peers().empty()) + << "rejected first attribute must not leave a half-created peer"; + run({"10.0.0.1", "ttl-security-hops", "1"}); + EXPECT_EQ(peers()[0].ttl_security_hops().value_or(0), 1); + run({"10.0.0.1", "ttl-security-hops", "255"}); + EXPECT_EQ(peers()[0].ttl_security_hops().value_or(0), 255); +} + TEST_F(CmdConfigBgpNeighborTestFixture, addPathMergesWithPeerGroupValue) { addPeerGroup("SPINE").add_path() = AddPath::SEND; run({"10.0.0.1", "peer-group", "SPINE"}); @@ -455,6 +507,11 @@ TEST_F(CmdConfigBgpNeighborTestFixture, rejectedValueLeavesPeerUntouched) { EXPECT_THAT( run({"10.0.0.1", "timers", "hold-time", "1"}), HasSubstr("Error")); EXPECT_FALSE(peers()[0].bgp_peer_timers().has_value()); + // Likewise a rejected route-limit value must not leave a seeded RouteLimit. + EXPECT_THAT( + run({"10.0.0.1", "max-route", "pre-warning-threshold", "-1"}), + HasSubstr("Error")); + EXPECT_FALSE(peers()[0].pre_filter().has_value()); EXPECT_EQ(peers().size(), 1); } diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpNeighborTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpNeighborTest.cpp index be697eb6fb137..684de21966273 100644 --- a/fboss/cli/fboss2/test/integration_test/ConfigBgpNeighborTest.cpp +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpNeighborTest.cpp @@ -128,6 +128,45 @@ TEST_F(ConfigBgpNeighborTest, GracefulRestartKeepsInheritedHoldTime) { EXPECT_EQ((*peer)["bgp_peer_timers"]["graceful_restart_seconds"].asInt(), 60); } +TEST_F(ConfigBgpNeighborTest, SessionKnobsAndCommit) { + // The per-session knobs bgpd resolves peer > group (remove-private-as, + // ttl-security-hops) reach the daemon's running config via the same path. + discardSession(); + clearBgpSession(); + stageNeighbor({kNeighbor, "remove-private-as", "true"}); + stageNeighbor({kNeighbor, "ttl-security-hops", "2"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + auto running = readRunningBgpConfigViaRpc(); + const auto* peer = findPeer(running, kNeighbor); + ASSERT_NE(peer, nullptr) << "bgpd's running config has no peer " << kNeighbor; + ASSERT_TRUE(peer->count("remove_private_as")); + EXPECT_TRUE((*peer)["remove_private_as"].asBool()); + ASSERT_TRUE(peer->count("ttl_security_hops")); + EXPECT_EQ((*peer)["ttl_security_hops"].asInt(), 2); +} + +TEST_F(ConfigBgpNeighborTest, RouteLimitWarningKeepsUnlimitedCap) { + // A warning threshold on a peer with no route limit above it must not + // introduce the 12000-route thrift default: bgpd reads max_routes 0 as + // unlimited, which is what the peer had before the edit. + discardSession(); + clearBgpSession(); + stageNeighbor({kNeighbor, "max-route", "pre-warning-threshold", "30000"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + auto running = readRunningBgpConfigViaRpc(); + const auto* peer = findPeer(running, kNeighbor); + ASSERT_NE(peer, nullptr) << "bgpd's running config has no peer " << kNeighbor; + ASSERT_TRUE(peer->count("pre_filter")); + EXPECT_EQ((*peer)["pre_filter"]["max_routes"].asInt(), 0); + EXPECT_EQ((*peer)["pre_filter"]["warning_limit"].asInt(), 30000); +} + TEST_F(ConfigBgpNeighborTest, DeleteNeighborAndCommit) { // Land a neighbor in the system config, then delete it through a second // commit and verify it is gone from the promoted config. diff --git a/fboss/cli/fboss2/tools/bgp_json_to_cli.py b/fboss/cli/fboss2/tools/bgp_json_to_cli.py index c9e7cd7e27708..ac61ef33f2c3a 100644 --- a/fboss/cli/fboss2/tools/bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/bgp_json_to_cli.py @@ -244,74 +244,120 @@ def format_bandwidth(bps: int) -> str: return str(bps) -# (json field, CLI attribute) — fields whose value maps 1:1 onto a neighbor -# attribute token. -_NEIGHBOR_SCALAR_FIELDS = [ - ("remote_as_4_byte", "remote-asn"), - ("local_as_4_byte", "local-asn"), - ("peer_group_name", "peer-group"), - ("description", "description"), - ("peer_tag", "peer-tag"), - ("local_addr", "bind-addr address"), - ("ingress_policy_name", "ingress-policy"), - ("egress_policy_name", "egress-policy"), - ("next_hop4", "next-hop4"), - ("next_hop6", "next-hop6"), - ("peer_id", "peer-id"), - ("type", "type"), +def _printable(text: Any) -> str: + """A JSON-sourced name made safe for a `#` comment line: no control + characters, so it cannot break out of the comment.""" + return "".join(ch if ch.isprintable() else "?" for ch in str(text)) + + +# --------------------------------------------------------------------------- +# Fields BgpPeer and PeerGroup share. The neighbor and peer-group dispatchers +# spell these attributes identically, so one table drives both generators. +# --------------------------------------------------------------------------- + +# (json field, CLI attribute) — boolean flags. +_BOOL_FIELDS = [ + ("is_rr_client", "rr-client"), + ("is_confed_peer", "confed-peer"), + ("is_redistribute_peer", "redistribute-peer"), + ("enhanced_route_refresh", "enhanced-route-refresh"), + ("route_refresh", "route-refresh"), + ("remove_private_as", "remove-private-as"), + ("enforce_first_as", "enforce-first-as"), + ("disable_ipv4_afi", "afi disable-ipv4-afi"), + ("disable_ipv6_afi", "afi disable-ipv6-afi"), + ("v4_over_v6_nexthop", "afi ipv4-over-ipv6-nh"), + ("enable_stateful_ha", "graceful-restart stateful-ha"), + ("next_hop_self", "next-hop-self"), ] +# (json field, CLI attribute) — integer-valued attributes. +_INT_FIELDS = [("ttl_security_hops", "ttl-security-hops")] + +_TIMER_FIELDS = [ + ("hold_time_seconds", "timers hold-time"), + ("keep_alive_seconds", "timers keepalive"), + ("out_delay_seconds", "timers out-delay"), + ("withdraw_unprog_delay_seconds", "timers withdraw-unprog-delay"), + ("graceful_restart_seconds", "graceful-restart restart-time"), +] + +# RouteLimit leaves, as (json field, attribute suffix, is_bool); the prefix is +# `max-route pre-` or `max-route post-`. +_ROUTE_LIMIT_FIELDS = [ + ("max_routes", "filter", False), + ("warning_limit", "warning-threshold", False), + ("warning_only", "warning-only", True), +] + +# Every shared key the generators below convert. Anything else present on the +# JSON object is reported (see _unconverted_field_warnings) instead of being +# dropped silently. +_SHARED_HANDLED_FIELDS = ( + { + "is_passive", + "link_bandwidth_bps", + "advertise_link_bandwidth", + "receive_link_bandwidth", + "add_path", + "bgp_peer_timers", + "pre_filter", + "post_filter", + } + | {field for field, _ in _BOOL_FIELDS} + | {field for field, _ in _INT_FIELDS} +) +_TIMER_HANDLED_FIELDS = {field for field, _ in _TIMER_FIELDS} +_ROUTE_LIMIT_HANDLED_FIELDS = {field for field, _, _ in _ROUTE_LIMIT_FIELDS} + + +def _generate_scalar_commands( + obj: dict[str, Any], prefix: str, fields: list[tuple[str, str]] +) -> list[str]: + """Generate ` ` for each present scalar field.""" + commands = [] + for field, cli_name in fields: + if field in obj: + commands.append(f"{prefix} {cli_name} {escape_shell_arg(obj[field])}") + return commands + + +def _generate_bool_commands(obj: dict[str, Any], prefix: str) -> list[str]: + """Generate boolean flag commands for a neighbor or peer group.""" + commands = [] + for field, cli_name in _BOOL_FIELDS: + if field in obj: + commands.append(f"{prefix} {cli_name} {_shell_bool(obj[field])}") + return commands + + +def _generate_link_bandwidth_commands(obj: dict[str, Any], prefix: str) -> list[str]: + """Generate the link-bandwidth trio for a neighbor or peer group. -def _generate_neighbor_basic_commands(peer: dict[str, Any], prefix: str) -> list[str]: - """Generate scalar neighbor commands (ASNs, names, policies, addresses).""" + advertise/receive enum values are passed through as found (name or + integer): the dispatchers accept both spellings for a thrift enum. + """ commands = [] - for field, cli_name in _NEIGHBOR_SCALAR_FIELDS: - if field in peer: - commands.append(f"{prefix} {cli_name} {escape_shell_arg(peer[field])}") - if "is_passive" in peer: - commands.append(f"{prefix} passive {_shell_bool(peer['is_passive'])}") - if "link_bandwidth_bps" in peer: - bw = peer["link_bandwidth_bps"] + if "link_bandwidth_bps" in obj: + bw = obj["link_bandwidth_bps"] bw_str = format_bandwidth(bw) if isinstance(bw, int) else str(bw) commands.append(f"{prefix} link-bandwidth {escape_shell_arg(bw_str)}") - if "advertise_link_bandwidth" in peer: + if "advertise_link_bandwidth" in obj: commands.append( - f"{prefix} advertise-lbw {escape_shell_arg(peer['advertise_link_bandwidth'])}" + f"{prefix} advertise-lbw {escape_shell_arg(obj['advertise_link_bandwidth'])}" ) - if "receive_link_bandwidth" in peer: + if "receive_link_bandwidth" in obj: commands.append( - f"{prefix} receive-lbw {escape_shell_arg(peer['receive_link_bandwidth'])}" + f"{prefix} receive-lbw {escape_shell_arg(obj['receive_link_bandwidth'])}" ) return commands -def _generate_neighbor_bool_commands(peer: dict[str, Any], prefix: str) -> list[str]: - """Generate boolean flag commands for a neighbor.""" - commands = [] - bool_fields = [ - ("is_rr_client", "rr-client"), - ("is_confed_peer", "confed-peer"), - ("is_redistribute_peer", "redistribute-peer"), - ("enhanced_route_refresh", "enhanced-route-refresh"), - ("disable_ipv4_afi", "afi disable-ipv4-afi"), - ("disable_ipv6_afi", "afi disable-ipv6-afi"), - ("v4_over_v6_nexthop", "afi ipv4-over-ipv6-nh"), - ("enable_stateful_ha", "graceful-restart stateful-ha"), - ("next_hop_self", "next-hop-self"), - ] - for field, cli_name in bool_fields: - if field in peer: - commands.append(f"{prefix} {cli_name} {_shell_bool(peer[field])}") - return commands - - -def _generate_neighbor_add_path_commands( - peer: dict[str, Any], prefix: str -) -> list[str]: +def _generate_add_path_commands(obj: dict[str, Any], prefix: str) -> list[str]: """Generate add-path commands from the AddPath enum (RECEIVE=1, SEND=2, BOTH=3).""" - if "add_path" not in peer: + if "add_path" not in obj: return [] - raw = peer["add_path"] + raw = obj["add_path"] value = ( {"RECEIVE": 1, "SEND": 2, "BOTH": 3}.get(raw, raw) if isinstance(raw, str) @@ -326,66 +372,99 @@ def _generate_neighbor_add_path_commands( return commands -def _generate_neighbor_timer_commands(timers: dict[str, Any], prefix: str) -> list[str]: - """Generate timer and graceful-restart commands for a neighbor.""" +def _generate_timer_commands(timers: dict[str, Any], prefix: str) -> list[str]: + """Generate timer and graceful-restart commands for a neighbor or peer group.""" + return _generate_scalar_commands(timers, prefix, _TIMER_FIELDS) + + +def _generate_route_limit_commands(obj: dict[str, Any], prefix: str) -> list[str]: + """Generate max-route commands from pre_filter/post_filter.""" commands = [] - if "hold_time_seconds" in timers: - commands.append( - f"{prefix} timers hold-time {escape_shell_arg(timers['hold_time_seconds'])}" - ) - if "keep_alive_seconds" in timers: - commands.append( - f"{prefix} timers keepalive {escape_shell_arg(timers['keep_alive_seconds'])}" - ) - if "out_delay_seconds" in timers: - commands.append( - f"{prefix} timers out-delay {escape_shell_arg(timers['out_delay_seconds'])}" - ) - if "withdraw_unprog_delay_seconds" in timers: - commands.append( - f"{prefix} timers withdraw-unprog-delay {escape_shell_arg(timers['withdraw_unprog_delay_seconds'])}" - ) - if "graceful_restart_seconds" in timers: - commands.append( - f"{prefix} graceful-restart restart-time {escape_shell_arg(timers['graceful_restart_seconds'])}" - ) + for struct, side in (("pre_filter", "pre"), ("post_filter", "post")): + limit = obj.get(struct, {}) + for field, suffix, is_bool in _ROUTE_LIMIT_FIELDS: + if field in limit: + value = ( + _shell_bool(limit[field]) + if is_bool + else escape_shell_arg(limit[field]) + ) + commands.append(f"{prefix} max-route {side}-{suffix} {value}") return commands -def _generate_neighbor_route_limit_commands( - peer: dict[str, Any], prefix: str -) -> list[str]: - """Generate max-route commands from pre_filter/post_filter.""" +def _generate_session_commands(obj: dict[str, Any], prefix: str) -> list[str]: + """Generate the commands for every field BgpPeer and PeerGroup share.""" commands = [] - pre_filter = peer.get("pre_filter", {}) - if "max_routes" in pre_filter: - commands.append( - f"{prefix} max-route pre-filter {escape_shell_arg(pre_filter['max_routes'])}" - ) - if "warning_limit" in pre_filter: - commands.append( - f"{prefix} max-route pre-warning-threshold {escape_shell_arg(pre_filter['warning_limit'])}" - ) - if "warning_only" in pre_filter: - commands.append( - f"{prefix} max-route pre-warning-only {_shell_bool(pre_filter['warning_only'])}" - ) - post_filter = peer.get("post_filter", {}) - if "max_routes" in post_filter: - commands.append( - f"{prefix} max-route post-filter {escape_shell_arg(post_filter['max_routes'])}" - ) - if "warning_limit" in post_filter: - commands.append( - f"{prefix} max-route post-warning-threshold {escape_shell_arg(post_filter['warning_limit'])}" - ) - if "warning_only" in post_filter: - commands.append( - f"{prefix} max-route post-warning-only {_shell_bool(post_filter['warning_only'])}" - ) + if "is_passive" in obj: + commands.append(f"{prefix} passive {_shell_bool(obj['is_passive'])}") + commands.extend(_generate_bool_commands(obj, prefix)) + commands.extend(_generate_scalar_commands(obj, prefix, _INT_FIELDS)) + commands.extend(_generate_link_bandwidth_commands(obj, prefix)) + commands.extend(_generate_add_path_commands(obj, prefix)) + commands.extend(_generate_timer_commands(obj.get("bgp_peer_timers", {}), prefix)) + commands.extend(_generate_route_limit_commands(obj, prefix)) return commands +def _unconverted_field_warnings( + obj: dict[str, Any], handled: set[str], subject: str +) -> list[str]: + """One `# WARNING` comment line per JSON field no generator converts. + + The dispatchers only expose the fields bgpd reads at that level, so a + field left here is either dead in the daemon or per-peer-only; either + way the operator must see it was not carried over. + """ + warnings = [] + + def warn(field: str) -> None: + warnings.append( + f"# WARNING: {subject}: field {_printable(field)} has no CLI " + "equivalent, not converted" + ) + + for field in obj: + if field not in handled: + warn(field) + for field in obj.get("bgp_peer_timers", {}): + if field not in _TIMER_HANDLED_FIELDS: + warn(f"bgp_peer_timers.{field}") + for struct in ("pre_filter", "post_filter"): + for field in obj.get(struct, {}): + if field not in _ROUTE_LIMIT_HANDLED_FIELDS: + warn(f"{struct}.{field}") + return warnings + + +# (json field, CLI attribute) — BgpPeer-only fields whose value maps 1:1 onto +# a neighbor attribute token. The deprecated i32 ASNs are converted to the +# 4-byte attribute: bgpd reads either representation and the CLI stores the +# 4-byte field. +_NEIGHBOR_SCALAR_FIELDS = [ + ("remote_as_4_byte", "remote-asn"), + ("remote_as", "remote-asn"), + ("local_as_4_byte", "local-asn"), + ("local_as", "local-asn"), + ("peer_group_name", "peer-group"), + ("description", "description"), + ("peer_tag", "peer-tag"), + ("local_addr", "bind-addr address"), + ("ingress_policy_name", "ingress-policy"), + ("egress_policy_name", "egress-policy"), + ("next_hop4", "next-hop4"), + ("next_hop6", "next-hop6"), + ("peer_id", "peer-id"), + ("type", "type"), +] + +_NEIGHBOR_HANDLED_FIELDS = ( + {"peer_addr"} + | {field for field, _ in _NEIGHBOR_SCALAR_FIELDS} + | _SHARED_HANDLED_FIELDS +) + + def generate_peer_commands(peer: dict[str, Any]) -> list[str]: """Generate `config protocol bgp neighbor` CLI commands for a peer.""" peer_addr = peer.get("peer_addr", "") @@ -393,14 +472,11 @@ def generate_peer_commands(peer: dict[str, Any]) -> list[str]: return [] prefix = f"config protocol bgp neighbor {escape_shell_arg(peer_addr)}" - commands = [] - commands.extend(_generate_neighbor_basic_commands(peer, prefix)) - commands.extend(_generate_neighbor_bool_commands(peer, prefix)) - commands.extend(_generate_neighbor_add_path_commands(peer, prefix)) - commands.extend( - _generate_neighbor_timer_commands(peer.get("bgp_peer_timers", {}), prefix) + commands = _unconverted_field_warnings( + peer, _NEIGHBOR_HANDLED_FIELDS, f"neighbor {_printable(peer_addr)}" ) - commands.extend(_generate_neighbor_route_limit_commands(peer, prefix)) + commands.extend(_generate_scalar_commands(peer, prefix, _NEIGHBOR_SCALAR_FIELDS)) + commands.extend(_generate_session_commands(peer, prefix)) return commands @@ -415,7 +491,7 @@ def generate_exec_commands(commands: list[str], binary: str = "fboss2") -> list[ "", ] for cmd in commands: - exec_commands.append(f"{binary} {cmd}") + exec_commands.append(cmd if cmd.startswith("#") else f"{binary} {cmd}") exec_commands.extend( [ @@ -428,22 +504,21 @@ def generate_exec_commands(commands: list[str], binary: str = "fboss2") -> list[ return exec_commands -def json_to_cli(config: dict[str, Any], binary: str = "fboss2") -> list[str]: - """Convert BGP++ JSON config to CLI commands.""" +def generate_commands(config: dict[str, Any]) -> list[str]: + """Convert BGP++ JSON config to raw CLI commands (plus `#` warning lines + for fields that have no CLI equivalent).""" commands = [] - - # Generate global commands commands.extend(generate_global_commands(config)) - - # Generate peer-group commands for peer_group in config.get("peer_groups", []): commands.extend(generate_peer_group_commands(peer_group)) - - # Generate peer commands for peer in config.get("peers", []): commands.extend(generate_peer_commands(peer)) + return commands - return generate_exec_commands(commands, binary) + +def json_to_cli(config: dict[str, Any], binary: str = "fboss2") -> list[str]: + """Convert BGP++ JSON config to an executable CLI script.""" + return generate_exec_commands(generate_commands(config), binary) def main() -> int: @@ -477,18 +552,15 @@ def main() -> int: print(f"Error: Invalid JSON in '{args.input}': {e}", file=sys.stderr) return 1 - if args.raw: - # Generate raw CLI commands only (no shell wrapper) - commands = [] - commands.extend(generate_global_commands(config)) - for peer_group in config.get("peer_groups", []): - commands.extend(generate_peer_group_commands(peer_group)) - for peer in config.get("peers", []): - commands.extend(generate_peer_commands(peer)) - output = "\n".join(commands) - else: - commands = json_to_cli(config, binary=args.binary) - output = "\n".join(commands) + commands = generate_commands(config) + # Fields with no CLI equivalent are flagged in the script AND on stderr, + # so a scripted conversion cannot lose them silently. + for line in commands: + if line.startswith("# WARNING"): + print(line[2:], file=sys.stderr) + if not args.raw: + commands = generate_exec_commands(commands, args.binary) + output = "\n".join(commands) if args.output: with open(args.output, "w") as f: diff --git a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py index d029545228924..ad85ded6c3a6f 100644 --- a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py @@ -20,6 +20,7 @@ from fboss.cli.fboss2.tools.bgp_json_to_cli import ( escape_shell_arg, format_bandwidth, + generate_commands, generate_exec_commands, generate_global_commands, generate_peer_commands, @@ -558,6 +559,190 @@ def test_add_path_symbolic(self) -> None: "config protocol bgp neighbor 2001:db8::1 add-path send true", commands ) + def test_session_flags(self) -> None: + """The session knobs bgpd resolves peer > group all have a spelling.""" + peer = { + "peer_addr": "2001:db8::1", + "route_refresh": True, + "remove_private_as": True, + "enforce_first_as": False, + "ttl_security_hops": 2, + } + commands = generate_peer_commands(peer) + prefix = "config protocol bgp neighbor 2001:db8::1" + self.assertIn(f"{prefix} route-refresh true", commands) + self.assertIn(f"{prefix} remove-private-as true", commands) + self.assertIn(f"{prefix} enforce-first-as false", commands) + self.assertIn(f"{prefix} ttl-security-hops 2", commands) + + def test_deprecated_asn_fields_map_to_4_byte_attribute(self) -> None: + """The deprecated i32 ASNs are carried over, not dropped.""" + peer = {"peer_addr": "2001:db8::1", "remote_as": 65000, "local_as": 64512} + commands = generate_peer_commands(peer) + prefix = "config protocol bgp neighbor 2001:db8::1" + self.assertIn(f"{prefix} remote-asn 65000", commands) + self.assertIn(f"{prefix} local-asn 64512", commands) + self.assertFalse(any(c.startswith("# WARNING") for c in commands)) + + def test_complete_neighbor_loses_no_field(self) -> None: + """Every BgpPeer field bgpd reads produces exactly one command, in the + grammar the neighbor dispatcher accepts, and nothing is flagged.""" + peer = { + "peer_addr": "2001:db8::1", + "remote_as_4_byte": 65000, + "local_as_4_byte": 64512, + "peer_group_name": "SPINE", + "description": "spine peer", + "peer_tag": "FSW", + "local_addr": "2001:db8::2", + "ingress_policy_name": "IN_POLICY", + "egress_policy_name": "OUT_POLICY", + "next_hop4": "10.1.1.1", + "next_hop6": "2001:db8::a", + "peer_id": "test-peer:v6:1", + "type": "BGP_MONITOR", + "is_passive": True, + "is_rr_client": True, + "is_confed_peer": False, + "is_redistribute_peer": True, + "enhanced_route_refresh": True, + "route_refresh": False, + "remove_private_as": True, + "enforce_first_as": True, + "disable_ipv4_afi": True, + "disable_ipv6_afi": False, + "v4_over_v6_nexthop": True, + "enable_stateful_ha": True, + "next_hop_self": True, + "ttl_security_hops": 2, + "link_bandwidth_bps": "10G", + "advertise_link_bandwidth": 1, + "receive_link_bandwidth": "ACCEPT", + "add_path": 3, + "bgp_peer_timers": { + "hold_time_seconds": 90, + "keep_alive_seconds": 30, + "out_delay_seconds": 5, + "withdraw_unprog_delay_seconds": 10, + "graceful_restart_seconds": 120, + }, + "pre_filter": { + "max_routes": 45000, + "warning_limit": 40000, + "warning_only": True, + }, + "post_filter": { + "max_routes": 50000, + "warning_limit": 48000, + "warning_only": False, + }, + } + expected = [ + "remote-asn 65000", + "local-asn 64512", + "peer-group SPINE", + "description 'spine peer'", + "peer-tag FSW", + "bind-addr address 2001:db8::2", + "ingress-policy IN_POLICY", + "egress-policy OUT_POLICY", + "next-hop4 10.1.1.1", + "next-hop6 2001:db8::a", + "peer-id test-peer:v6:1", + "type BGP_MONITOR", + "passive true", + "rr-client true", + "confed-peer false", + "redistribute-peer true", + "enhanced-route-refresh true", + "route-refresh false", + "remove-private-as true", + "enforce-first-as true", + "afi disable-ipv4-afi true", + "afi disable-ipv6-afi false", + "afi ipv4-over-ipv6-nh true", + "graceful-restart stateful-ha true", + "next-hop-self true", + "ttl-security-hops 2", + "link-bandwidth 10G", + "advertise-lbw 1", + "receive-lbw ACCEPT", + "add-path receive true", + "add-path send true", + "timers hold-time 90", + "timers keepalive 30", + "timers out-delay 5", + "timers withdraw-unprog-delay 10", + "graceful-restart restart-time 120", + "max-route pre-filter 45000", + "max-route pre-warning-threshold 40000", + "max-route pre-warning-only true", + "max-route post-filter 50000", + "max-route post-warning-threshold 48000", + "max-route post-warning-only false", + ] + prefix = "config protocol bgp neighbor 2001:db8::1" + commands = generate_peer_commands(peer) + self.assertEqual(commands, [f"{prefix} {e}" for e in expected]) + # One command per leaf field: nothing in the input was dropped. + leaf_fields = ( + len(peer) + - 1 # peer_addr is the key, not an attribute + - 3 # the three nested structs are counted by their leaves below + + 1 # add_path BOTH expands to two commands + + len(peer["bgp_peer_timers"]) + + len(peer["pre_filter"]) + + len(peer["post_filter"]) + ) + self.assertEqual(len(commands), leaf_fields) + + def test_unrepresentable_neighbor_fields_warn(self) -> None: + """Fields nothing in bgpd reads are reported, not silently dropped.""" + peer = { + "peer_addr": "2001:db8::1", + "remote_as_4_byte": 65000, + "enabled": True, + "router_port_id": 7, + "bgp_peer_timers": { + "hold_time_seconds": 90, + "graceful_restart_end_of_rib_seconds": 30, + }, + } + commands = generate_peer_commands(peer) + prefix = "config protocol bgp neighbor 2001:db8::1" + warnings = [c for c in commands if c.startswith("# WARNING")] + self.assertEqual( + warnings, + [ + "# WARNING: neighbor 2001:db8::1: field enabled has no CLI " + "equivalent, not converted", + "# WARNING: neighbor 2001:db8::1: field router_port_id has no CLI " + "equivalent, not converted", + "# WARNING: neighbor 2001:db8::1: field " + "bgp_peer_timers.graceful_restart_end_of_rib_seconds has no CLI " + "equivalent, not converted", + ], + ) + # ... and only the convertible fields produce commands. + self.assertEqual( + [c for c in commands if not c.startswith("#")], + [f"{prefix} remote-asn 65000", f"{prefix} timers hold-time 90"], + ) + + def test_warning_subject_cannot_break_out_of_comment(self) -> None: + """A hostile peer_addr must not turn a warning into a script line.""" + peer = {"peer_addr": "x\nrm -rf /", "enabled": True} + script = json_to_cli(peer_config(peer), binary="fboss2") + self.assertFalse(any(line.startswith("rm ") for line in script)) + self.assertTrue( + any(line.startswith("# WARNING: neighbor x?rm") for line in script) + ) + + +def peer_config(peer: dict) -> dict: + """A whole-config wrapper around one peer.""" + return {"peers": [peer]} + class GenerateScriptCommandsTest(unittest.TestCase): """Tests for generate_exec_commands.""" @@ -578,6 +763,22 @@ def test_exec_commands_prepends_binary(self) -> None: "/path/to/fboss2 config protocol bgp global router-id 10.0.0.1", result ) + def test_exec_commands_keep_warning_comments(self) -> None: + """A `# WARNING` line is a comment in the script, not a command.""" + commands = [ + "# WARNING: neighbor 2001:db8::1: field enabled has no CLI equivalent" + ] + result = generate_exec_commands(commands, "/path/to/fboss2") + self.assertIn(commands[0], result) + self.assertFalse(any(line.startswith("/path/to/fboss2 #") for line in result)) + + def test_generate_commands_carries_warnings_in_raw_output(self) -> None: + """--raw output keeps the warning lines next to the object's commands.""" + commands = generate_commands( + peer_config({"peer_addr": "2001:db8::1", "enabled": True}) + ) + self.assertTrue(commands[0].startswith("# WARNING: neighbor 2001:db8::1")) + class JsonToCliIntegrationTest(unittest.TestCase): """Integration tests for json_to_cli function.""" From 092a4860746f41282bbad276e809041124dd9e74 Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Fri, 7 Aug 2026 18:27:03 +0000 Subject: [PATCH 09/22] [Nexthop] fboss2 config/delete protocol bgp peer-group commands Collapses the 18 per-attribute `config protocol bgp peer-group` command classes (built on the deprecated `folly::dynamic` BgpConfigSession) into a single typed dispatcher, mirroring CmdConfigProtocolBgpNeighbor. Also adds `delete protocol bgp peer-group `. The group name is the first positional token; the next one or two tokens name the attribute, matched longest-prefix-first so `timers hold-time` wins over any `timers` prefix; the rest are its value(s). Handlers mutate the typed bgp::thrift::PeerGroup through ConfigSession::getBgpConfig() / saveBgpConfig(), so adding a tunable is a one-entry change in the dispatch table rather than a new command class. 36 command files are deleted. - Covers the 29 dispatch keys that map to a PeerGroup thrift field: remote-asn / local-asn (4-byte-bounded), description, peer-tag, ingress-policy / egress-policy, rr-client, confed-peer, redistribute-peer, enhanced-route-refresh, connect-mode, add-path send|receive, afi disable-ipv4-afi|disable-ipv6-afi|ipv4-over-ipv6-nh, graceful-restart restart-time|stateful-ha, max-route pre-filter|post-filter (plus the warning-threshold / warning-only knobs), timers hold-time|keepalive|out-delay|withdraw-unprog-delay, and next-hop-self. - Rejected rather than persisted as dead config (precedent: cluster-id in the global command, connect-mode BOTH in the neighbor command): connect-mode BOTH, since thrift only models is_passive. add-path send|receive merges into the AddPath enum bitmask, and clearing the last direction unsets the field. - Attributes with no per-peer-group thrift field (afi ipv4-labeled-unicast, afi ipv6-labeled-unicast, peer-port) are absent from the dispatch table, so they are refused at parse time as unknown attributes; a rejected value never lands on disk. - positionals_at_end() stops parent-chain subcommand fallthrough from reclassifying an attribute token that matches a sibling command name once the group name has been consumed (same fix as the neighbor command). - unit tests (16): CmdConfigBgpPeerGroupTest (12) covering arg validation, longest-prefix match, the add-path merge matrix, connect-mode, the bool/string/route-limit attributes, value validation and unknown-attribute rejection, plus CmdDeleteBgpPeerGroupTest (4). - integration tests (3): ConfigBgpPeerGroupTest asserts the committed peer_groups through bgpd's getRunningConfig RPC, proving the daemon parsed and adopted the promoted config rather than only checking the file the CLI wrote. --- cmake/CliFboss2.cmake | 38 +- cmake/CliFboss2TestConfig.cmake | 2 + cmake/CliFboss2TestIntegrationTest.cmake | 1 + fboss/cli/fboss2/BUCK | 34 +- fboss/cli/fboss2/CmdListConfig.cpp | 198 +------ .../CmdConfigProtocolBgpPeerGroup.cpp | 482 +++++++++++++++++- .../CmdConfigProtocolBgpPeerGroup.h | 44 +- ...mdConfigProtocolBgpPeerGroupConfedPeer.cpp | 61 --- .../CmdConfigProtocolBgpPeerGroupConfedPeer.h | 46 -- ...dConfigProtocolBgpPeerGroupDescription.cpp | 57 --- ...CmdConfigProtocolBgpPeerGroupDescription.h | 46 -- ...nfigProtocolBgpPeerGroupDisableIpv4Afi.cpp | 62 --- ...ConfigProtocolBgpPeerGroupDisableIpv4Afi.h | 46 -- ...ConfigProtocolBgpPeerGroupEgressPolicy.cpp | 61 --- ...mdConfigProtocolBgpPeerGroupEgressPolicy.h | 46 -- ...onfigProtocolBgpPeerGroupIngressPolicy.cpp | 61 --- ...dConfigProtocolBgpPeerGroupIngressPolicy.h | 46 -- ...CmdConfigProtocolBgpPeerGroupMaxRoutes.cpp | 71 --- .../CmdConfigProtocolBgpPeerGroupMaxRoutes.h | 46 -- ...dConfigProtocolBgpPeerGroupNextHopSelf.cpp | 63 --- ...CmdConfigProtocolBgpPeerGroupNextHopSelf.h | 46 -- .../CmdConfigProtocolBgpPeerGroupPeerTag.cpp | 58 --- .../CmdConfigProtocolBgpPeerGroupPeerTag.h | 45 -- ...CmdConfigProtocolBgpPeerGroupRemoteAsn.cpp | 64 --- .../CmdConfigProtocolBgpPeerGroupRemoteAsn.h | 46 -- .../CmdConfigProtocolBgpPeerGroupRrClient.cpp | 62 --- .../CmdConfigProtocolBgpPeerGroupRrClient.h | 45 -- .../CmdConfigProtocolBgpPeerGroupTimers.cpp | 44 -- .../CmdConfigProtocolBgpPeerGroupTimers.h | 41 -- ...nfigProtocolBgpPeerGroupTimersHoldTime.cpp | 65 --- ...ConfigProtocolBgpPeerGroupTimersHoldTime.h | 46 -- ...figProtocolBgpPeerGroupTimersKeepalive.cpp | 71 --- ...onfigProtocolBgpPeerGroupTimersKeepalive.h | 46 -- ...nfigProtocolBgpPeerGroupTimersOutDelay.cpp | 71 --- ...ConfigProtocolBgpPeerGroupTimersOutDelay.h | 46 -- ...lBgpPeerGroupTimersWithdrawUnprogDelay.cpp | 70 --- ...colBgpPeerGroupTimersWithdrawUnprogDelay.h | 48 -- ...mdConfigProtocolBgpPeerGroupV4OverV6Nh.cpp | 61 --- .../CmdConfigProtocolBgpPeerGroupV4OverV6Nh.h | 46 -- ...ConfigProtocolBgpPeerGroupWarningLimit.cpp | 70 --- ...mdConfigProtocolBgpPeerGroupWarningLimit.h | 46 -- ...dConfigProtocolBgpPeerGroupWarningOnly.cpp | 72 --- ...CmdConfigProtocolBgpPeerGroupWarningOnly.h | 46 -- .../CmdDeleteProtocolBgpPeerGroup.cpp | 81 +++ .../CmdDeleteProtocolBgpPeerGroup.h | 61 +++ fboss/cli/fboss2/test/config/BUCK | 2 + .../test/config/CmdConfigBgpPeerGroupTest.cpp | 247 +++++++++ .../test/config/CmdDeleteBgpPeerGroupTest.cpp | 106 ++++ fboss/cli/fboss2/test/integration_test/BUCK | 1 + .../ConfigBgpPeerGroupTest.cpp | 131 +++++ 50 files changed, 1168 insertions(+), 2227 deletions(-) delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDisableIpv4Afi.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDisableIpv4Afi.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupEgressPolicy.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupEgressPolicy.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupIngressPolicy.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupIngressPolicy.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupMaxRoutes.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupMaxRoutes.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupNextHopSelf.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupNextHopSelf.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupPeerTag.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupPeerTag.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRemoteAsn.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRemoteAsn.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRrClient.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRrClient.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersHoldTime.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersHoldTime.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.h delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.cpp delete mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.h create mode 100644 fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.cpp create mode 100644 fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.h create mode 100644 fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp create mode 100644 fboss/cli/fboss2/test/config/CmdDeleteBgpPeerGroupTest.cpp create mode 100644 fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp diff --git a/cmake/CliFboss2.cmake b/cmake/CliFboss2.cmake index d5bcb348a0762..4df1a9a13e888 100644 --- a/cmake/CliFboss2.cmake +++ b/cmake/CliFboss2.cmake @@ -951,42 +951,6 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDisableIpv4Afi.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDisableIpv4Afi.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupEgressPolicy.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupEgressPolicy.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupIngressPolicy.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupIngressPolicy.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupMaxRoutes.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupMaxRoutes.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupNextHopSelf.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupNextHopSelf.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupPeerTag.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupPeerTag.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRemoteAsn.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRemoteAsn.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRrClient.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRrClient.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersHoldTime.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersHoldTime.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.h - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.cpp - fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.h fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.cpp fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.h fboss/cli/fboss2/commands/config/ptp/transparent_clock/CmdConfigPtpTransparentClock.cpp @@ -1103,6 +1067,8 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.cpp fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h + fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.cpp + fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.h fboss/cli/fboss2/commands/delete/protocol/CmdDeleteProtocol.h fboss/cli/fboss2/commands/delete/protocol/static/CmdDeleteProtocolStatic.cpp fboss/cli/fboss2/commands/delete/protocol/static/CmdDeleteProtocolStatic.h diff --git a/cmake/CliFboss2TestConfig.cmake b/cmake/CliFboss2TestConfig.cmake index fa42a48694e00..27a52a374adfb 100644 --- a/cmake/CliFboss2TestConfig.cmake +++ b/cmake/CliFboss2TestConfig.cmake @@ -12,6 +12,7 @@ add_executable(fboss2_cmd_config_test fboss/cli/fboss2/test/config/CmdConfigBgpGlobalTest.cpp fboss/cli/fboss2/test/config/CmdConfigIcmpV4UnavailableSrcAddrTest.cpp fboss/cli/fboss2/test/config/CmdConfigBgpNeighborTest.cpp + fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp fboss/cli/fboss2/test/config/CmdConfigCoppTest.cpp fboss/cli/fboss2/test/config/CmdConfigDhcpTest.cpp fboss/cli/fboss2/test/config/CmdConfigHostnameTest.cpp @@ -45,6 +46,7 @@ add_executable(fboss2_cmd_config_test fboss/cli/fboss2/test/config/CmdDeleteAclRuleTest.cpp fboss/cli/fboss2/test/config/CmdDeleteArpTest.cpp fboss/cli/fboss2/test/config/CmdDeleteBgpNeighborTest.cpp + fboss/cli/fboss2/test/config/CmdDeleteBgpPeerGroupTest.cpp fboss/cli/fboss2/test/config/CmdDeleteConfigInterfaceTest.cpp fboss/cli/fboss2/test/config/CmdDeleteCoppQueueTest.cpp fboss/cli/fboss2/test/config/CmdDeleteCoppReasonTest.cpp diff --git a/cmake/CliFboss2TestIntegrationTest.cmake b/cmake/CliFboss2TestIntegrationTest.cmake index 84e2861766058..d8ade0e72e6ae 100644 --- a/cmake/CliFboss2TestIntegrationTest.cmake +++ b/cmake/CliFboss2TestIntegrationTest.cmake @@ -15,6 +15,7 @@ add_executable(fboss2_integration_test fboss/cli/fboss2/test/integration_test/ConfigSflowSampleDestTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpNeighborTest.cpp + fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp fboss/cli/fboss2/test/integration_test/ConfigConcurrentSessionsTest.cpp fboss/cli/fboss2/test/integration_test/ConfigHostnameTest.cpp diff --git a/fboss/cli/fboss2/BUCK b/fboss/cli/fboss2/BUCK index 4db1c8e7d5a41..ee63b3a3d2e86 100644 --- a/fboss/cli/fboss2/BUCK +++ b/fboss/cli/fboss2/BUCK @@ -1166,24 +1166,6 @@ cpp_library( "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp", "commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.cpp", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDisableIpv4Afi.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupEgressPolicy.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupIngressPolicy.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupMaxRoutes.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupNextHopSelf.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupPeerTag.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRemoteAsn.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRrClient.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersHoldTime.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.cpp", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.cpp", "commands/config/protocol/static/CmdConfigProtocolStatic.cpp", "commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.cpp", "commands/config/ptp/CmdConfigPtp.cpp", @@ -1241,6 +1223,7 @@ cpp_library( "commands/delete/protocol/CmdDeleteProtocol.cpp", "commands/delete/protocol/bgp/CmdDeleteProtocolBgp.cpp", "commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.cpp", + "commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.cpp", "commands/delete/protocol/static/CmdDeleteProtocolStatic.cpp", "commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.cpp", "commands/delete/qos/CmdDeleteQos.cpp", @@ -1308,20 +1291,6 @@ cpp_library( "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h", "commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.h", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.h", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDisableIpv4Afi.h", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupEgressPolicy.h", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupIngressPolicy.h", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupMaxRoutes.h", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupNextHopSelf.h", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRemoteAsn.h", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRrClient.h", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.h", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersHoldTime.h", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.h", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.h", - "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.h", "commands/config/protocol/static/CmdConfigProtocolStatic.h", "commands/config/protocol/static/route/StaticRouteUtils.h", "commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.h", @@ -1380,6 +1349,7 @@ cpp_library( "commands/delete/protocol/CmdDeleteProtocol.h", "commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h", "commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h", + "commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.h", "commands/delete/protocol/static/CmdDeleteProtocolStatic.h", "commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.h", "commands/delete/qos/CmdDeleteQos.h", diff --git a/fboss/cli/fboss2/CmdListConfig.cpp b/fboss/cli/fboss2/CmdListConfig.cpp index b141cc0924e01..76ab199dad2da 100644 --- a/fboss/cli/fboss2/CmdListConfig.cpp +++ b/fboss/cli/fboss2/CmdListConfig.cpp @@ -41,24 +41,6 @@ #include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDisableIpv4Afi.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupEgressPolicy.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupIngressPolicy.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupMaxRoutes.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupNextHopSelf.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupPeerTag.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRemoteAsn.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRrClient.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersHoldTime.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.h" -#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.h" #include "fboss/cli/fboss2/commands/config/protocol/static/CmdConfigProtocolStatic.h" #include "fboss/cli/fboss2/commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.h" #include "fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.h" @@ -113,6 +95,7 @@ #include "fboss/cli/fboss2/commands/delete/protocol/CmdDeleteProtocol.h" #include "fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h" #include "fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.h" #include "fboss/cli/fboss2/commands/delete/protocol/static/CmdDeleteProtocolStatic.h" #include "fboss/cli/fboss2/commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.h" #include "fboss/cli/fboss2/commands/delete/qos/CmdDeleteQos.h" @@ -379,157 +362,22 @@ const CommandTree& kConfigCommandTree() { }, { "peer-group", - "Configure BGP peer group", + "Configure BGP peer-group: " + "[ ...] (remote-asn, local-asn, " + "description, peer-tag, ingress-policy, " + "egress-policy, rr-client, confed-peer, " + "redistribute-peer, enhanced-route-refresh, " + "connect-mode, next-hop-self, add-path send|receive, " + "afi disable-ipv4-afi|disable-ipv6-afi|" + "ipv4-over-ipv6-nh, " + "graceful-restart restart-time|stateful-ha, " + "max-route pre-filter|post-filter|" + "pre-warning-threshold|post-warning-threshold|" + "pre-warning-only|post-warning-only, " + "timers hold-time|keepalive|out-delay|" + "withdraw-unprog-delay)", commandHandler, argRegistrar, - { - { - "description", - "Set peer group description", - commandHandler< - CmdConfigProtocolBgpPeerGroupDescription>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupDescriptionTraits>, - }, - { - "remote-asn", - "Set remote AS number", - commandHandler< - CmdConfigProtocolBgpPeerGroupRemoteAsn>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupRemoteAsnTraits>, - }, - { - "next-hop-self", - "Enable/disable next-hop-self", - commandHandler< - CmdConfigProtocolBgpPeerGroupNextHopSelf>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupNextHopSelfTraits>, - }, - { - "confed-peer", - "Enable/disable confederation peer", - commandHandler< - CmdConfigProtocolBgpPeerGroupConfedPeer>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupConfedPeerTraits>, - }, - { - "disable-ipv4-afi", - "Enable/disable IPv4 AFI", - commandHandler< - CmdConfigProtocolBgpPeerGroupDisableIpv4Afi>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupDisableIpv4AfiTraits>, - }, - { - "v4-over-v6-nh", - "Enable/disable v4-over-v6 next-hop", - commandHandler< - CmdConfigProtocolBgpPeerGroupV4OverV6Nh>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupV4OverV6NhTraits>, - }, - { - "rr-client", - "Enable/disable route reflector client", - commandHandler< - CmdConfigProtocolBgpPeerGroupRrClient>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupRrClientTraits>, - }, - { - "ingress-policy", - "Set ingress policy", - commandHandler< - CmdConfigProtocolBgpPeerGroupIngressPolicy>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupIngressPolicyTraits>, - }, - { - "egress-policy", - "Set egress policy", - commandHandler< - CmdConfigProtocolBgpPeerGroupEgressPolicy>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupEgressPolicyTraits>, - }, - { - "max-routes", - "Set maximum routes", - commandHandler< - CmdConfigProtocolBgpPeerGroupMaxRoutes>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupMaxRoutesTraits>, - }, - { - "peer-tag", - "Set peer tag", - commandHandler< - CmdConfigProtocolBgpPeerGroupPeerTag>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupPeerTagTraits>, - }, - { - "warning-only", - "Enable/disable warning-only mode", - commandHandler< - CmdConfigProtocolBgpPeerGroupWarningOnly>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupWarningOnlyTraits>, - }, - { - "warning-limit", - "Set warning limit percentage", - commandHandler< - CmdConfigProtocolBgpPeerGroupWarningLimit>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupWarningLimitTraits>, - }, - { - "timers", - "Configure BGP timers", - commandHandler< - CmdConfigProtocolBgpPeerGroupTimers>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupTimersTraits>, - { - { - "hold-time", - "Set hold time", - commandHandler< - CmdConfigProtocolBgpPeerGroupTimersHoldTime>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupTimersHoldTimeTraits>, - }, - { - "keepalive", - "Set keepalive interval", - commandHandler< - CmdConfigProtocolBgpPeerGroupTimersKeepalive>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupTimersKeepaliveTraits>, - }, - { - "out-delay", - "Set output delay", - commandHandler< - CmdConfigProtocolBgpPeerGroupTimersOutDelay>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupTimersOutDelayTraits>, - }, - { - "withdraw-unprog-delay", - "Set withdraw unprogrammed delay", - commandHandler< - CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay>, - argRegistrar< - CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelayTraits>, - }, - }, - }, - }, }, { "neighbor", @@ -870,11 +718,17 @@ const CommandTree& kConfigCommandTree() { commandHandler, argTypeHandler, {{ - "neighbor", - "Delete a BGP neighbor: ", - commandHandler, - argRegistrar, - }}, + "neighbor", + "Delete a BGP neighbor: ", + commandHandler, + argRegistrar, + }, + { + "peer-group", + "Delete a BGP peer-group: ", + commandHandler, + argRegistrar, + }}, }, { "static", diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp index 9fdb301293a89..a9eede5cdd694 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp @@ -13,30 +13,480 @@ #include "fboss/cli/fboss2/CmdHandler.cpp" #include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "configerator/structs/neteng/fboss/bgp/if/gen-cpp2/bgp_attr_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliAttrHandlers.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" +#include "fmt/format.h" namespace facebook::fboss { +namespace { + +// CLI attribute names, exactly as documented (two-token attributes keep their +// space). Kept here (not as raw string literals at the dispatch sites) so the +// valid-attribute set and the handler table stay in sync. +constexpr std::string_view kRemoteAsn = "remote-asn"; +constexpr std::string_view kLocalAsn = "local-asn"; +constexpr std::string_view kDescription = "description"; +constexpr std::string_view kPeerTag = "peer-tag"; +constexpr std::string_view kIngressPolicy = "ingress-policy"; +constexpr std::string_view kEgressPolicy = "egress-policy"; +constexpr std::string_view kRrClient = "rr-client"; +constexpr std::string_view kRedistributePeer = "redistribute-peer"; +constexpr std::string_view kEnhancedRouteRefresh = "enhanced-route-refresh"; +constexpr std::string_view kConnectMode = "connect-mode"; +constexpr std::string_view kAddPathSend = "add-path send"; +constexpr std::string_view kAddPathReceive = "add-path receive"; +constexpr std::string_view kAfiDisableIpv4Afi = "afi disable-ipv4-afi"; +constexpr std::string_view kAfiDisableIpv6Afi = "afi disable-ipv6-afi"; +constexpr std::string_view kAfiIpv4OverIpv6Nh = "afi ipv4-over-ipv6-nh"; +constexpr std::string_view kGracefulRestartTime = + "graceful-restart restart-time"; +constexpr std::string_view kGracefulRestartStatefulHa = + "graceful-restart stateful-ha"; +constexpr std::string_view kMaxRoutePreFilter = "max-route pre-filter"; +constexpr std::string_view kMaxRoutePostFilter = "max-route post-filter"; +constexpr std::string_view kMaxRoutePostWarningThreshold = + "max-route post-warning-threshold"; +constexpr std::string_view kTimersHoldTime = "timers hold-time"; +constexpr std::string_view kTimersKeepalive = "timers keepalive"; +constexpr std::string_view kTimersOutDelay = "timers out-delay"; +// Parity attributes: not in the documented peer-group grammar, but supported +// by the per-attribute peer-group commands this dispatcher replaces (and by +// real PeerGroup thrift fields), so dropping them would regress existing +// usage. +constexpr std::string_view kConfedPeer = "confed-peer"; +constexpr std::string_view kNextHopSelf = "next-hop-self"; +constexpr std::string_view kTimersWithdrawUnprogDelay = + "timers withdraw-unprog-delay"; +constexpr std::string_view kMaxRoutePreWarningThreshold = + "max-route pre-warning-threshold"; +constexpr std::string_view kMaxRoutePreWarningOnly = + "max-route pre-warning-only"; +constexpr std::string_view kMaxRoutePostWarningOnly = + "max-route post-warning-only"; + +// connect-mode values +constexpr std::string_view kConnectModePassive = "PASSIVE"; +constexpr std::string_view kConnectModeActive = "ACTIVE"; +constexpr std::string_view kConnectModeBoth = "BOTH"; + +using PeerGroup = bgp::thrift::PeerGroup; +using bgpcli::asnAttr; +using bgpcli::AttrHandler; +using bgpcli::boolAttr; +using bgpcli::err; +using bgpcli::joinedStringAttr; +using bgpcli::ok; +using bgpcli::parseBool; +using bgpcli::Result; +using bgpcli::routeCountAttr; +using bgpcli::secondsAttr; +using bgpcli::stringAttr; +using bgpcli::Tokens; +using facebook::neteng::fboss::bgp_attr::AddPath; + +// ---- per-attribute setters ------------------------------------------------- +// Pure thrift assignment: no validation, no messages. Parsing, bounds and the +// user-facing text all live in the shared factories in BgpCliAttrHandlers.h, +// so a setter is just the field this attribute writes. Field names map 1:1 to +// bgp_config.thrift, so the compiler validates them. + +void setRemoteAsn(PeerGroup& g, int64_t v) { + g.remote_as_4_byte() = v; +} + +void setLocalAsn(PeerGroup& g, int64_t v) { + g.local_as_4_byte() = v; +} + +void setDescription(PeerGroup& g, const std::string& v) { + g.description() = v; +} + +void setPeerTag(PeerGroup& g, const std::string& v) { + g.peer_tag() = v; +} + +void setIngressPolicy(PeerGroup& g, const std::string& v) { + g.ingress_policy_name() = v; +} + +void setEgressPolicy(PeerGroup& g, const std::string& v) { + g.egress_policy_name() = v; +} + +void setRrClient(PeerGroup& g, bool v) { + g.is_rr_client() = v; +} + +void setConfedPeer(PeerGroup& g, bool v) { + g.is_confed_peer() = v; +} + +void setRedistributePeer(PeerGroup& g, bool v) { + g.is_redistribute_peer() = v; +} + +void setEnhancedRouteRefresh(PeerGroup& g, bool v) { + g.enhanced_route_refresh() = v; +} + +void setDisableIpv4Afi(PeerGroup& g, bool v) { + g.disable_ipv4_afi() = v; +} + +void setDisableIpv6Afi(PeerGroup& g, bool v) { + g.disable_ipv6_afi() = v; +} + +void setIpv4OverIpv6Nh(PeerGroup& g, bool v) { + g.v4_over_v6_nexthop() = v; +} + +void setGracefulRestartTime(PeerGroup& g, int32_t v) { + g.bgp_peer_timers().ensure().graceful_restart_seconds() = v; +} + +void setGracefulRestartStatefulHa(PeerGroup& g, bool v) { + g.enable_stateful_ha() = v; +} + +void setMaxRoutePreFilter(PeerGroup& g, int64_t v) { + g.pre_filter().ensure().max_routes() = v; +} + +void setMaxRoutePostFilter(PeerGroup& g, int64_t v) { + g.post_filter().ensure().max_routes() = v; +} + +// RouteLimit.warning_limit is an absolute route count in the thrift schema, +// not a percentage. +void setMaxRoutePreWarningThreshold(PeerGroup& g, int64_t v) { + g.pre_filter().ensure().warning_limit() = v; +} + +void setMaxRoutePostWarningThreshold(PeerGroup& g, int64_t v) { + g.post_filter().ensure().warning_limit() = v; +} + +void setMaxRoutePreWarningOnly(PeerGroup& g, bool v) { + g.pre_filter().ensure().warning_only() = v; +} + +void setMaxRoutePostWarningOnly(PeerGroup& g, bool v) { + g.post_filter().ensure().warning_only() = v; +} + +void setTimersHoldTime(PeerGroup& g, int32_t v) { + g.bgp_peer_timers().ensure().hold_time_seconds() = v; +} + +void setTimersKeepalive(PeerGroup& g, int32_t v) { + g.bgp_peer_timers().ensure().keep_alive_seconds() = v; +} + +void setTimersOutDelay(PeerGroup& g, int32_t v) { + g.bgp_peer_timers().ensure().out_delay_seconds() = v; +} + +void setTimersWithdrawUnprogDelay(PeerGroup& g, int32_t v) { + g.bgp_peer_timers().ensure().withdraw_unprog_delay_seconds() = v; +} + +void setNextHopSelf(PeerGroup& g, bool v) { + g.next_hop_self() = v; +} + +// ---- hand-written handlers ------------------------------------------------- +// Only for value shapes no factory covers, because the shape is unique to this +// attribute rather than reusable. Both mirror the neighbor dispatcher's +// versions, which write the identically-named BgpPeer fields. + +// connect-mode: the thrift model only has is_passive (listen vs actively +// connect), so this is not an enum mapping — BOTH has no representation and is +// rejected with an explanation rather than silently mapped. +Result connectMode(PeerGroup& group, const Tokens& values) { + if (values.size() != 1) { + return err( + fmt::format( + "Error: {} requires <{}|{}>", + kConnectMode, + kConnectModePassive, + kConnectModeActive)); + } + if (values[0] == kConnectModePassive) { + group.is_passive() = true; + } else if (values[0] == kConnectModeActive) { + group.is_passive() = false; + } else if (values[0] == kConnectModeBoth) { + return err( + fmt::format( + "Error: {} {} has no representation in bgp_config.thrift " + "(PeerGroup only models is_passive); use {} or {}", + kConnectMode, + kConnectModeBoth, + kConnectModePassive, + kConnectModeActive)); + } else { + return err( + fmt::format( + "Error: Invalid {} value '{}'; expected {} or {}", + kConnectMode, + values[0], + kConnectModePassive, + kConnectModeActive)); + } + return ok(fmt::format("Successfully set {} to: {}", kConnectMode, values[0])); +} + +// add-path send/receive: a boolean at the CLI, but the two attributes share +// one thrift field — add_path is a single enum whose values form a bitmask by +// design (RECEIVE=1, SEND=2, BOTH=3). Each takes the boolAttr text but merges +// its direction into the current value instead of overwriting it; clearing the +// last direction unsets the field. +Result applyAddPath(PeerGroup& group, bool send, const Tokens& values) { + const std::string_view name = send ? kAddPathSend : kAddPathReceive; + if (values.size() != 1) { + return err(fmt::format("Error: {} requires ", name)); + } + auto enable = parseBool(values[0]); + if (!enable) { + return err( + fmt::format( + "Error: Invalid {} value '{}'; expected true or false", + name, + values[0])); + } + int bits = group.add_path() ? static_cast(*group.add_path()) : 0; + const int bit = send ? static_cast(AddPath::SEND) + : static_cast(AddPath::RECEIVE); + bits = *enable ? (bits | bit) : (bits & ~bit); + if (bits == 0) { + group.add_path().reset(); + } else { + group.add_path() = static_cast(bits); + } + return ok( + fmt::format( + "Successfully {} {}", *enable ? "enabled" : "disabled", name)); +} + +Result addPathSend(PeerGroup& group, const Tokens& values) { + return applyAddPath(group, /* send */ true, values); +} + +Result addPathReceive(PeerGroup& group, const Tokens& values) { + return applyAddPath(group, /* send */ false, values); +} + +// ---- the dispatch table ---------------------------------------------------- +// One line per attribute: dispatch key, value shape, setter. There are no +// handler bodies here by design — if an attribute appears to need one, its +// value shape is missing a factory and the fix is to add the factory. + +const std::map, std::less<>>& +attrHandlers() { + static const std::map, std::less<>> + kHandlers = { + {std::string(kRemoteAsn), + asnAttr(kRemoteAsn, setRemoteAsn)}, + {std::string(kLocalAsn), asnAttr(kLocalAsn, setLocalAsn)}, + {std::string(kDescription), + joinedStringAttr(kDescription, setDescription)}, + {std::string(kPeerTag), + stringAttr(kPeerTag, "string", setPeerTag)}, + {std::string(kIngressPolicy), + stringAttr( + kIngressPolicy, "policy-name", setIngressPolicy)}, + {std::string(kEgressPolicy), + stringAttr( + kEgressPolicy, "policy-name", setEgressPolicy)}, + {std::string(kRrClient), boolAttr(kRrClient, setRrClient)}, + {std::string(kConfedPeer), + boolAttr(kConfedPeer, setConfedPeer)}, + {std::string(kRedistributePeer), + boolAttr(kRedistributePeer, setRedistributePeer)}, + {std::string(kEnhancedRouteRefresh), + boolAttr(kEnhancedRouteRefresh, setEnhancedRouteRefresh)}, + {std::string(kConnectMode), connectMode}, + {std::string(kAddPathSend), addPathSend}, + {std::string(kAddPathReceive), addPathReceive}, + {std::string(kAfiDisableIpv4Afi), + boolAttr(kAfiDisableIpv4Afi, setDisableIpv4Afi)}, + {std::string(kAfiDisableIpv6Afi), + boolAttr(kAfiDisableIpv6Afi, setDisableIpv6Afi)}, + {std::string(kAfiIpv4OverIpv6Nh), + boolAttr(kAfiIpv4OverIpv6Nh, setIpv4OverIpv6Nh)}, + {std::string(kGracefulRestartTime), + secondsAttr( + kGracefulRestartTime, setGracefulRestartTime)}, + {std::string(kGracefulRestartStatefulHa), + boolAttr( + kGracefulRestartStatefulHa, setGracefulRestartStatefulHa)}, + {std::string(kMaxRoutePreFilter), + routeCountAttr(kMaxRoutePreFilter, setMaxRoutePreFilter)}, + {std::string(kMaxRoutePostFilter), + routeCountAttr( + kMaxRoutePostFilter, setMaxRoutePostFilter)}, + {std::string(kMaxRoutePreWarningThreshold), + routeCountAttr( + kMaxRoutePreWarningThreshold, setMaxRoutePreWarningThreshold)}, + {std::string(kMaxRoutePostWarningThreshold), + routeCountAttr( + kMaxRoutePostWarningThreshold, setMaxRoutePostWarningThreshold)}, + {std::string(kMaxRoutePreWarningOnly), + boolAttr( + kMaxRoutePreWarningOnly, setMaxRoutePreWarningOnly)}, + {std::string(kMaxRoutePostWarningOnly), + boolAttr( + kMaxRoutePostWarningOnly, setMaxRoutePostWarningOnly)}, + {std::string(kTimersHoldTime), + secondsAttr(kTimersHoldTime, setTimersHoldTime)}, + {std::string(kTimersKeepalive), + secondsAttr(kTimersKeepalive, setTimersKeepalive)}, + {std::string(kTimersOutDelay), + secondsAttr(kTimersOutDelay, setTimersOutDelay)}, + {std::string(kTimersWithdrawUnprogDelay), + secondsAttr( + kTimersWithdrawUnprogDelay, setTimersWithdrawUnprogDelay)}, + {std::string(kNextHopSelf), + boolAttr(kNextHopSelf, setNextHopSelf)}, + }; + return kHandlers; +} + +std::string validAttrList() { + std::string out; + for (const auto& [name, _] : attrHandlers()) { + if (!out.empty()) { + out += ", "; + } + out += name; + } + return out; +} + +// Find the peer-group keyed by name, creating it if absent. Setting an +// attribute on a not-yet-created group implicitly creates it, so command +// ordering stays forgiving; a bare `peer-group ` creates one explicitly. +// Unlike a BgpPeer, PeerGroup's only non-optional field is `name`, so no +// address seeding is needed. +bgp::thrift::PeerGroup& findOrCreatePeerGroup( + bgp::thrift::BgpConfig& cfg, + const std::string& groupName) { + auto& groups = cfg.peer_groups().ensure(); + for (auto& group : groups) { + if (*group.name() == groupName) { + return group; + } + } + groups.emplace_back(); + auto& group = groups.back(); + group.name() = groupName; + return group; +} + +// Whether a peer-group with the given name already exists. +bool peerGroupExists( + const bgp::thrift::BgpConfig& cfg, + const std::string& groupName) { + if (!cfg.peer_groups().has_value()) { + return false; + } + for (const auto& group : *cfg.peer_groups()) { + if (*group.name() == groupName) { + return true; + } + } + return false; +} + +} // namespace + +// Parse + validate at construction so queryClient stays a thin dispatch. +// Throwing std::invalid_argument is how the framework surfaces arg parse +// errors (same mechanism as BgpNeighborConfig). +BgpPeerGroupConfig::BgpPeerGroupConfig(std::vector v) + : utils::BaseObjectArgType(v) { + if (v.empty()) { + throw std::invalid_argument( + "Error: peer-group is required, followed by an optional " + " "); + } + if (v[0].empty()) { + throw std::invalid_argument("Error: peer-group name must not be empty"); + } + groupName_ = v[0]; + + if (v.size() == 1) { + return; // bare `peer-group `: create the group + } + + // Attributes may span two tokens (e.g. `timers hold-time`); match the + // longest prefix of the remaining tokens against the dispatch table. + const auto& handlers = attrHandlers(); + if (v.size() >= 3) { + std::string twoToken = v[1] + " " + v[2]; + if (handlers.find(twoToken) != handlers.end()) { + attr_ = std::move(twoToken); + values_.assign(v.begin() + 3, v.end()); + return; + } + } + if (handlers.find(v[1]) != handlers.end()) { + attr_ = v[1]; + values_.assign(v.begin() + 2, v.end()); + return; + } + throw std::invalid_argument( + fmt::format( + "Error: unknown peer-group attribute '{}'. Valid attributes: {}", + v[1], + validAttrList())); +} + CmdConfigProtocolBgpPeerGroupTraits::RetType CmdConfigProtocolBgpPeerGroup::queryClient( const HostInfo& /* hostInfo */, - const ObjectArgType& groupNameMsg) { - auto& session = BgpConfigSession::getInstance(); + const ObjectArgType& args) { + auto& session = ConfigSession::getInstance(); + auto& cfg = session.getBgpConfig(); + const bool created = !peerGroupExists(cfg, args.groupName()); + auto& group = findOrCreatePeerGroup(cfg, args.groupName()); - if (groupNameMsg.data().empty()) { - return "Error: peer-group name is required"; + Result result = args.attr().empty() + ? ok(fmt::format( + "Successfully created BGP peer-group {}", args.groupName())) + // The attribute is guaranteed valid: BgpPeerGroupConfig's constructor + // rejects an unknown attribute before we get here. + : attrHandlers().find(args.attr())->second(group, args.values()); + if (result.ok) { + if (!args.attr().empty()) { + result.message += fmt::format(" for peer-group {}", args.groupName()); + } + session.saveBgpConfig(); + result.message += + fmt::format("\nConfig saved to: {}", session.getBgpSessionConfigPath()); + } else if (created) { + // A rejected value must not leave a half-created group in the in-memory + // config (visible to later lookups in the same process, e.g. tests). + cfg.peer_groups()->pop_back(); } - - std::string groupName = groupNameMsg.data()[0]; - session.createPeerGroup(groupName); - session.saveConfig(); - - return fmt::format( - "BGP peer-group {} created.\n" - "Use subcommands: remote-asn, description, hold-time, keepalive\n" - "Config saved to: {}", - groupName, - session.getSessionConfigPath()); + return result.message; } void CmdConfigProtocolBgpPeerGroup::printOutput(const RetType& output) { diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h index 1c6d74e0f375a..1eca3e696eff3 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h @@ -10,18 +10,54 @@ #pragma once +#include +#include +#include "CLI/App.hpp" #include "fboss/cli/fboss2/CmdHandler.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/CmdConfigProtocolBgp.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" namespace facebook::fboss { +// Parsed `peer-group [ ...]`, validated at +// construction. Attributes may span two tokens (e.g. `timers hold-time`), +// matched longest-prefix-first. Bare `peer-group ` creates the group. +// Mirrors BgpNeighborConfig. +class BgpPeerGroupConfig : public utils::BaseObjectArgType { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + /* implicit */ BgpPeerGroupConfig(std::vector v); + const std::string& groupName() const { + return groupName_; + } + const std::string& attr() const { + return attr_; + } + const std::vector& values() const { + return values_; + } + const static utils::ObjectArgTypeId id = + utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_MESSAGE; + + private: + std::string groupName_; + std::string attr_; // matched dispatch key ("" = bare create) + std::vector values_; +}; + +// Single handler for the whole `config protocol bgp peer-group` family; +// dispatch and per-attribute parsing live in the .cpp so a new tunable is a +// one-entry change. Mirrors CmdConfigProtocolBgpNeighbor. struct CmdConfigProtocolBgpPeerGroupTraits : public WriteCommandTraits { using ParentCmd = CmdConfigProtocolBgp; static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); + // Stops CLI11 from classifying attribute tokens as subcommands once the + // group name is consumed. See CmdConfigProtocolBgpNeighborTraits. + cmd.positionals_at_end(); + cmd.add_option("args", args, " [ ...]"); } - using ObjectArgType = utils::Message; + using ObjectArgType = BgpPeerGroupConfig; using RetType = std::string; }; @@ -32,7 +68,7 @@ class CmdConfigProtocolBgpPeerGroup : public CmdHandler< using ObjectArgType = CmdConfigProtocolBgpPeerGroupTraits::ObjectArgType; using RetType = CmdConfigProtocolBgpPeerGroupTraits::RetType; - RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& groupName); + RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& args); void printOutput(const RetType& output); }; diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.cpp deleted file mode 100644 index 13b8c1bd31e76..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.cpp +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupConfedPeerTraits::RetType -CmdConfigProtocolBgpPeerGroupConfedPeer::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& peerGroupNameMsg, - const ObjectArgType& enableMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerGroupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - std::string peerGroupName = peerGroupNameMsg.data()[0]; - - if (enableMsg.data().empty()) { - return fmt::format( - "Error: enable value required for peer-group '{}'", peerGroupName); - } - std::string enableStr = enableMsg.data()[0]; - bool enable = (enableStr == "true" || enableStr == "1" || enableStr == "yes"); - - session.createPeerGroup(peerGroupName); - session.setPeerGroupConfedPeer(peerGroupName, enable); - session.saveConfig(); - - return fmt::format( - "Successfully {} confederation-peer for peer-group '{}'\n" - "Config saved to: {}", - enable ? "enabled" : "disabled", - peerGroupName, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupConfedPeer::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupConfedPeer, - CmdConfigProtocolBgpPeerGroupConfedPeerTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.h deleted file mode 100644 index 32fb0f19c28a6..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupConfedPeer.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupConfedPeerTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroup; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupConfedPeer - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupConfedPeer, - CmdConfigProtocolBgpPeerGroupConfedPeerTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupConfedPeerTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupConfedPeerTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& peerGroupName, - const ObjectArgType& enable); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.cpp deleted file mode 100644 index f316e3477be66..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.cpp +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupDescriptionTraits::RetType -CmdConfigProtocolBgpPeerGroupDescription::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& groupNameMsg, - const ObjectArgType& descriptionMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (groupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - if (descriptionMsg.data().empty()) { - return "Error: description is required"; - } - - std::string groupName = groupNameMsg.data()[0]; - std::string description = descriptionMsg.data()[0]; - - session.setPeerGroupDescription(groupName, description); - session.saveConfig(); - - return fmt::format( - "Successfully set peer-group {} description to: {}\nConfig saved to: {}", - groupName, - description, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupDescription::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupDescription, - CmdConfigProtocolBgpPeerGroupDescriptionTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.h deleted file mode 100644 index 37ee936eeaaa1..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDescription.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupDescriptionTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroup; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupDescription - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupDescription, - CmdConfigProtocolBgpPeerGroupDescriptionTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupDescriptionTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupDescriptionTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& groupName, - const ObjectArgType& description); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDisableIpv4Afi.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDisableIpv4Afi.cpp deleted file mode 100644 index 0ab361e4cbeb7..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDisableIpv4Afi.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDisableIpv4Afi.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupDisableIpv4AfiTraits::RetType -CmdConfigProtocolBgpPeerGroupDisableIpv4Afi::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& peerGroupNameMsg, - const ObjectArgType& disableMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerGroupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - std::string peerGroupName = peerGroupNameMsg.data()[0]; - - if (disableMsg.data().empty()) { - return fmt::format( - "Error: disable value required for peer-group '{}'", peerGroupName); - } - std::string disableStr = disableMsg.data()[0]; - bool disable = - (disableStr == "true" || disableStr == "1" || disableStr == "yes"); - - session.createPeerGroup(peerGroupName); - session.setPeerGroupDisableIpv4Afi(peerGroupName, disable); - session.saveConfig(); - - return fmt::format( - "Successfully {} IPv4 AFI for peer-group '{}'\n" - "Config saved to: {}", - disable ? "disabled" : "enabled", - peerGroupName, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupDisableIpv4Afi::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupDisableIpv4Afi, - CmdConfigProtocolBgpPeerGroupDisableIpv4AfiTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDisableIpv4Afi.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDisableIpv4Afi.h deleted file mode 100644 index cd06d0f7568d8..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupDisableIpv4Afi.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupDisableIpv4AfiTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroup; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupDisableIpv4Afi - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupDisableIpv4Afi, - CmdConfigProtocolBgpPeerGroupDisableIpv4AfiTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupDisableIpv4AfiTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupDisableIpv4AfiTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& peerGroupName, - const ObjectArgType& disable); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupEgressPolicy.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupEgressPolicy.cpp deleted file mode 100644 index de09a95dedf21..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupEgressPolicy.cpp +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupEgressPolicy.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupEgressPolicyTraits::RetType -CmdConfigProtocolBgpPeerGroupEgressPolicy::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& peerGroupNameMsg, - const ObjectArgType& policyMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerGroupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - std::string peerGroupName = peerGroupNameMsg.data()[0]; - - if (policyMsg.data().empty()) { - return fmt::format( - "Error: egress policy name is required for peer-group '{}'", - peerGroupName); - } - std::string policyName = policyMsg.data()[0]; - - session.createPeerGroup(peerGroupName); - session.setPeerGroupEgressPolicy(peerGroupName, policyName); - session.saveConfig(); - - return fmt::format( - "Successfully set egress policy for peer-group '{}' to: '{}'\n" - "Config saved to: {}", - peerGroupName, - policyName, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupEgressPolicy::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupEgressPolicy, - CmdConfigProtocolBgpPeerGroupEgressPolicyTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupEgressPolicy.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupEgressPolicy.h deleted file mode 100644 index e55ae153a80d7..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupEgressPolicy.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupEgressPolicyTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroup; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupEgressPolicy - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupEgressPolicy, - CmdConfigProtocolBgpPeerGroupEgressPolicyTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupEgressPolicyTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupEgressPolicyTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& peerGroupName, - const ObjectArgType& policyName); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupIngressPolicy.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupIngressPolicy.cpp deleted file mode 100644 index 3e7b7fe50d2b9..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupIngressPolicy.cpp +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupIngressPolicy.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupIngressPolicyTraits::RetType -CmdConfigProtocolBgpPeerGroupIngressPolicy::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& peerGroupNameMsg, - const ObjectArgType& policyMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerGroupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - std::string peerGroupName = peerGroupNameMsg.data()[0]; - - if (policyMsg.data().empty()) { - return fmt::format( - "Error: ingress policy name is required for peer-group '{}'", - peerGroupName); - } - std::string policyName = policyMsg.data()[0]; - - session.createPeerGroup(peerGroupName); - session.setPeerGroupIngressPolicy(peerGroupName, policyName); - session.saveConfig(); - - return fmt::format( - "Successfully set ingress policy for peer-group '{}' to: '{}'\n" - "Config saved to: {}", - peerGroupName, - policyName, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupIngressPolicy::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupIngressPolicy, - CmdConfigProtocolBgpPeerGroupIngressPolicyTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupIngressPolicy.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupIngressPolicy.h deleted file mode 100644 index f3345936405ae..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupIngressPolicy.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupIngressPolicyTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroup; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupIngressPolicy - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupIngressPolicy, - CmdConfigProtocolBgpPeerGroupIngressPolicyTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupIngressPolicyTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupIngressPolicyTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& peerGroupName, - const ObjectArgType& policyName); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupMaxRoutes.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupMaxRoutes.cpp deleted file mode 100644 index a3af4c904f8b4..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupMaxRoutes.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupMaxRoutes.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupMaxRoutesTraits::RetType -CmdConfigProtocolBgpPeerGroupMaxRoutes::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& peerGroupNameMsg, - const ObjectArgType& maxRoutesMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerGroupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - std::string peerGroupName = peerGroupNameMsg.data()[0]; - - if (maxRoutesMsg.data().empty()) { - return fmt::format( - "Error: max-routes value is required for peer-group '{}'", - peerGroupName); - } - - int64_t maxRoutes; - try { - maxRoutes = folly::to(maxRoutesMsg.data()[0]); - } catch (const std::exception& e) { - return fmt::format( - "Error: invalid max-routes value '{}': {}", - maxRoutesMsg.data()[0], - e.what()); - } - - session.createPeerGroup(peerGroupName); - session.setPeerGroupPreFilterMaxRoutes(peerGroupName, maxRoutes); - session.saveConfig(); - - return fmt::format( - "Successfully set max-routes for peer-group '{}' to: {}\n" - "Config saved to: {}", - peerGroupName, - maxRoutes, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupMaxRoutes::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupMaxRoutes, - CmdConfigProtocolBgpPeerGroupMaxRoutesTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupMaxRoutes.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupMaxRoutes.h deleted file mode 100644 index 655b445f342cb..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupMaxRoutes.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupMaxRoutesTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroup; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupMaxRoutes - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupMaxRoutes, - CmdConfigProtocolBgpPeerGroupMaxRoutesTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupMaxRoutesTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupMaxRoutesTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& peerGroupName, - const ObjectArgType& maxRoutes); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupNextHopSelf.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupNextHopSelf.cpp deleted file mode 100644 index 6800cdddac8c9..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupNextHopSelf.cpp +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupNextHopSelf.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupNextHopSelfTraits::RetType -CmdConfigProtocolBgpPeerGroupNextHopSelf::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& peerGroupNameMsg, - const ObjectArgType& enableMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerGroupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - std::string peerGroupName = peerGroupNameMsg.data()[0]; - - if (enableMsg.data().empty()) { - return fmt::format( - "Error: enable value (true/false) is required for peer-group '{}'", - peerGroupName); - } - - std::string enableStr = enableMsg.data()[0]; - bool enable = (enableStr == "true" || enableStr == "1" || enableStr == "yes"); - - session.createPeerGroup(peerGroupName); - session.setPeerGroupNextHopSelf(peerGroupName, enable); - session.saveConfig(); - - return fmt::format( - "Successfully {} next-hop-self for peer-group '{}'\n" - "Config saved to: {}", - enable ? "enabled" : "disabled", - peerGroupName, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupNextHopSelf::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupNextHopSelf, - CmdConfigProtocolBgpPeerGroupNextHopSelfTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupNextHopSelf.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupNextHopSelf.h deleted file mode 100644 index 83e39041a99b9..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupNextHopSelf.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupNextHopSelfTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroup; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupNextHopSelf - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupNextHopSelf, - CmdConfigProtocolBgpPeerGroupNextHopSelfTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupNextHopSelfTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupNextHopSelfTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& peerGroupName, - const ObjectArgType& enable); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupPeerTag.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupPeerTag.cpp deleted file mode 100644 index 1fbb6a228e603..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupPeerTag.cpp +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupPeerTag.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupPeerTagTraits::RetType -CmdConfigProtocolBgpPeerGroupPeerTag::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& groupNameArg, - const ObjectArgType& peerTagMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (groupNameArg.data().empty()) { - return "Error: peer-group name is required"; - } - std::string groupName = groupNameArg.data()[0]; - - if (peerTagMsg.data().empty()) { - return fmt::format( - "Error: peer-tag value is required for peer-group '{}'", groupName); - } - - session.createPeerGroup(groupName); - session.setPeerGroupPeerTag(groupName, peerTagMsg.data()[0]); - session.saveConfig(); - - return fmt::format( - "Successfully set peer_tag for peer-group '{}' to: '{}'\n" - "Config saved to: {}", - groupName, - peerTagMsg.data()[0], - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupPeerTag::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupPeerTag, - CmdConfigProtocolBgpPeerGroupPeerTagTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupPeerTag.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupPeerTag.h deleted file mode 100644 index 4715f131fe8b8..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupPeerTag.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupPeerTagTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroup; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupPeerTag - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupPeerTag, - CmdConfigProtocolBgpPeerGroupPeerTagTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupPeerTagTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupPeerTagTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& groupName, - const ObjectArgType& peerTag); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRemoteAsn.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRemoteAsn.cpp deleted file mode 100644 index ea967b32e0e2d..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRemoteAsn.cpp +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRemoteAsn.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupRemoteAsnTraits::RetType -CmdConfigProtocolBgpPeerGroupRemoteAsn::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& groupNameMsg, - const ObjectArgType& remoteAsnMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (groupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - if (remoteAsnMsg.data().empty()) { - return "Error: remote-asn value is required"; - } - - std::string groupName = groupNameMsg.data()[0]; - std::string asnStr = remoteAsnMsg.data()[0]; - uint64_t remoteAsn; - try { - remoteAsn = folly::to(asnStr); - } catch (const std::exception& e) { - return fmt::format("Error: Invalid remote-asn '{}': {}", asnStr, e.what()); - } - - session.setPeerGroupRemoteAsn(groupName, remoteAsn); - session.saveConfig(); - - return fmt::format( - "Successfully set peer-group {} remote-asn to: {}\nConfig saved to: {}", - groupName, - remoteAsn, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupRemoteAsn::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupRemoteAsn, - CmdConfigProtocolBgpPeerGroupRemoteAsnTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRemoteAsn.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRemoteAsn.h deleted file mode 100644 index 7f7c79898d233..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRemoteAsn.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupRemoteAsnTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroup; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupRemoteAsn - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupRemoteAsn, - CmdConfigProtocolBgpPeerGroupRemoteAsnTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupRemoteAsnTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupRemoteAsnTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& groupName, - const ObjectArgType& remoteAsn); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRrClient.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRrClient.cpp deleted file mode 100644 index e6cff47df37bb..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRrClient.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRrClient.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupRrClientTraits::RetType -CmdConfigProtocolBgpPeerGroupRrClient::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& peerGroupNameMsg, - const ObjectArgType& enableMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerGroupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - std::string peerGroupName = peerGroupNameMsg.data()[0]; - - if (enableMsg.data().empty()) { - return fmt::format( - "Error: enable value (true/false) is required for peer-group '{}'", - peerGroupName); - } - - std::string enableStr = enableMsg.data()[0]; - bool enable = (enableStr == "true" || enableStr == "1" || enableStr == "yes"); - - session.createPeerGroup(peerGroupName); - session.setPeerGroupRrClient(peerGroupName, enable); - session.saveConfig(); - - return fmt::format( - "Successfully {} route-reflector-client for peer-group '{}'\n" - "Config saved to: {}", - enable ? "enabled" : "disabled", - peerGroupName, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupRrClient::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupRrClient, - CmdConfigProtocolBgpPeerGroupRrClientTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRrClient.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRrClient.h deleted file mode 100644 index c98cac2f2f97c..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupRrClient.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupRrClientTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroup; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupRrClient - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupRrClient, - CmdConfigProtocolBgpPeerGroupRrClientTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupRrClientTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupRrClientTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& peerGroupName, - const ObjectArgType& enable); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.cpp deleted file mode 100644 index f1ebe72da4592..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupTimersTraits::RetType -CmdConfigProtocolBgpPeerGroupTimers::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& peerGroupNameMsg) { - if (peerGroupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - - std::string groupName = peerGroupNameMsg.data()[0]; - - return fmt::format( - "Configure timers for peer-group {}.\n" - "Use subcommands: hold-time, keepalive, out-delay", - groupName); -} - -void CmdConfigProtocolBgpPeerGroupTimers::printOutput(const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupTimers, - CmdConfigProtocolBgpPeerGroupTimersTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.h deleted file mode 100644 index 2b457c60909f3..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupTimersTraits : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroup; - using ObjectArgType = std::monostate; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupTimers - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupTimers, - CmdConfigProtocolBgpPeerGroupTimersTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupTimersTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupTimersTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& peerGroupName); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersHoldTime.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersHoldTime.cpp deleted file mode 100644 index 0e5b83da36dd2..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersHoldTime.cpp +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersHoldTime.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupTimersHoldTimeTraits::RetType -CmdConfigProtocolBgpPeerGroupTimersHoldTime::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& peerGroupNameMsg, - const ObjectArgType& holdTimeMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerGroupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - if (holdTimeMsg.data().empty()) { - return "Error: hold-time value is required"; - } - - std::string groupName = peerGroupNameMsg.data()[0]; - std::string holdTimeStr = holdTimeMsg.data()[0]; - int32_t holdTime; - try { - holdTime = folly::to(holdTimeStr); - } catch (const std::exception& e) { - return fmt::format( - "Error: Invalid hold-time '{}': {}", holdTimeStr, e.what()); - } - - session.setPeerGroupHoldTime(groupName, holdTime); - session.saveConfig(); - - return fmt::format( - "Successfully set peer-group {} hold-time to: {} seconds\nConfig saved to: {}", - groupName, - holdTime, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupTimersHoldTime::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupTimersHoldTime, - CmdConfigProtocolBgpPeerGroupTimersHoldTimeTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersHoldTime.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersHoldTime.h deleted file mode 100644 index 21351570d9400..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersHoldTime.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupTimersHoldTimeTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroupTimers; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupTimersHoldTime - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupTimersHoldTime, - CmdConfigProtocolBgpPeerGroupTimersHoldTimeTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupTimersHoldTimeTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupTimersHoldTimeTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& peerGroupName, - const ObjectArgType& holdTime); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.cpp deleted file mode 100644 index 7038f9a0e2c50..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupTimersKeepaliveTraits::RetType -CmdConfigProtocolBgpPeerGroupTimersKeepalive::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& peerGroupNameMsg, - const ObjectArgType& keepaliveMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerGroupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - std::string peerGroupName = peerGroupNameMsg.data()[0]; - - if (keepaliveMsg.data().empty()) { - return fmt::format( - "Error: keepalive interval is required for peer-group '{}'", - peerGroupName); - } - - int32_t keepalive; - try { - keepalive = folly::to(keepaliveMsg.data()[0]); - } catch (const std::exception& e) { - return fmt::format( - "Error: invalid keepalive value '{}': {}", - keepaliveMsg.data()[0], - e.what()); - } - - session.createPeerGroup(peerGroupName); - session.setPeerGroupKeepalive(peerGroupName, keepalive); - session.saveConfig(); - - return fmt::format( - "Successfully set keepalive interval for peer-group '{}' to: {} seconds\n" - "Config saved to: {}", - peerGroupName, - keepalive, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupTimersKeepalive::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupTimersKeepalive, - CmdConfigProtocolBgpPeerGroupTimersKeepaliveTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.h deleted file mode 100644 index c2088385b391d..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersKeepalive.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupTimersKeepaliveTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroupTimers; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupTimersKeepalive - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupTimersKeepalive, - CmdConfigProtocolBgpPeerGroupTimersKeepaliveTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupTimersKeepaliveTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupTimersKeepaliveTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& peerGroupName, - const ObjectArgType& keepalive); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.cpp deleted file mode 100644 index 21e4dc4c47eeb..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupTimersOutDelayTraits::RetType -CmdConfigProtocolBgpPeerGroupTimersOutDelay::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& peerGroupNameMsg, - const ObjectArgType& outDelayMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerGroupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - std::string peerGroupName = peerGroupNameMsg.data()[0]; - - if (outDelayMsg.data().empty()) { - return fmt::format( - "Error: out-delay value is required for peer-group '{}'", - peerGroupName); - } - - int32_t outDelay; - try { - outDelay = folly::to(outDelayMsg.data()[0]); - } catch (const std::exception& e) { - return fmt::format( - "Error: invalid out-delay value '{}': {}", - outDelayMsg.data()[0], - e.what()); - } - - session.createPeerGroup(peerGroupName); - session.setPeerGroupOutDelay(peerGroupName, outDelay); - session.saveConfig(); - - return fmt::format( - "Successfully set out-delay for peer-group '{}' to: {} seconds\n" - "Config saved to: {}", - peerGroupName, - outDelay, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupTimersOutDelay::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupTimersOutDelay, - CmdConfigProtocolBgpPeerGroupTimersOutDelayTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.h deleted file mode 100644 index 16542ecc3b225..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersOutDelay.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupTimersOutDelayTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroupTimers; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupTimersOutDelay - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupTimersOutDelay, - CmdConfigProtocolBgpPeerGroupTimersOutDelayTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupTimersOutDelayTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupTimersOutDelayTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& peerGroupName, - const ObjectArgType& outDelay); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay.cpp deleted file mode 100644 index 92ccb97ff4a69..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelayTraits::RetType -CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& groupNameArg, - const ObjectArgType& delayMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (groupNameArg.data().empty()) { - return "Error: peer-group name is required"; - } - std::string groupName = groupNameArg.data()[0]; - - if (delayMsg.data().empty()) { - return fmt::format( - "Error: withdraw-unprog-delay value (seconds) is required for peer-group '{}'", - groupName); - } - - int32_t delay; - try { - delay = std::stoi(delayMsg.data()[0]); - } catch (const std::exception& e) { - return fmt::format( - "Error: invalid withdraw-unprog-delay value '{}': {}", - delayMsg.data()[0], - e.what()); - } - - session.createPeerGroup(groupName); - session.setPeerGroupWithdrawUnprogDelay(groupName, delay); - session.saveConfig(); - - return fmt::format( - "Successfully set withdraw-unprog-delay for peer-group '{}' to: {} seconds\n" - "Config saved to: {}", - groupName, - delay, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay, - CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelayTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay.h deleted file mode 100644 index 83355fc1b1e96..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupTimers.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelayTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroupTimers; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelay, - CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelayTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelayTraits:: - ObjectArgType; - using RetType = - CmdConfigProtocolBgpPeerGroupTimersWithdrawUnprogDelayTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& peerGroupName, - const ObjectArgType& withdrawUnprogDelay); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.cpp deleted file mode 100644 index ae03372ca1686..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.cpp +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupV4OverV6NhTraits::RetType -CmdConfigProtocolBgpPeerGroupV4OverV6Nh::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& peerGroupNameMsg, - const ObjectArgType& enableMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (peerGroupNameMsg.data().empty()) { - return "Error: peer-group name is required"; - } - std::string peerGroupName = peerGroupNameMsg.data()[0]; - - if (enableMsg.data().empty()) { - return fmt::format( - "Error: enable value required for peer-group '{}'", peerGroupName); - } - std::string enableStr = enableMsg.data()[0]; - bool enable = (enableStr == "true" || enableStr == "1" || enableStr == "yes"); - - session.createPeerGroup(peerGroupName); - session.setPeerGroupIpv4OverIpv6Nh(peerGroupName, enable); - session.saveConfig(); - - return fmt::format( - "Successfully {} v4-over-v6-nexthop for peer-group '{}'\n" - "Config saved to: {}", - enable ? "enabled" : "disabled", - peerGroupName, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupV4OverV6Nh::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupV4OverV6Nh, - CmdConfigProtocolBgpPeerGroupV4OverV6NhTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.h deleted file mode 100644 index 89e5f72751f7b..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupV4OverV6Nh.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupV4OverV6NhTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroup; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupV4OverV6Nh - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupV4OverV6Nh, - CmdConfigProtocolBgpPeerGroupV4OverV6NhTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupV4OverV6NhTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupV4OverV6NhTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& peerGroupName, - const ObjectArgType& enable); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.cpp deleted file mode 100644 index ec21c05d82865..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupWarningLimitTraits::RetType -CmdConfigProtocolBgpPeerGroupWarningLimit::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& groupNameArg, - const ObjectArgType& warningLimitMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (groupNameArg.data().empty()) { - return "Error: peer-group name is required"; - } - std::string groupName = groupNameArg.data()[0]; - - if (warningLimitMsg.data().empty()) { - return fmt::format( - "Error: warning-limit value is required for peer-group '{}'", - groupName); - } - - int64_t warningLimit; - try { - warningLimit = std::stoll(warningLimitMsg.data()[0]); - } catch (const std::exception& e) { - return fmt::format( - "Error: invalid warning-limit value '{}': {}", - warningLimitMsg.data()[0], - e.what()); - } - - session.createPeerGroup(groupName); - session.setPeerGroupPreFilterWarningLimit(groupName, warningLimit); - session.saveConfig(); - - return fmt::format( - "Successfully set pre_filter warning_limit for peer-group '{}' to: {}\n" - "Config saved to: {}", - groupName, - warningLimit, - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupWarningLimit::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupWarningLimit, - CmdConfigProtocolBgpPeerGroupWarningLimitTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.h deleted file mode 100644 index c72b72da59750..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningLimit.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupWarningLimitTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroup; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupWarningLimit - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupWarningLimit, - CmdConfigProtocolBgpPeerGroupWarningLimitTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupWarningLimitTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupWarningLimitTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& groupName, - const ObjectArgType& warningLimit); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.cpp deleted file mode 100644 index ca39a63b8ecd4..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.cpp +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.h" - -#include "fboss/cli/fboss2/CmdHandler.cpp" - -#include -#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpConfigSession.h" - -namespace facebook::fboss { - -CmdConfigProtocolBgpPeerGroupWarningOnlyTraits::RetType -CmdConfigProtocolBgpPeerGroupWarningOnly::queryClient( - const HostInfo& /* hostInfo */, - const utils::Message& groupNameArg, - const ObjectArgType& warningOnlyMsg) { - auto& session = BgpConfigSession::getInstance(); - - if (groupNameArg.data().empty()) { - return "Error: peer-group name is required"; - } - std::string groupName = groupNameArg.data()[0]; - - if (warningOnlyMsg.data().empty()) { - return fmt::format( - "Error: warning-only value (true|false) is required for peer-group '{}'", - groupName); - } - - std::string value = warningOnlyMsg.data()[0]; - bool warningOnly; - if (value == "true" || value == "1") { - warningOnly = true; - } else if (value == "false" || value == "0") { - warningOnly = false; - } else { - return fmt::format( - "Error: invalid warning-only value '{}', expected 'true' or 'false'", - value); - } - - session.createPeerGroup(groupName); - session.setPeerGroupPreFilterWarningOnly(groupName, warningOnly); - session.saveConfig(); - - return fmt::format( - "Successfully set pre_filter warning_only for peer-group '{}' to: {}\n" - "Config saved to: {}", - groupName, - warningOnly ? "true" : "false", - session.getSessionConfigPath()); -} - -void CmdConfigProtocolBgpPeerGroupWarningOnly::printOutput( - const RetType& output) { - std::cout << output << std::endl; -} - -// Explicit template instantiation -template void CmdHandler< - CmdConfigProtocolBgpPeerGroupWarningOnly, - CmdConfigProtocolBgpPeerGroupWarningOnlyTraits>::run(); - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.h b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.h deleted file mode 100644 index 9288c4dd9ae45..0000000000000 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroupWarningOnly.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" -#include "fboss/cli/fboss2/utils/CmdUtils.h" - -namespace facebook::fboss { - -struct CmdConfigProtocolBgpPeerGroupWarningOnlyTraits - : public WriteCommandTraits { - using ParentCmd = CmdConfigProtocolBgpPeerGroup; - static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("msg", args, "Message"); - } - using ObjectArgType = utils::Message; - using RetType = std::string; -}; - -class CmdConfigProtocolBgpPeerGroupWarningOnly - : public CmdHandler< - CmdConfigProtocolBgpPeerGroupWarningOnly, - CmdConfigProtocolBgpPeerGroupWarningOnlyTraits> { - public: - using ObjectArgType = - CmdConfigProtocolBgpPeerGroupWarningOnlyTraits::ObjectArgType; - using RetType = CmdConfigProtocolBgpPeerGroupWarningOnlyTraits::RetType; - - RetType queryClient( - const HostInfo& hostInfo, - const utils::Message& groupName, - const ObjectArgType& warningOnly); - - void printOutput(const RetType& output); -}; - -} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.cpp b/fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.cpp new file mode 100644 index 0000000000000..7fbe569d326e1 --- /dev/null +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.cpp @@ -0,0 +1,81 @@ +/* + * 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/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.h" + +#include "fboss/cli/fboss2/CmdHandler.cpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" +#include "fmt/format.h" + +namespace facebook::fboss { + +// Parse + validate at construction so queryClient stays a thin dispatch. +// Throwing std::invalid_argument is how the framework surfaces arg parse +// errors (same mechanism as BgpPeerGroupConfig). +BgpPeerGroupRef::BgpPeerGroupRef(std::vector v) + : utils::BaseObjectArgType(v) { + if (v.size() != 1) { + throw std::invalid_argument( + "Error: delete protocol bgp peer-group requires exactly one "); + } + if (v[0].empty()) { + throw std::invalid_argument("Error: peer-group name must not be empty"); + } + groupName_ = std::move(v[0]); +} + +CmdDeleteProtocolBgpPeerGroupTraits::RetType +CmdDeleteProtocolBgpPeerGroup::queryClient( + const HostInfo& /* hostInfo */, + const ObjectArgType& args) { + auto& session = ConfigSession::getInstance(); + auto& cfg = session.getBgpConfig(); + if (!cfg.peer_groups().has_value()) { + // Nothing is persisted for an unknown group, so a typo'd delete can't + // stage an unrelated session change. + return fmt::format("Error: BGP peer-group {} not found", args.groupName()); + } + auto& groups = *cfg.peer_groups(); + auto it = std::find_if(groups.begin(), groups.end(), [&](const auto& group) { + return *group.name() == args.groupName(); + }); + if (it == groups.end()) { + return fmt::format("Error: BGP peer-group {} not found", args.groupName()); + } + groups.erase(it); + session.saveBgpConfig(); + return fmt::format( + "Successfully deleted BGP peer-group {}\nConfig saved to: {}", + args.groupName(), + session.getBgpSessionConfigPath()); +} + +void CmdDeleteProtocolBgpPeerGroup::printOutput(const RetType& output) { + std::cout << output << std::endl; +} + +// Explicit template instantiation +template void CmdHandler< + CmdDeleteProtocolBgpPeerGroup, + CmdDeleteProtocolBgpPeerGroupTraits>::run(); + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.h b/fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.h new file mode 100644 index 0000000000000..c9793226e9a24 --- /dev/null +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.h @@ -0,0 +1,61 @@ +/* + * 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 +#include +#include "CLI/App.hpp" +#include "fboss/cli/fboss2/CmdHandler.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +namespace facebook::fboss { + +// Parsed `delete protocol bgp peer-group `, validated at construction. +// The group name is the group's identity (an arbitrary string), matched +// exactly the same way the config command stores it. Mirrors BgpNeighborRef. +class BgpPeerGroupRef : public utils::BaseObjectArgType { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + /* implicit */ BgpPeerGroupRef(std::vector v); + const std::string& groupName() const { + return groupName_; + } + const static utils::ObjectArgTypeId id = + utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_MESSAGE; + + private: + std::string groupName_; +}; + +struct CmdDeleteProtocolBgpPeerGroupTraits : public WriteCommandTraits { + using ParentCmd = CmdDeleteProtocolBgp; + static void addCliArg(CLI::App& cmd, std::vector& args) { + cmd.add_option("args", args, ""); + } + using ObjectArgType = BgpPeerGroupRef; + using RetType = std::string; +}; + +class CmdDeleteProtocolBgpPeerGroup : public CmdHandler< + CmdDeleteProtocolBgpPeerGroup, + CmdDeleteProtocolBgpPeerGroupTraits> { + public: + using ObjectArgType = CmdDeleteProtocolBgpPeerGroupTraits::ObjectArgType; + using RetType = CmdDeleteProtocolBgpPeerGroupTraits::RetType; + + RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& args); + + void printOutput(const RetType& output); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/config/BUCK b/fboss/cli/fboss2/test/config/BUCK index e1991b9a16b35..56efb2732b425 100644 --- a/fboss/cli/fboss2/test/config/BUCK +++ b/fboss/cli/fboss2/test/config/BUCK @@ -13,6 +13,7 @@ cpp_unittest( "CmdConfigArpTest.cpp", "CmdConfigBgpGlobalTest.cpp", "CmdConfigBgpNeighborTest.cpp", + "CmdConfigBgpPeerGroupTest.cpp", "CmdConfigCoppTest.cpp", "CmdConfigDhcpTest.cpp", "CmdConfigHistoryTest.cpp", @@ -60,6 +61,7 @@ cpp_unittest( "CmdDeleteTunnelIpInIpTest.cpp", "CmdDeleteVlanTest.cpp", "ConfigSessionSystemdTest.cpp", + "CmdDeleteBgpPeerGroupTest.cpp", "ProfileValidationTest.cpp", ], headers = [ diff --git a/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp b/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp new file mode 100644 index 0000000000000..1585e6e683dcb --- /dev/null +++ b/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp @@ -0,0 +1,247 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include +#include +#include + +#include +#include +#include +#include + +#include "configerator/structs/neteng/fboss/bgp/if/gen-cpp2/bgp_attr_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/test/config/CmdConfigTestBase.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +using namespace ::testing; +using facebook::neteng::fboss::bgp_attr::AddPath; + +namespace facebook::fboss { + +// The peer-group dispatcher only touches the BGP side of ConfigSession, which +// seeds from thrift schema defaults when neither a staged session nor a +// system bgpcpp.conf exists — so no seed agent config is needed (mirrors +// CmdConfigBgpNeighborTest). +class CmdConfigBgpPeerGroupTestFixture : public CmdConfigTestBase { + public: + CmdConfigBgpPeerGroupTestFixture() + : CmdConfigTestBase("bgp_peer_group_test_%%%%-%%%%-%%%%", "") {} + + void SetUp() override { + CmdConfigTestBase::SetUp(); + setupTestableConfigSession(); + } + + std::string run(const std::vector& tokens) { + CmdConfigProtocolBgpPeerGroup cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpPeerGroupConfig(tokens)); + } + + const std::vector& groups() { + return ConfigSession::getInstance().getBgpConfig().peer_groups().ensure(); + } + + bool sessionFileExists() { + return std::filesystem::exists( + ConfigSession::getInstance().getBgpSessionConfigPath()); + } +}; + +// ============================================================================== +// BgpPeerGroupConfig (arg) validation +// ============================================================================== + +TEST_F(CmdConfigBgpPeerGroupTestFixture, argValidation) { + // Bare create. + EXPECT_EQ(BgpPeerGroupConfig({"SPINE"}).groupName(), "SPINE"); + EXPECT_TRUE(BgpPeerGroupConfig({"SPINE"}).attr().empty()); + + // Single-token attribute. + auto singleTok = BgpPeerGroupConfig({"SPINE", "remote-asn", "65000"}); + EXPECT_EQ(singleTok.attr(), "remote-asn"); + EXPECT_EQ(singleTok.values(), std::vector({"65000"})); + + // Two-token attribute wins longest-prefix match. + auto twoTok = BgpPeerGroupConfig({"SPINE", "timers", "hold-time", "30"}); + EXPECT_EQ(twoTok.attr(), "timers hold-time"); + EXPECT_EQ(twoTok.values(), std::vector({"30"})); + + // Invalid: empty, empty name, unknown attribute, incomplete group. + EXPECT_THROW(BgpPeerGroupConfig({}), std::invalid_argument); + EXPECT_THROW(BgpPeerGroupConfig({""}), std::invalid_argument); + EXPECT_THROW( + BgpPeerGroupConfig({"SPINE", "no-such-attr", "1"}), + std::invalid_argument); + EXPECT_THROW(BgpPeerGroupConfig({"SPINE", "timers"}), std::invalid_argument); +} + +// ============================================================================== +// Attribute handlers +// ============================================================================== + +TEST_F(CmdConfigBgpPeerGroupTestFixture, bareCreate) { + auto result = run({"SPINE"}); + EXPECT_THAT(result, HasSubstr("Successfully created BGP peer-group SPINE")); + ASSERT_EQ(groups().size(), 1); + EXPECT_EQ(*groups()[0].name(), "SPINE"); + // PeerGroup's only non-optional field is `name`; nothing else is seeded. + EXPECT_FALSE(groups()[0].local_addr().has_value()); + EXPECT_TRUE(sessionFileExists()); + + run({"LEAF"}); + ASSERT_EQ(groups().size(), 2); + EXPECT_EQ(*groups()[1].name(), "LEAF"); +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, setRemoteAsn) { + auto result = run({"SPINE", "remote-asn", "65000"}); + EXPECT_THAT(result, HasSubstr("65000")); + ASSERT_EQ(groups().size(), 1); + EXPECT_EQ(groups()[0].remote_as_4_byte().value_or(0), 65000); +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, timersAccumulate) { + run({"SPINE", "timers", "hold-time", "90"}); + run({"SPINE", "timers", "keepalive", "30"}); + run({"SPINE", "graceful-restart", "restart-time", "120"}); + ASSERT_EQ(groups().size(), 1); + const auto& timers = *groups()[0].bgp_peer_timers(); + EXPECT_EQ(*timers.hold_time_seconds(), 90); + EXPECT_EQ(*timers.keep_alive_seconds(), 30); + EXPECT_EQ(timers.graceful_restart_seconds().value_or(0), 120); +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, addPathMerge) { + run({"SPINE", "add-path", "receive", "true"}); + EXPECT_EQ(groups()[0].add_path().value_or(AddPath(0)), AddPath::RECEIVE); + + run({"SPINE", "add-path", "send", "true"}); + EXPECT_EQ(groups()[0].add_path().value_or(AddPath(0)), AddPath::BOTH); + + run({"SPINE", "add-path", "receive", "false"}); + EXPECT_EQ(groups()[0].add_path().value_or(AddPath(0)), AddPath::SEND); + + run({"SPINE", "add-path", "send", "false"}); + EXPECT_FALSE(groups()[0].add_path().has_value()); +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, connectMode) { + run({"SPINE", "connect-mode", "PASSIVE"}); + EXPECT_TRUE(groups()[0].is_passive().value_or(false)); + + run({"SPINE", "connect-mode", "ACTIVE"}); + EXPECT_FALSE(groups()[0].is_passive().value_or(true)); + + // BOTH has no thrift representation and must be rejected, leaving the + // previous value intact and nothing new persisted. + auto result = run({"SPINE", "connect-mode", "BOTH"}); + EXPECT_THAT(result, HasSubstr("Error")); + EXPECT_FALSE(groups()[0].is_passive().value_or(true)); +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, boolAndStringAttributes) { + run({"SPINE", "rr-client", "true"}); + run({"SPINE", "confed-peer", "false"}); + run({"SPINE", "redistribute-peer", "true"}); + run({"SPINE", "enhanced-route-refresh", "true"}); + run({"SPINE", "afi", "disable-ipv4-afi", "true"}); + run({"SPINE", "afi", "disable-ipv6-afi", "false"}); + run({"SPINE", "afi", "ipv4-over-ipv6-nh", "true"}); + run({"SPINE", "graceful-restart", "stateful-ha", "true"}); + run({"SPINE", "peer-tag", "FSW"}); + run({"SPINE", "ingress-policy", "IN_POLICY"}); + run({"SPINE", "egress-policy", "OUT_POLICY"}); + run({"SPINE", "local-asn", "64512"}); + run({"SPINE", "next-hop-self", "true"}); + run({"SPINE", "description", "spine", "group"}); + + ASSERT_EQ(groups().size(), 1); + const auto& group = groups()[0]; + EXPECT_TRUE(group.is_rr_client().value_or(false)); + EXPECT_FALSE(group.is_confed_peer().value_or(true)); + EXPECT_TRUE(group.is_redistribute_peer().value_or(false)); + EXPECT_TRUE(group.enhanced_route_refresh().value_or(false)); + EXPECT_TRUE(group.disable_ipv4_afi().value_or(false)); + EXPECT_FALSE(group.disable_ipv6_afi().value_or(true)); + EXPECT_TRUE(group.v4_over_v6_nexthop().value_or(false)); + EXPECT_TRUE(group.enable_stateful_ha().value_or(false)); + EXPECT_EQ(group.peer_tag().value_or(""), "FSW"); + EXPECT_EQ(group.ingress_policy_name().value_or(""), "IN_POLICY"); + EXPECT_EQ(group.egress_policy_name().value_or(""), "OUT_POLICY"); + EXPECT_EQ(group.local_as_4_byte().value_or(0), 64512); + EXPECT_TRUE(group.next_hop_self().value_or(false)); + // Multi-token description is re-joined. + EXPECT_EQ(group.description().value_or(""), "spine group"); +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, routeLimits) { + run({"SPINE", "max-route", "pre-filter", "45000"}); + run({"SPINE", "max-route", "post-filter", "50000"}); + run({"SPINE", "max-route", "post-warning-threshold", "48000"}); + // Parity attributes carried over from the deleted per-attribute commands. + run({"SPINE", "max-route", "pre-warning-threshold", "40000"}); + run({"SPINE", "max-route", "pre-warning-only", "true"}); + run({"SPINE", "max-route", "post-warning-only", "false"}); + const auto& group = groups()[0]; + EXPECT_EQ(*group.pre_filter()->max_routes(), 45000); + EXPECT_EQ(*group.post_filter()->max_routes(), 50000); + EXPECT_EQ(*group.post_filter()->warning_limit(), 48000); + EXPECT_EQ(*group.pre_filter()->warning_limit(), 40000); + EXPECT_TRUE(*group.pre_filter()->warning_only()); + EXPECT_FALSE(*group.post_filter()->warning_only()); +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, valueValidation) { + // ASN must fit in 4 bytes (RFC 6793). + EXPECT_THAT(run({"SPINE", "remote-asn", "4294967296"}), HasSubstr("Invalid")); + EXPECT_THAT( + run({"SPINE", "local-asn", "18446744073709551615"}), + HasSubstr("Invalid")); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, namedGroupsAreDistinct) { + run({"SPINE", "remote-asn", "65000"}); + run({"LEAF", "remote-asn", "65001"}); + ASSERT_EQ(groups().size(), 2); + // Re-referencing an existing group by name updates it, not appends. + run({"SPINE", "local-asn", "64512"}); + EXPECT_EQ(groups().size(), 2); +} + +// ============================================================================== +// Reject paths — the error is surfaced and nothing is persisted +// ============================================================================== + +TEST_F(CmdConfigBgpPeerGroupTestFixture, invalidValuesRejected) { + EXPECT_THAT(run({"SPINE", "rr-client", "maybe"}), HasSubstr("Invalid")); + EXPECT_THAT( + run({"SPINE", "timers", "hold-time", "-1"}), HasSubstr("non-negative")); + EXPECT_THAT( + run({"SPINE", "remote-asn", "not-a-number"}), HasSubstr("Invalid")); + EXPECT_THAT( + run({"SPINE", "max-route", "pre-filter", "-5"}), + HasSubstr("non-negative")); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, unsupportedAttributesRejected) { + // These attributes have no per-peer-group thrift field, so they are not in + // the dispatch table and are refused at parse time as unknown attributes. + EXPECT_THROW(run({"SPINE", "peer-port", "1179"}), std::invalid_argument); + EXPECT_THROW( + run({"SPINE", "afi", "ipv4-labeled-unicast", "true"}), + std::invalid_argument); + EXPECT_THROW( + run({"SPINE", "afi", "ipv6-labeled-unicast", "true"}), + std::invalid_argument); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/config/CmdDeleteBgpPeerGroupTest.cpp b/fboss/cli/fboss2/test/config/CmdDeleteBgpPeerGroupTest.cpp new file mode 100644 index 0000000000000..826e20c805127 --- /dev/null +++ b/fboss/cli/fboss2/test/config/CmdDeleteBgpPeerGroupTest.cpp @@ -0,0 +1,106 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include +#include +#include + +#include +#include +#include +#include + +#include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/test/config/CmdConfigTestBase.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +using namespace ::testing; + +namespace facebook::fboss { + +// Deleting only touches the BGP side of ConfigSession, which seeds from +// thrift schema defaults when neither a staged session nor a system +// bgpcpp.conf exists — so no seed agent config is needed (mirrors +// CmdConfigBgpPeerGroupTest). +class CmdDeleteBgpPeerGroupTestFixture : public CmdConfigTestBase { + public: + CmdDeleteBgpPeerGroupTestFixture() + : CmdConfigTestBase("bgp_peer_group_delete_test_%%%%-%%%%-%%%%", "") {} + + void SetUp() override { + CmdConfigTestBase::SetUp(); + setupTestableConfigSession(); + } + + std::string configure(const std::vector& tokens) { + CmdConfigProtocolBgpPeerGroup cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpPeerGroupConfig(tokens)); + } + + std::string del(const std::vector& tokens) { + CmdDeleteProtocolBgpPeerGroup cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpPeerGroupRef(tokens)); + } + + const std::vector& groups() { + return ConfigSession::getInstance().getBgpConfig().peer_groups().ensure(); + } + + bool sessionFileExists() { + return std::filesystem::exists( + ConfigSession::getInstance().getBgpSessionConfigPath()); + } +}; + +// ============================================================================== +// BgpPeerGroupRef (arg) validation +// ============================================================================== + +TEST_F(CmdDeleteBgpPeerGroupTestFixture, argValidation) { + EXPECT_EQ(BgpPeerGroupRef({"SPINE"}).groupName(), "SPINE"); + + // Invalid: empty, empty name, extra tokens. + EXPECT_THROW(BgpPeerGroupRef({}), std::invalid_argument); + EXPECT_THROW(BgpPeerGroupRef({""}), std::invalid_argument); + EXPECT_THROW(BgpPeerGroupRef({"SPINE", "LEAF"}), std::invalid_argument); +} + +// ============================================================================== +// queryClient +// ============================================================================== + +TEST_F(CmdDeleteBgpPeerGroupTestFixture, deleteExistingGroup) { + configure({"SPINE", "remote-asn", "65000"}); + configure({"LEAF", "remote-asn", "65001"}); + ASSERT_EQ(groups().size(), 2); + + auto result = del({"SPINE"}); + EXPECT_THAT(result, HasSubstr("Successfully deleted BGP peer-group SPINE")); + // The other group survives untouched. + ASSERT_EQ(groups().size(), 1); + EXPECT_EQ(*groups()[0].name(), "LEAF"); + EXPECT_EQ(groups()[0].remote_as_4_byte().value_or(0), 65001); + EXPECT_TRUE(sessionFileExists()); +} + +TEST_F(CmdDeleteBgpPeerGroupTestFixture, deleteUnknownGroupRejected) { + auto result = del({"NO-SUCH-GROUP"}); + EXPECT_THAT( + result, HasSubstr("Error: BGP peer-group NO-SUCH-GROUP not found")); + // Nothing was persisted for the failed delete. + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected delete"; +} + +TEST_F(CmdDeleteBgpPeerGroupTestFixture, deleteUnknownLeavesOthersIntact) { + configure({"SPINE", "remote-asn", "65000"}); + auto result = del({"LEAF"}); + EXPECT_THAT(result, HasSubstr("not found")); + ASSERT_EQ(groups().size(), 1); + EXPECT_EQ(*groups()[0].name(), "SPINE"); +} + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/integration_test/BUCK b/fboss/cli/fboss2/test/integration_test/BUCK index 9d37e37383c40..6986b1c6d4618 100644 --- a/fboss/cli/fboss2/test/integration_test/BUCK +++ b/fboss/cli/fboss2/test/integration_test/BUCK @@ -22,6 +22,7 @@ cpp_binary( "ConfigArpTest.cpp", "ConfigBgpGlobalTest.cpp", "ConfigBgpNeighborTest.cpp", + "ConfigBgpPeerGroupTest.cpp", "ConfigBgpSessionTest.cpp", "ConfigConcurrentSessionsTest.cpp", "ConfigCoppTest.cpp", diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp new file mode 100644 index 0000000000000..544a437efea22 --- /dev/null +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp @@ -0,0 +1,131 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +/** + * End-to-end tests for `fboss2-dev config protocol bgp peer-group + * [ ...]`. + * + * Scope: the per-peer-group attributes. Every test stages the change AND + * commits it, then asserts the value landed at the correct thrift field path + * inside the matching .peer_groups[] entry of bgpd's running config (via + * getRunningConfig RPC). Stage-only behavior (attribute parsing, validation, + * rejection) is covered by the unit tests; an integration test that never + * commits exercises no daemon. + * + * Requirements: + * - The fboss2-dev binary under test (config subcommand tree). + * - HOME is set (the session file lives under $HOME/.fboss2). + * - bgpd is installed/active (commit restarts it). + */ + +#include +#include +#include + +#include "fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h" +#include "folly/json/dynamic.h" +#include "gmock/gmock.h" + +using namespace facebook::fboss; +using ::testing::HasSubstr; +using ::testing::Not; + +namespace { +// Test-only peer-group names, unlikely to collide with a real group in the +// device's running BGP config. +const std::string kGroup = "FBOSS2-TEST-GROUP"; +} // namespace + +class ConfigBgpPeerGroupTest : public ConfigBgpTestBase { + protected: + // Stage `config protocol bgp peer-group ` WITHOUT clearing the + // staged session, so attributes can accumulate across invocations. Returns + // the staged session JSON. + folly::dynamic stagePeerGroup(const std::vector& tokens) { + std::vector args = {"config", "protocol", "bgp", "peer-group"}; + args.insert(args.end(), tokens.begin(), tokens.end()); + auto result = runCli(args); + EXPECT_EQ(result.exitCode, 0) + << "stdout=" << result.stdout << " stderr=" << result.stderr; + EXPECT_THAT(result.stdout, Not(HasSubstr("Error:"))); + return readBgpSessionConfig(); + } + + // The .peer_groups[] entry named `name`, or nullptr. + static const folly::dynamic* findGroup( + const folly::dynamic& config, + const std::string& name) { + if (config.count("peer_groups") == 0) { + return nullptr; + } + for (const auto& group : config["peer_groups"]) { + if (group.count("name") && group["name"].asString() == name) { + return &group; + } + } + return nullptr; + } +}; + +TEST_F(ConfigBgpPeerGroupTest, SetRemoteAsnAndCommit) { + discardSession(); + clearBgpSession(); + stagePeerGroup({kGroup, "remote-asn", "65000"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + // Two layers: the promoted file proves what the commit wrote; the daemon's + // getRunningConfig RPC proves bgpd parsed and adopted it after the restart. + auto config = readSystemBgpConfig(); + const auto* group = findGroup(config, kGroup); + ASSERT_NE(group, nullptr) << "committed config has no peer-group " << kGroup; + ASSERT_TRUE(group->count("remote_as_4_byte")); + EXPECT_EQ((*group)["remote_as_4_byte"].asInt(), 65000); + + auto running = readRunningBgpConfigViaRpc(); + const auto* runningGroup = findGroup(running, kGroup); + ASSERT_NE(runningGroup, nullptr) + << "bgpd's running config has no peer-group " << kGroup; + EXPECT_EQ((*runningGroup)["remote_as_4_byte"].asInt(), 65000); +} + +TEST_F(ConfigBgpPeerGroupTest, SetTimersHoldTimeAndCommit) { + discardSession(); + clearBgpSession(); + stagePeerGroup({kGroup, "timers", "hold-time", "90"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + // Verify nested timer fields through the daemon's own view of its config. + auto running = readRunningBgpConfigViaRpc(); + const auto* group = findGroup(running, kGroup); + ASSERT_NE(group, nullptr) + << "bgpd's running config has no peer-group " << kGroup; + ASSERT_TRUE(group->count("bgp_peer_timers")); + EXPECT_EQ((*group)["bgp_peer_timers"]["hold_time_seconds"].asInt(), 90); +} + +TEST_F(ConfigBgpPeerGroupTest, DeleteGroupAndCommit) { + // Land a peer-group in the system config, then delete it through a second + // commit and verify it is gone from bgpd's running config. + discardSession(); + clearBgpSession(); + stagePeerGroup({kGroup, "remote-asn", "65000"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + ASSERT_NE(findGroup(readRunningBgpConfigViaRpc(), kGroup), nullptr) + << "setup commit did not land the peer-group in bgpd's running config"; + + clearBgpSession(); + auto result = runCli({"delete", "protocol", "bgp", "peer-group", kGroup}); + EXPECT_THAT(result.stdout, HasSubstr("Successfully deleted")); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after delete commit; state=" + << bgpDaemonActiveState(); + EXPECT_EQ(findGroup(readRunningBgpConfigViaRpc(), kGroup), nullptr) + << "deleted peer-group still present in bgpd's running config"; +} From c49765e6108f7d2cf0f8033cfccbf0811671267c Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Thu, 20 Aug 2026 16:41:30 +0000 Subject: [PATCH 10/22] [Nexthop] Guard peer-group delete against neighbor references; close UT gaps delete protocol bgp peer-group now refuses to delete a group that a neighbor's peer_group_name still references, naming the referencing neighbors, instead of committing a dangling reference bgpd has no defined behavior for. New/extended unit tests close the coverage gaps found in review: - timers out-delay and withdraw-unprog-delay (previously untested; withdraw-unprog-delay is a parity attribute tested for neighbor) - valueValidation now asserts the half-created group is rolled back (state assertion, not just the error string) - wrongValueCountRejected: zero/too-many value token counts - overwriteSameAttributeLastWins: re-setting an attribute updates in place - namedGroupsAreDistinct now asserts field preservation across mutations and sibling isolation - delete: referenced-group rejected (all referencing neighbors named), unreferenced delete unaffected by other references, delete-then- recreate starts clean Co-Authored-By: Claude Fable 5 --- .../CmdDeleteProtocolBgpPeerGroup.cpp | 18 +++++++ .../test/config/CmdConfigBgpPeerGroupTest.cpp | 41 +++++++++++++- .../test/config/CmdDeleteBgpPeerGroupTest.cpp | 54 +++++++++++++++++++ 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.cpp b/fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.cpp index 7fbe569d326e1..b34cd75ece87c 100644 --- a/fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.cpp +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.cpp @@ -13,6 +13,7 @@ #include "fboss/cli/fboss2/CmdHandler.cpp" #include +#include #include #include #include @@ -61,6 +62,23 @@ CmdDeleteProtocolBgpPeerGroup::queryClient( if (it == groups.end()) { return fmt::format("Error: BGP peer-group {} not found", args.groupName()); } + // A neighbor's peer_group_name resolves against this group by name at + // daemon load; erasing the group while a neighbor still points at it would + // commit a dangling reference. Refuse and name the neighbors instead. + std::vector referencingPeers; + for (const auto& peer : *cfg.peers()) { + if (peer.peer_group_name().has_value() && + *peer.peer_group_name() == args.groupName()) { + referencingPeers.push_back(*peer.peer_addr()); + } + } + if (!referencingPeers.empty()) { + return fmt::format( + "Error: BGP peer-group {} is still referenced by neighbor(s): {}; " + "delete or re-point those neighbors first", + args.groupName(), + folly::join(", ", referencingPeers)); + } groups.erase(it); session.saveBgpConfig(); return fmt::format( diff --git a/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp b/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp index 1585e6e683dcb..b894f859b1a90 100644 --- a/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp +++ b/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp @@ -106,11 +106,15 @@ TEST_F(CmdConfigBgpPeerGroupTestFixture, setRemoteAsn) { TEST_F(CmdConfigBgpPeerGroupTestFixture, timersAccumulate) { run({"SPINE", "timers", "hold-time", "90"}); run({"SPINE", "timers", "keepalive", "30"}); + run({"SPINE", "timers", "out-delay", "5"}); + run({"SPINE", "timers", "withdraw-unprog-delay", "10"}); run({"SPINE", "graceful-restart", "restart-time", "120"}); ASSERT_EQ(groups().size(), 1); const auto& timers = *groups()[0].bgp_peer_timers(); EXPECT_EQ(*timers.hold_time_seconds(), 90); EXPECT_EQ(*timers.keep_alive_seconds(), 30); + EXPECT_EQ(*timers.out_delay_seconds(), 5); + EXPECT_EQ(*timers.withdraw_unprog_delay_seconds(), 10); EXPECT_EQ(timers.graceful_restart_seconds().value_or(0), 120); } @@ -200,17 +204,50 @@ TEST_F(CmdConfigBgpPeerGroupTestFixture, valueValidation) { EXPECT_THAT( run({"SPINE", "local-asn", "18446744073709551615"}), HasSubstr("Invalid")); + // The rejected value implicitly created SPINE; the rollback must remove it, + // not just skip persisting it. + EXPECT_TRUE(groups().empty()) + << "rejected first attribute must not leave a half-created group"; EXPECT_FALSE(sessionFileExists()) << "session file should not exist after rejected input"; } +TEST_F(CmdConfigBgpPeerGroupTestFixture, wrongValueCountRejected) { + // Known attribute, but no value: parses fine, handler rejects at runtime. + EXPECT_THAT(run({"SPINE", "remote-asn"}), HasSubstr("requires")); + EXPECT_THAT(run({"SPINE", "timers", "hold-time"}), HasSubstr("requires")); + EXPECT_THAT(run({"SPINE", "description"}), HasSubstr("requires")); + // Too many values for single-value attributes. + EXPECT_THAT( + run({"SPINE", "remote-asn", "65000", "65001"}), HasSubstr("requires")); + EXPECT_THAT( + run({"SPINE", "rr-client", "true", "false"}), HasSubstr("requires")); + EXPECT_TRUE(groups().empty()) + << "rejected first attribute must not leave a half-created group"; + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, overwriteSameAttributeLastWins) { + run({"SPINE", "remote-asn", "65000"}); + run({"SPINE", "remote-asn", "65001"}); + ASSERT_EQ(groups().size(), 1); + EXPECT_EQ(groups()[0].remote_as_4_byte().value_or(0), 65001); +} + TEST_F(CmdConfigBgpPeerGroupTestFixture, namedGroupsAreDistinct) { run({"SPINE", "remote-asn", "65000"}); run({"LEAF", "remote-asn", "65001"}); ASSERT_EQ(groups().size(), 2); - // Re-referencing an existing group by name updates it, not appends. + // Re-referencing an existing group by name updates it, not appends — + // and mutating one field preserves the group's other fields and leaves + // the sibling group untouched. run({"SPINE", "local-asn", "64512"}); - EXPECT_EQ(groups().size(), 2); + ASSERT_EQ(groups().size(), 2); + EXPECT_EQ(groups()[0].remote_as_4_byte().value_or(0), 65000); + EXPECT_EQ(groups()[0].local_as_4_byte().value_or(0), 64512); + EXPECT_EQ(groups()[1].remote_as_4_byte().value_or(0), 65001); + EXPECT_FALSE(groups()[1].local_as_4_byte().has_value()); } // ============================================================================== diff --git a/fboss/cli/fboss2/test/config/CmdDeleteBgpPeerGroupTest.cpp b/fboss/cli/fboss2/test/config/CmdDeleteBgpPeerGroupTest.cpp index 826e20c805127..5ef75558b0e4e 100644 --- a/fboss/cli/fboss2/test/config/CmdDeleteBgpPeerGroupTest.cpp +++ b/fboss/cli/fboss2/test/config/CmdDeleteBgpPeerGroupTest.cpp @@ -49,6 +49,20 @@ class CmdDeleteBgpPeerGroupTestFixture : public CmdConfigTestBase { return ConfigSession::getInstance().getBgpConfig().peer_groups().ensure(); } + // Reference a peer-group from a neighbor the way a committed config would, + // without pulling the neighbor command (and its whole dispatch table) into + // this test binary. + void addPeerReferencing( + const std::string& peerAddr, + const std::string& groupName) { + bgp::thrift::BgpPeer peer; + peer.local_addr() = "10.0.0.1"; + peer.peer_addr() = peerAddr; + peer.peer_group_name() = groupName; + ConfigSession::getInstance().getBgpConfig().peers()->push_back( + std::move(peer)); + } + bool sessionFileExists() { return std::filesystem::exists( ConfigSession::getInstance().getBgpSessionConfigPath()); @@ -103,4 +117,44 @@ TEST_F(CmdDeleteBgpPeerGroupTestFixture, deleteUnknownLeavesOthersIntact) { EXPECT_EQ(*groups()[0].name(), "SPINE"); } +TEST_F(CmdDeleteBgpPeerGroupTestFixture, deleteReferencedGroupRejected) { + configure({"SPINE", "remote-asn", "65000"}); + addPeerReferencing("10.0.0.2", "SPINE"); + addPeerReferencing("10.0.0.3", "SPINE"); + + auto result = del({"SPINE"}); + EXPECT_THAT(result, HasSubstr("still referenced")); + // The refusal names every referencing neighbor so the user can act on it. + EXPECT_THAT(result, HasSubstr("10.0.0.2")); + EXPECT_THAT(result, HasSubstr("10.0.0.3")); + ASSERT_EQ(groups().size(), 1); + EXPECT_EQ(*groups()[0].name(), "SPINE"); + EXPECT_EQ(groups()[0].remote_as_4_byte().value_or(0), 65000); +} + +TEST_F(CmdDeleteBgpPeerGroupTestFixture, deleteUnreferencedGroupSucceeds) { + configure({"SPINE", "remote-asn", "65000"}); + configure({"LEAF", "remote-asn", "65001"}); + // A neighbor referencing SPINE must not block deleting LEAF. + addPeerReferencing("10.0.0.2", "SPINE"); + + auto result = del({"LEAF"}); + EXPECT_THAT(result, HasSubstr("Successfully deleted BGP peer-group LEAF")); + ASSERT_EQ(groups().size(), 1); + EXPECT_EQ(*groups()[0].name(), "SPINE"); +} + +TEST_F(CmdDeleteBgpPeerGroupTestFixture, deleteThenRecreateStartsClean) { + configure({"SPINE", "remote-asn", "65000"}); + auto result = del({"SPINE"}); + EXPECT_THAT(result, HasSubstr("Successfully deleted")); + EXPECT_TRUE(groups().empty()); + + configure({"SPINE"}); + ASSERT_EQ(groups().size(), 1); + // The recreated group starts clean — no fields leak across the + // delete/recreate boundary. + EXPECT_FALSE(groups()[0].remote_as_4_byte().has_value()); +} + } // namespace facebook::fboss From b51caceb6d136b9f3e419cd4985c32fde3a231aa Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Fri, 21 Aug 2026 03:24:20 +0000 Subject: [PATCH 11/22] [Nexthop] Deflake BGP and VLAN-default CLI integration tests Two pre-existing CI failures surfaced on every CLI-touching PR: - ConfigBgp*Test: the SetUp gate sampled systemctl is-active once. On fboss-sim bgpd flaps activating->active->failed from boot, so one unlucky sample lands in the brief active window, runs the test, and the bgpd thrift RPC then times out after 90s (seen as SetGracefulRestartTime / SetCountConfedsInAsPathLenFalse failures; green runs are the ones where every sample landed on inactive and skipped). The gate now also requires bgpd to answer its getRunningConfig RPC within 10s, else the test skips like the rest of the flap cycle. - ConfigVlanDefaultTest.ChangeDefaultVlanWithPortInNonDefaultVlan vacated only the test port before changing the default VLAN, but the change-default guard refuses while ANY port still uses the old default VLAN without an interface. On baselines generated before the default VLAN, so the guard always fired. Vacate every eth port from the old default VLAN first and restore them after. Co-Authored-By: Claude Fable 5 --- .../test/integration_test/ConfigBgpTestBase.h | 12 ++++++ .../ConfigVlanDefaultTest.cpp | 40 ++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h b/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h index 83e3077963dc6..195372f1f0456 100644 --- a/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h @@ -48,6 +48,18 @@ class ConfigBgpTestBase : public Fboss2IntegrationTest { if (bgpDaemonActiveState() != "active") { GTEST_SKIP() << "skipping because bgp is not active"; } + // systemd "active" is a point sample: on an image where bgpd cannot hold + // its boot config it flaps activating -> active -> failed, and a single + // sample can land inside the brief active window (seen on fboss-sim, + // where the sibling tests' samples all landed on inactive and skipped). + // Require the daemon to actually answer its thrift RPC before running, + // and skip otherwise — the same skip the rest of the flap cycle produces. + try { + readRunningBgpConfigViaRpc(std::chrono::seconds(10)); + } catch (const std::exception& ex) { + GTEST_SKIP() << "skipping because bgpd is active but not answering its " + << "thrift RPC (flapping unit?): " << ex.what(); + } // Snapshot the config bgpd runs with; TearDown restores it, so committed // test objects never outlive the test regardless of where it failed. haveBgpSnapshot_ = folly::readFile(kBgpDaemonConfigPath, bgpSnapshot_); diff --git a/fboss/cli/fboss2/test/integration_test/ConfigVlanDefaultTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigVlanDefaultTest.cpp index 0b6578cc37436..861da08d48e73 100644 --- a/fboss/cli/fboss2/test/integration_test/ConfigVlanDefaultTest.cpp +++ b/fboss/cli/fboss2/test/integration_test/ConfigVlanDefaultTest.cpp @@ -307,6 +307,34 @@ TEST_F(ConfigVlanDefaultTest, ChangeDefaultVlanWithPortInNonDefaultVlan) { << "Failed to move " << ifName << " to side VLAN " << sideVlan; XLOG(INFO) << "[Step 1] Moved " << ifName << " to VLAN " << sideVlan; + // Vacate every other eth port still parked on the current default VLAN + // (baselines generated before the per-port ingress VLAN change park all + // ports there). The change-default guard refuses while ANY port uses the + // old default VLAN and it has no interface, not just the test port. + std::vector movedPorts; + for (const auto& port : initialConfig["sw"]["ports"]) { + auto name = port.getDefault("name", "").asString(); + if (name.rfind("eth", 0) != 0 || name == ifName) { + continue; + } + if (port.getDefault("ingressVlan", -1).asInt() != currentDefault) { + continue; + } + auto r = runCli( + {"config", + "interface", + name, + "switchport", + "access", + "vlan", + std::to_string(sideVlan)}); + if (r.exitCode == 0) { + movedPorts.push_back(name); + } + } + XLOG(INFO) << "[Step 1b] Vacated " << movedPorts.size() + << " other ports off default VLAN " << currentDefault; + // Set a new default VLAN — the command must succeed even when no ports // remain on the old default VLAN. The target is auto-created, so it must be // an ID the agent can back with a route table (T284228086). @@ -329,7 +357,7 @@ TEST_F(ConfigVlanDefaultTest, ChangeDefaultVlanWithPortInNonDefaultVlan) { waitForAgentReady(); EXPECT_EQ(getSwConfigField("defaultVlan"), newDefault); - // Restore: move the port back and reset defaultVlan. + // Restore: move the ports back and reset defaultVlan. XLOG(INFO) << "[Restore] Moving " << ifName << " back to VLAN " << originalPortVlan; runCli( @@ -340,6 +368,16 @@ TEST_F(ConfigVlanDefaultTest, ChangeDefaultVlanWithPortInNonDefaultVlan) { "access", "vlan", std::to_string(originalPortVlan)}); + for (const auto& name : movedPorts) { + runCli( + {"config", + "interface", + name, + "switchport", + "access", + "vlan", + std::to_string(currentDefault)}); + } auto restoreDefault = runCli({"config", "vlan", "default", std::to_string(currentDefault)}); From 2c9b1a186cd058f583dd991c08528acd2a678850 Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Fri, 21 Aug 2026 16:48:47 +0000 Subject: [PATCH 12/22] [Nexthop] Bump cmd_config_test to size=large MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suite is ~60 files of ConfigSession-backed tests, each doing several fsync-backed writeFileAtomic calls per case; with this PR's added peer-group tests the full run takes ~530s on a dev box and timed out at the CI job's default moderate test timeout (300s) — killed mid-fsync in ConfigSessionTestFixture.commandTrackingMultipleCommands, 50/51 remaining tests green. size=large raises the test timeout to 900s (same precedent as thrift_packet_stream/tests). Co-Authored-By: Claude Fable 5 --- fboss/cli/fboss2/test/config/BUCK | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fboss/cli/fboss2/test/config/BUCK b/fboss/cli/fboss2/test/config/BUCK index 56efb2732b425..0ae31a32db654 100644 --- a/fboss/cli/fboss2/test/config/BUCK +++ b/fboss/cli/fboss2/test/config/BUCK @@ -5,6 +5,10 @@ oncall("fboss_oss") cpp_unittest( name = "cmd_config_test", + # The suite is ~60 files of ConfigSession tests, each doing several + # fsync-backed writeFileAtomic calls; on CI disks the run can exceed the + # default "moderate" (300s) Bazel test timeout. + size = "large", srcs = [ "BgpConfigSessionTest.cpp", "CmdConfigAclRuleTest.cpp", From 4e1342b88f80477deacbe6040e0f604ce57ea50b Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Wed, 9 Sep 2026 11:36:35 +0000 Subject: [PATCH 13/22] [Nexthop] Seed inherited peer-group timers, expose passive, fix j2c grammar Signed-off-by: hillol-nexthop --- fboss/cli/fboss2/CmdListConfig.cpp | 2 +- .../CmdConfigProtocolBgpPeerGroup.cpp | 304 ++++++++++-------- .../test/config/CmdConfigBgpPeerGroupTest.cpp | 101 +++++- .../ConfigBgpPeerGroupTest.cpp | 23 ++ fboss/cli/fboss2/tools/bgp_json_to_cli.py | 147 ++------- .../tools/tests/test_bgp_json_to_cli.py | 245 ++++++++++---- 6 files changed, 508 insertions(+), 314 deletions(-) diff --git a/fboss/cli/fboss2/CmdListConfig.cpp b/fboss/cli/fboss2/CmdListConfig.cpp index 76ab199dad2da..1174f7bc7caaf 100644 --- a/fboss/cli/fboss2/CmdListConfig.cpp +++ b/fboss/cli/fboss2/CmdListConfig.cpp @@ -367,7 +367,7 @@ const CommandTree& kConfigCommandTree() { "description, peer-tag, ingress-policy, " "egress-policy, rr-client, confed-peer, " "redistribute-peer, enhanced-route-refresh, " - "connect-mode, next-hop-self, add-path send|receive, " + "passive, next-hop-self, add-path send|receive, " "afi disable-ipv4-afi|disable-ipv6-afi|" "ipv4-over-ipv6-nh, " "graceful-restart restart-time|stateful-ha, " diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp index a9eede5cdd694..4a6da72e65737 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp @@ -47,7 +47,7 @@ constexpr std::string_view kEgressPolicy = "egress-policy"; constexpr std::string_view kRrClient = "rr-client"; constexpr std::string_view kRedistributePeer = "redistribute-peer"; constexpr std::string_view kEnhancedRouteRefresh = "enhanced-route-refresh"; -constexpr std::string_view kConnectMode = "connect-mode"; +constexpr std::string_view kPassive = "passive"; constexpr std::string_view kAddPathSend = "add-path send"; constexpr std::string_view kAddPathReceive = "add-path receive"; constexpr std::string_view kAfiDisableIpv4Afi = "afi disable-ipv4-afi"; @@ -79,11 +79,14 @@ constexpr std::string_view kMaxRoutePreWarningOnly = constexpr std::string_view kMaxRoutePostWarningOnly = "max-route post-warning-only"; -// connect-mode values -constexpr std::string_view kConnectModePassive = "PASSIVE"; -constexpr std::string_view kConnectModeActive = "ACTIVE"; -constexpr std::string_view kConnectModeBoth = "BOTH"; +// Protocol ranges bgpd enforces (or the wire format silently truncates to): +// the OPEN hold time is 16 bits and bgpd refuses 1-2s; the graceful-restart +// restart time is the 12-bit field of RFC 4724. +constexpr int32_t kHoldTimeMinSeconds = 3; +constexpr int32_t kHoldTimeMaxSeconds = 65535; +constexpr int32_t kGracefulRestartMaxSeconds = 4095; +using BgpConfig = bgp::thrift::BgpConfig; using PeerGroup = bgp::thrift::PeerGroup; using bgpcli::asnAttr; using bgpcli::AttrHandler; @@ -95,6 +98,7 @@ using bgpcli::parseBool; using bgpcli::Result; using bgpcli::routeCountAttr; using bgpcli::secondsAttr; +using bgpcli::SecondsRange; using bgpcli::stringAttr; using bgpcli::Tokens; using facebook::neteng::fboss::bgp_attr::AddPath; @@ -145,6 +149,13 @@ void setEnhancedRouteRefresh(PeerGroup& g, bool v) { g.enhanced_route_refresh() = v; } +// bgpd reads is_passive=true as PASSIVE_ONLY (listen) and false as +// PASSIVE_ACTIVE (listen and connect); there is no active-only mode, so the +// CLI exposes the flag itself rather than inventing connect-mode names. +void setPassive(PeerGroup& g, bool v) { + g.is_passive() = v; +} + void setDisableIpv4Afi(PeerGroup& g, bool v) { g.disable_ipv4_afi() = v; } @@ -211,48 +222,56 @@ void setNextHopSelf(PeerGroup& g, bool v) { g.next_hop_self() = v; } -// ---- hand-written handlers ------------------------------------------------- -// Only for value shapes no factory covers, because the shape is unique to this -// attribute rather than reusable. Both mirror the neighbor dispatcher's -// versions, which write the identically-named BgpPeer fields. - -// connect-mode: the thrift model only has is_passive (listen vs actively -// connect), so this is not an enum mapping — BOTH has no representation and is -// rejected with an explanation rather than silently mapped. -Result connectMode(PeerGroup& group, const Tokens& values) { - if (values.size() != 1) { - return err( - fmt::format( - "Error: {} requires <{}|{}>", - kConnectMode, - kConnectModePassive, - kConnectModeActive)); - } - if (values[0] == kConnectModePassive) { - group.is_passive() = true; - } else if (values[0] == kConnectModeActive) { - group.is_passive() = false; - } else if (values[0] == kConnectModeBoth) { - return err( - fmt::format( - "Error: {} {} has no representation in bgp_config.thrift " - "(PeerGroup only models is_passive); use {} or {}", - kConnectMode, - kConnectModeBoth, - kConnectModePassive, - kConnectModeActive)); - } else { - return err( - fmt::format( - "Error: Invalid {} value '{}'; expected {} or {}", - kConnectMode, - values[0], - kConnectModePassive, - kConnectModeActive)); +// ---- inherited values ------------------------------------------------------ +// bgpd applies a peer group's bgp_peer_timers to its members as a WHOLE +// struct: once the group carries one, every member takes hold, keepalive and +// out-delay from it, and hold_time_seconds / keep_alive_seconds are +// non-optional i32 (default 0). Hold time 0 means no keepalives, so a timers +// struct created for one field would silently disable dead-peer detection for +// the whole group. Before the first timer attribute is set on a group, the +// struct is therefore seeded from what its members use today -- the global +// hold time, with the conventional hold/3 keepalive -- and the requested field +// is then changed on top of it. pre_filter / post_filter need no seeding: a +// group has nothing above it to inherit a RouteLimit from, and the setter's +// ensure() yields the thrift defaults, which is also what bgpd applies to a +// peer without one. + +void seedTimers(const BgpConfig& cfg, PeerGroup& group) { + if (group.bgp_peer_timers()) { + return; } - return ok(fmt::format("Successfully set {} to: {}", kConnectMode, values[0])); + bgp::thrift::BgpPeerTimers timers; + timers.hold_time_seconds() = *cfg.hold_time(); + timers.keep_alive_seconds() = *cfg.hold_time() / 3; + group.bgp_peer_timers() = std::move(timers); +} + +// A peer-group handler sees the whole config (for the global defaults) plus +// the group being edited. The shared factories only know the group, so they +// are adapted; the adapter for timer attributes seeds first. +using PeerGroupHandler = + std::function; + +PeerGroupHandler groupOnly(AttrHandler handler) { + return [handler = std::move(handler)]( + const BgpConfig&, PeerGroup& group, const Tokens& values) { + return handler(group, values); + }; +} + +PeerGroupHandler withTimers(AttrHandler handler) { + return [handler = std::move(handler)]( + const BgpConfig& cfg, PeerGroup& group, const Tokens& values) { + seedTimers(cfg, group); + return handler(group, values); + }; } +// ---- hand-written handlers ------------------------------------------------- +// Only for value shapes no factory covers, because the shape is unique to this +// attribute rather than reusable. Mirrors the neighbor dispatcher's version, +// which writes the identically-named BgpPeer field. + // add-path send/receive: a boolean at the CLI, but the two attributes share // one thrift field — add_path is a single enum whose values form a bitmask by // design (RECEIVE=1, SEND=2, BOTH=3). Each takes the boolAttr text but merges @@ -298,73 +317,102 @@ Result addPathReceive(PeerGroup& group, const Tokens& values) { // handler bodies here by design — if an attribute appears to need one, its // value shape is missing a factory and the fix is to add the factory. -const std::map, std::less<>>& -attrHandlers() { - static const std::map, std::less<>> - kHandlers = { +const std::map>& attrHandlers() { + auto plain = [](AttrHandler h) { return groupOnly(std::move(h)); }; + auto timers = [](AttrHandler h) { + return withTimers(std::move(h)); + }; + static const std::map> kHandlers = + { {std::string(kRemoteAsn), - asnAttr(kRemoteAsn, setRemoteAsn)}, - {std::string(kLocalAsn), asnAttr(kLocalAsn, setLocalAsn)}, + plain(asnAttr(kRemoteAsn, setRemoteAsn))}, + {std::string(kLocalAsn), + plain(asnAttr(kLocalAsn, setLocalAsn))}, {std::string(kDescription), - joinedStringAttr(kDescription, setDescription)}, + plain(joinedStringAttr(kDescription, setDescription))}, {std::string(kPeerTag), - stringAttr(kPeerTag, "string", setPeerTag)}, + plain(stringAttr(kPeerTag, "string", setPeerTag))}, {std::string(kIngressPolicy), - stringAttr( - kIngressPolicy, "policy-name", setIngressPolicy)}, + plain( + stringAttr( + kIngressPolicy, "policy-name", setIngressPolicy))}, {std::string(kEgressPolicy), - stringAttr( - kEgressPolicy, "policy-name", setEgressPolicy)}, - {std::string(kRrClient), boolAttr(kRrClient, setRrClient)}, + plain( + stringAttr( + kEgressPolicy, "policy-name", setEgressPolicy))}, + {std::string(kRrClient), + plain(boolAttr(kRrClient, setRrClient))}, {std::string(kConfedPeer), - boolAttr(kConfedPeer, setConfedPeer)}, + plain(boolAttr(kConfedPeer, setConfedPeer))}, {std::string(kRedistributePeer), - boolAttr(kRedistributePeer, setRedistributePeer)}, + plain(boolAttr(kRedistributePeer, setRedistributePeer))}, {std::string(kEnhancedRouteRefresh), - boolAttr(kEnhancedRouteRefresh, setEnhancedRouteRefresh)}, - {std::string(kConnectMode), connectMode}, - {std::string(kAddPathSend), addPathSend}, - {std::string(kAddPathReceive), addPathReceive}, + plain( + boolAttr( + kEnhancedRouteRefresh, setEnhancedRouteRefresh))}, + {std::string(kPassive), + plain(boolAttr(kPassive, setPassive))}, + {std::string(kAddPathSend), plain(addPathSend)}, + {std::string(kAddPathReceive), plain(addPathReceive)}, {std::string(kAfiDisableIpv4Afi), - boolAttr(kAfiDisableIpv4Afi, setDisableIpv4Afi)}, + plain(boolAttr(kAfiDisableIpv4Afi, setDisableIpv4Afi))}, {std::string(kAfiDisableIpv6Afi), - boolAttr(kAfiDisableIpv6Afi, setDisableIpv6Afi)}, + plain(boolAttr(kAfiDisableIpv6Afi, setDisableIpv6Afi))}, {std::string(kAfiIpv4OverIpv6Nh), - boolAttr(kAfiIpv4OverIpv6Nh, setIpv4OverIpv6Nh)}, + plain(boolAttr(kAfiIpv4OverIpv6Nh, setIpv4OverIpv6Nh))}, {std::string(kGracefulRestartTime), - secondsAttr( - kGracefulRestartTime, setGracefulRestartTime)}, + timers( + secondsAttr( + kGracefulRestartTime, + setGracefulRestartTime, + SecondsRange{0, kGracefulRestartMaxSeconds}))}, {std::string(kGracefulRestartStatefulHa), - boolAttr( - kGracefulRestartStatefulHa, setGracefulRestartStatefulHa)}, + plain( + boolAttr( + kGracefulRestartStatefulHa, setGracefulRestartStatefulHa))}, {std::string(kMaxRoutePreFilter), - routeCountAttr(kMaxRoutePreFilter, setMaxRoutePreFilter)}, + plain( + routeCountAttr( + kMaxRoutePreFilter, setMaxRoutePreFilter))}, {std::string(kMaxRoutePostFilter), - routeCountAttr( - kMaxRoutePostFilter, setMaxRoutePostFilter)}, + plain( + routeCountAttr( + kMaxRoutePostFilter, setMaxRoutePostFilter))}, {std::string(kMaxRoutePreWarningThreshold), - routeCountAttr( - kMaxRoutePreWarningThreshold, setMaxRoutePreWarningThreshold)}, + plain( + routeCountAttr( + kMaxRoutePreWarningThreshold, + setMaxRoutePreWarningThreshold))}, {std::string(kMaxRoutePostWarningThreshold), - routeCountAttr( - kMaxRoutePostWarningThreshold, setMaxRoutePostWarningThreshold)}, + plain( + routeCountAttr( + kMaxRoutePostWarningThreshold, + setMaxRoutePostWarningThreshold))}, {std::string(kMaxRoutePreWarningOnly), - boolAttr( - kMaxRoutePreWarningOnly, setMaxRoutePreWarningOnly)}, + plain( + boolAttr( + kMaxRoutePreWarningOnly, setMaxRoutePreWarningOnly))}, {std::string(kMaxRoutePostWarningOnly), - boolAttr( - kMaxRoutePostWarningOnly, setMaxRoutePostWarningOnly)}, + plain( + boolAttr( + kMaxRoutePostWarningOnly, setMaxRoutePostWarningOnly))}, {std::string(kTimersHoldTime), - secondsAttr(kTimersHoldTime, setTimersHoldTime)}, + timers( + secondsAttr( + kTimersHoldTime, + setTimersHoldTime, + SecondsRange{kHoldTimeMinSeconds, kHoldTimeMaxSeconds}))}, {std::string(kTimersKeepalive), - secondsAttr(kTimersKeepalive, setTimersKeepalive)}, + timers( + secondsAttr(kTimersKeepalive, setTimersKeepalive))}, {std::string(kTimersOutDelay), - secondsAttr(kTimersOutDelay, setTimersOutDelay)}, + timers(secondsAttr(kTimersOutDelay, setTimersOutDelay))}, {std::string(kTimersWithdrawUnprogDelay), - secondsAttr( - kTimersWithdrawUnprogDelay, setTimersWithdrawUnprogDelay)}, + timers( + secondsAttr( + kTimersWithdrawUnprogDelay, setTimersWithdrawUnprogDelay))}, {std::string(kNextHopSelf), - boolAttr(kNextHopSelf, setNextHopSelf)}, + plain(boolAttr(kNextHopSelf, setNextHopSelf))}, }; return kHandlers; } @@ -380,39 +428,20 @@ std::string validAttrList() { return out; } -// Find the peer-group keyed by name, creating it if absent. Setting an -// attribute on a not-yet-created group implicitly creates it, so command -// ordering stays forgiving; a bare `peer-group ` creates one explicitly. -// Unlike a BgpPeer, PeerGroup's only non-optional field is `name`, so no -// address seeding is needed. -bgp::thrift::PeerGroup& findOrCreatePeerGroup( - bgp::thrift::BgpConfig& cfg, +// The group named `groupName`, or end(). Setting an attribute on a +// not-yet-created group implicitly creates it, so command ordering stays +// forgiving; a bare `peer-group ` creates one explicitly. Unlike a +// BgpPeer, PeerGroup's only non-optional field is `name`, so a new group needs +// no address seeding. +std::vector::iterator findPeerGroup( + std::vector& groups, const std::string& groupName) { - auto& groups = cfg.peer_groups().ensure(); - for (auto& group : groups) { - if (*group.name() == groupName) { - return group; + for (auto it = groups.begin(); it != groups.end(); ++it) { + if (*it->name() == groupName) { + return it; } } - groups.emplace_back(); - auto& group = groups.back(); - group.name() = groupName; - return group; -} - -// Whether a peer-group with the given name already exists. -bool peerGroupExists( - const bgp::thrift::BgpConfig& cfg, - const std::string& groupName) { - if (!cfg.peer_groups().has_value()) { - return false; - } - for (const auto& group : *cfg.peer_groups()) { - if (*group.name() == groupName) { - return true; - } - } - return false; + return groups.end(); } } // namespace @@ -465,27 +494,38 @@ CmdConfigProtocolBgpPeerGroup::queryClient( const ObjectArgType& args) { auto& session = ConfigSession::getInstance(); auto& cfg = session.getBgpConfig(); - const bool created = !peerGroupExists(cfg, args.groupName()); - auto& group = findOrCreatePeerGroup(cfg, args.groupName()); - + auto& groups = cfg.peer_groups().ensure(); + auto existing = findPeerGroup(groups, args.groupName()); + + // Edit a copy: a rejected value (or a struct seeded for it) must leave the + // in-memory config exactly as it was, since later lookups in the same + // process see it whether or not it was saved. + PeerGroup group; + if (existing != groups.end()) { + group = *existing; + } else { + group.name() = args.groupName(); + } Result result = args.attr().empty() ? ok(fmt::format( "Successfully created BGP peer-group {}", args.groupName())) // The attribute is guaranteed valid: BgpPeerGroupConfig's constructor // rejects an unknown attribute before we get here. - : attrHandlers().find(args.attr())->second(group, args.values()); - if (result.ok) { - if (!args.attr().empty()) { - result.message += fmt::format(" for peer-group {}", args.groupName()); - } - session.saveBgpConfig(); - result.message += - fmt::format("\nConfig saved to: {}", session.getBgpSessionConfigPath()); - } else if (created) { - // A rejected value must not leave a half-created group in the in-memory - // config (visible to later lookups in the same process, e.g. tests). - cfg.peer_groups()->pop_back(); + : attrHandlers().find(args.attr())->second(cfg, group, args.values()); + if (!result.ok) { + return result.message; + } + if (existing != groups.end()) { + *existing = std::move(group); + } else { + groups.push_back(std::move(group)); + } + if (!args.attr().empty()) { + result.message += fmt::format(" for peer-group {}", args.groupName()); } + session.saveBgpConfig(); + result.message += + fmt::format("\nConfig saved to: {}", session.getBgpSessionConfigPath()); return result.message; } diff --git a/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp b/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp index b894f859b1a90..14ff26a2111ba 100644 --- a/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp +++ b/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp @@ -44,6 +44,10 @@ class CmdConfigBgpPeerGroupTestFixture : public CmdConfigTestBase { return ConfigSession::getInstance().getBgpConfig().peer_groups().ensure(); } + bgp::thrift::BgpConfig& config() { + return ConfigSession::getInstance().getBgpConfig(); + } + bool sessionFileExists() { return std::filesystem::exists( ConfigSession::getInstance().getBgpSessionConfigPath()); @@ -132,18 +136,20 @@ TEST_F(CmdConfigBgpPeerGroupTestFixture, addPathMerge) { EXPECT_FALSE(groups()[0].add_path().has_value()); } -TEST_F(CmdConfigBgpPeerGroupTestFixture, connectMode) { - run({"SPINE", "connect-mode", "PASSIVE"}); +TEST_F(CmdConfigBgpPeerGroupTestFixture, passive) { + // is_passive=true is PASSIVE_ONLY in bgpd, false is PASSIVE_ACTIVE (listen + // and connect); the CLI exposes the flag rather than connect-mode names. + run({"SPINE", "passive", "true"}); EXPECT_TRUE(groups()[0].is_passive().value_or(false)); - run({"SPINE", "connect-mode", "ACTIVE"}); + run({"SPINE", "passive", "false"}); EXPECT_FALSE(groups()[0].is_passive().value_or(true)); - // BOTH has no thrift representation and must be rejected, leaving the - // previous value intact and nothing new persisted. - auto result = run({"SPINE", "connect-mode", "BOTH"}); - EXPECT_THAT(result, HasSubstr("Error")); + auto result = run({"SPINE", "passive", "BOTH"}); + EXPECT_THAT(result, HasSubstr("Invalid")); EXPECT_FALSE(groups()[0].is_passive().value_or(true)); + EXPECT_THROW( + run({"SPINE", "connect-mode", "PASSIVE"}), std::invalid_argument); } TEST_F(CmdConfigBgpPeerGroupTestFixture, boolAndStringAttributes) { @@ -250,6 +256,87 @@ TEST_F(CmdConfigBgpPeerGroupTestFixture, namedGroupsAreDistinct) { EXPECT_FALSE(groups()[1].local_as_4_byte().has_value()); } +// ============================================================================== +// Inherited values: bgpd applies a group's timers to its members as a whole +// struct, so the first timer attribute must seed the rest from the global +// defaults instead of leaving them at the thrift default of 0. +// ============================================================================== + +TEST_F(CmdConfigBgpPeerGroupTestFixture, timersSeededFromGlobal) { + config().hold_time() = 180; + run({"SPINE", "graceful-restart", "restart-time", "60"}); + + ASSERT_EQ(groups().size(), 1); + const auto& timers = *groups()[0].bgp_peer_timers(); + EXPECT_EQ(*timers.hold_time_seconds(), 180); + EXPECT_EQ(*timers.keep_alive_seconds(), 60); + EXPECT_EQ(*timers.out_delay_seconds(), 0); + EXPECT_EQ(*timers.withdraw_unprog_delay_seconds(), 0); + EXPECT_EQ(timers.graceful_restart_seconds().value_or(0), 60); +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, timersNotReseededOncePresent) { + run({"SPINE", "timers", "hold-time", "90"}); + run({"SPINE", "timers", "keepalive", "30"}); + // Seeding happens once: a later global change does not leak into a group + // that already carries its own timers, and other fields are preserved. + config().hold_time() = 180; + run({"SPINE", "timers", "out-delay", "2"}); + + const auto& timers = *groups()[0].bgp_peer_timers(); + EXPECT_EQ(*timers.hold_time_seconds(), 90); + EXPECT_EQ(*timers.keep_alive_seconds(), 30); + EXPECT_EQ(*timers.out_delay_seconds(), 2); +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, routeLimitOtherFieldsKeepDefaults) { + // No level above a group to inherit a RouteLimit from: the untouched fields + // take the thrift defaults bgpd applies to a peer without one. + run({"SPINE", "max-route", "pre-warning-only", "true"}); + const auto& pre = *groups()[0].pre_filter(); + EXPECT_EQ(*pre.max_routes(), 12000); + EXPECT_EQ(*pre.warning_limit(), 0); + EXPECT_TRUE(*pre.warning_only()); + EXPECT_FALSE(groups()[0].post_filter().has_value()); +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, timerRanges) { + // Hold time: 0 or 3..65535 (bgpd refuses 1-2; the OPEN field is 16 bits). + EXPECT_THAT(run({"SPINE", "timers", "hold-time", "1"}), HasSubstr("Error")); + EXPECT_THAT(run({"SPINE", "timers", "hold-time", "2"}), HasSubstr("Error")); + EXPECT_THAT( + run({"SPINE", "timers", "hold-time", "65536"}), HasSubstr("Error")); + EXPECT_TRUE(groups().empty()) + << "rejected first attribute must not leave a half-created group"; + run({"SPINE", "timers", "hold-time", "0"}); + EXPECT_EQ(*groups()[0].bgp_peer_timers()->hold_time_seconds(), 0); + run({"SPINE", "timers", "hold-time", "3"}); + EXPECT_EQ(*groups()[0].bgp_peer_timers()->hold_time_seconds(), 3); + run({"SPINE", "timers", "hold-time", "65535"}); + EXPECT_EQ(*groups()[0].bgp_peer_timers()->hold_time_seconds(), 65535); + + // Graceful-restart time: 0..4095 (12-bit field, RFC 4724). + EXPECT_THAT( + run({"SPINE", "graceful-restart", "restart-time", "4096"}), + HasSubstr("Error")); + EXPECT_FALSE(groups()[0].bgp_peer_timers()->graceful_restart_seconds()); + run({"SPINE", "graceful-restart", "restart-time", "4095"}); + EXPECT_EQ( + groups()[0].bgp_peer_timers()->graceful_restart_seconds().value_or(0), + 4095); +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, rejectedValueLeavesGroupUntouched) { + run({"SPINE", "remote-asn", "65000"}); + // A rejected timer must not leave behind the timers struct that was seeded + // for it: that would silently pin the group's members to the current + // global hold time. + EXPECT_THAT(run({"SPINE", "timers", "hold-time", "2"}), HasSubstr("Error")); + ASSERT_EQ(groups().size(), 1); + EXPECT_FALSE(groups()[0].bgp_peer_timers().has_value()); + EXPECT_EQ(groups()[0].remote_as_4_byte().value_or(0), 65000); +} + // ============================================================================== // Reject paths — the error is surfaced and nothing is persisted // ============================================================================== diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp index 544a437efea22..92086d85316c7 100644 --- a/fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp @@ -104,6 +104,29 @@ TEST_F(ConfigBgpPeerGroupTest, SetTimersHoldTimeAndCommit) { << "bgpd's running config has no peer-group " << kGroup; ASSERT_TRUE(group->count("bgp_peer_timers")); EXPECT_EQ((*group)["bgp_peer_timers"]["hold_time_seconds"].asInt(), 90); + // bgpd applies the group's timers struct to its members as a whole, so the + // fields the user did not set must carry the global defaults (hold/3 + // keepalive), not the thrift default of 0 that would disable keepalives. + ASSERT_TRUE(running.count("hold_time")); + EXPECT_EQ( + (*group)["bgp_peer_timers"]["keep_alive_seconds"].asInt(), + running["hold_time"].asInt() / 3); +} + +TEST_F(ConfigBgpPeerGroupTest, SetPassiveAndCommit) { + discardSession(); + clearBgpSession(); + stagePeerGroup({kGroup, "passive", "true"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + auto running = readRunningBgpConfigViaRpc(); + const auto* group = findGroup(running, kGroup); + ASSERT_NE(group, nullptr) + << "bgpd's running config has no peer-group " << kGroup; + ASSERT_TRUE(group->count("is_passive")); + EXPECT_TRUE((*group)["is_passive"].asBool()); } TEST_F(ConfigBgpPeerGroupTest, DeleteGroupAndCommit) { diff --git a/fboss/cli/fboss2/tools/bgp_json_to_cli.py b/fboss/cli/fboss2/tools/bgp_json_to_cli.py index ac61ef33f2c3a..a5fcf94dc0ac4 100644 --- a/fboss/cli/fboss2/tools/bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/bgp_json_to_cli.py @@ -114,121 +114,6 @@ def generate_global_commands(config: dict[str, Any]) -> list[str]: return commands -def _generate_peer_group_basic_commands( - peer_group: dict[str, Any], escaped_name: str -) -> list[str]: - """Generate basic peer-group commands (remote-asn, description, policies).""" - commands = [] - if "remote_as_4_byte" in peer_group: - commands.append( - f"config protocol bgp peer-group {escaped_name} remote-asn {escape_shell_arg(peer_group['remote_as_4_byte'])}" - ) - if "description" in peer_group: - commands.append( - f"config protocol bgp peer-group {escaped_name} description {escape_shell_arg(peer_group['description'])}" - ) - if "ingress_policy_name" in peer_group: - commands.append( - f"config protocol bgp peer-group {escaped_name} ingress-policy {escape_shell_arg(peer_group['ingress_policy_name'])}" - ) - if "egress_policy_name" in peer_group: - commands.append( - f"config protocol bgp peer-group {escaped_name} egress-policy {escape_shell_arg(peer_group['egress_policy_name'])}" - ) - if "peer_tag" in peer_group: - commands.append( - f"config protocol bgp peer-group {escaped_name} peer-tag {escape_shell_arg(peer_group['peer_tag'])}" - ) - return commands - - -def _generate_peer_group_bool_commands( - peer_group: dict[str, Any], escaped_name: str -) -> list[str]: - """Generate boolean flag commands for peer-group.""" - commands = [] - bool_fields = [ - ("is_rr_client", "rr-client"), - ("next_hop_self", "next-hop-self"), - ("is_confed_peer", "confed-peer"), - ("v4_over_v6_nexthop", "v4-over-v6-nh"), - ("disable_ipv4_afi", "disable-ipv4-afi"), - ] - for field, cli_name in bool_fields: - if field in peer_group: - commands.append( - f"config protocol bgp peer-group {escaped_name} {cli_name} {_shell_bool(peer_group[field])}" - ) - return commands - - -def _generate_peer_group_timer_commands( - timers: dict[str, Any], escaped_name: str -) -> list[str]: - """Generate timer commands for peer-group.""" - commands = [] - if timers.get("hold_time_seconds"): - commands.append( - f"config protocol bgp peer-group {escaped_name} timers hold-time {escape_shell_arg(timers['hold_time_seconds'])}" - ) - if timers.get("keep_alive_seconds"): - commands.append( - f"config protocol bgp peer-group {escaped_name} timers keepalive {escape_shell_arg(timers['keep_alive_seconds'])}" - ) - if "out_delay_seconds" in timers: - commands.append( - f"config protocol bgp peer-group {escaped_name} timers out-delay {escape_shell_arg(timers['out_delay_seconds'])}" - ) - if "withdraw_unprog_delay_seconds" in timers: - commands.append( - f"config protocol bgp peer-group {escaped_name} timers withdraw-unprog-delay {escape_shell_arg(timers['withdraw_unprog_delay_seconds'])}" - ) - return commands - - -def _generate_peer_group_prefilter_commands( - pre_filter: dict[str, Any], escaped_name: str -) -> list[str]: - """Generate pre_filter commands for peer-group.""" - commands = [] - if pre_filter.get("max_routes"): - commands.append( - f"config protocol bgp peer-group {escaped_name} max-routes {escape_shell_arg(pre_filter['max_routes'])}" - ) - if "warning_limit" in pre_filter: - commands.append( - f"config protocol bgp peer-group {escaped_name} warning-limit {escape_shell_arg(pre_filter['warning_limit'])}" - ) - if "warning_only" in pre_filter: - commands.append( - f"config protocol bgp peer-group {escaped_name} warning-only {_shell_bool(pre_filter['warning_only'])}" - ) - return commands - - -def generate_peer_group_commands(peer_group: dict[str, Any]) -> list[str]: - """Generate CLI commands for a peer group.""" - name = peer_group.get("name", "") - if not name: - return [] - - escaped_name = escape_shell_arg(name) - commands = [] - commands.extend(_generate_peer_group_basic_commands(peer_group, escaped_name)) - commands.extend(_generate_peer_group_bool_commands(peer_group, escaped_name)) - commands.extend( - _generate_peer_group_timer_commands( - peer_group.get("bgp_peer_timers", {}), escaped_name - ) - ) - commands.extend( - _generate_peer_group_prefilter_commands( - peer_group.get("pre_filter", {}), escaped_name - ) - ) - return commands - - def format_bandwidth(bps: int) -> str: """Format bandwidth in bps with the largest exact K/M/G suffix. @@ -480,6 +365,38 @@ def generate_peer_commands(peer: dict[str, Any]) -> list[str]: return commands +# (json field, CLI attribute) — PeerGroup-only fields whose value maps 1:1 +# onto a peer-group attribute token. +_PEER_GROUP_SCALAR_FIELDS = [ + ("remote_as_4_byte", "remote-asn"), + ("local_as_4_byte", "local-asn"), + ("description", "description"), + ("peer_tag", "peer-tag"), + ("ingress_policy_name", "ingress-policy"), + ("egress_policy_name", "egress-policy"), +] + + +def generate_peer_group_commands(peer_group: dict[str, Any]) -> list[str]: + """Generate `config protocol bgp peer-group` CLI commands for a peer group. + + The peer-group and neighbor dispatchers share one attribute grammar for + the fields both thrift structs carry, so the per-shape generators are + shared; only the scalar field list differs. + """ + name = peer_group.get("name", "") + if not name: + return [] + + prefix = f"config protocol bgp peer-group {escape_shell_arg(name)}" + commands = [] + commands.extend( + _generate_scalar_commands(peer_group, prefix, _PEER_GROUP_SCALAR_FIELDS) + ) + commands.extend(_generate_session_commands(peer_group, prefix)) + return commands + + def generate_exec_commands(commands: list[str], binary: str = "fboss2") -> list[str]: """Generate executable commands with custom binary.""" exec_commands = [ diff --git a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py index ad85ded6c3a6f..4db9ae9d707e5 100644 --- a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py @@ -232,7 +232,9 @@ def test_empty_config(self) -> None: class GeneratePeerGroupCommandsTest(unittest.TestCase): - """Tests for generate_peer_group_commands function.""" + """Tests for generate_peer_group_commands (peer-group grammar).""" + + PREFIX = "config protocol bgp peer-group TEST" def test_empty_name_returns_empty(self) -> None: """Peer group without name should return empty list.""" @@ -248,42 +250,68 @@ def test_remote_asn(self) -> None: "config protocol bgp peer-group TEST-GROUP remote-asn 65000", commands ) + def test_local_asn(self) -> None: + """local_as_4_byte should generate local-asn command.""" + peer_group = {"name": "TEST", "local_as_4_byte": 64512} + commands = generate_peer_group_commands(peer_group) + self.assertIn(f"{self.PREFIX} local-asn 64512", commands) + def test_description_with_spaces(self) -> None: - """Description with spaces should be properly quoted.""" + """Description with spaces should be quoted.""" peer_group = {"name": "TEST", "description": "Test peer group"} commands = generate_peer_group_commands(peer_group) - self.assertIn( - "config protocol bgp peer-group TEST description 'Test peer group'", - commands, - ) + self.assertIn(f"{self.PREFIX} description 'Test peer group'", commands) def test_boolean_flag_true(self) -> None: - """Boolean flag with true value should generate 'true'.""" + """Boolean true should generate 'true'.""" peer_group = {"name": "TEST", "next_hop_self": True} commands = generate_peer_group_commands(peer_group) - self.assertIn( - "config protocol bgp peer-group TEST next-hop-self true", commands - ) + self.assertIn(f"{self.PREFIX} next-hop-self true", commands) def test_boolean_flag_false(self) -> None: - """Boolean flag with false value should generate 'false' (not omitted).""" + """Boolean false should generate 'false' (not be omitted).""" peer_group = {"name": "TEST", "disable_ipv4_afi": False} commands = generate_peer_group_commands(peer_group) - self.assertIn( - "config protocol bgp peer-group TEST disable-ipv4-afi false", commands - ) + self.assertIn(f"{self.PREFIX} afi disable-ipv4-afi false", commands) + + def test_afi_flags(self) -> None: + """AFI flags use the two-token `afi ...` attributes.""" + peer_group = { + "name": "TEST", + "disable_ipv4_afi": True, + "disable_ipv6_afi": False, + "v4_over_v6_nexthop": True, + } + commands = generate_peer_group_commands(peer_group) + self.assertIn(f"{self.PREFIX} afi disable-ipv4-afi true", commands) + self.assertIn(f"{self.PREFIX} afi disable-ipv6-afi false", commands) + self.assertIn(f"{self.PREFIX} afi ipv4-over-ipv6-nh true", commands) - def test_is_rr_client(self) -> None: - """is_rr_client should generate rr-client command.""" - peer_group = {"name": "TEST", "is_rr_client": True} + def test_session_flags(self) -> None: + """Session flags map onto their single-token boolean attributes.""" + peer_group = { + "name": "TEST", + "is_rr_client": True, + "is_confed_peer": True, + "is_redistribute_peer": False, + "enhanced_route_refresh": True, + "enable_stateful_ha": True, + } commands = generate_peer_group_commands(peer_group) - self.assertIn("config protocol bgp peer-group TEST rr-client true", commands) + self.assertIn(f"{self.PREFIX} rr-client true", commands) + self.assertIn(f"{self.PREFIX} confed-peer true", commands) + self.assertIn(f"{self.PREFIX} redistribute-peer false", commands) + self.assertIn(f"{self.PREFIX} enhanced-route-refresh true", commands) + self.assertIn(f"{self.PREFIX} graceful-restart stateful-ha true", commands) - def test_is_confed_peer(self) -> None: - """is_confed_peer should generate confed-peer command.""" - peer_group = {"name": "TEST", "is_confed_peer": True} + def test_is_passive(self) -> None: + """is_passive should generate the passive flag.""" + peer_group = {"name": "TEST", "is_passive": True} + commands = generate_peer_group_commands(peer_group) + self.assertIn(f"{self.PREFIX} passive true", commands) + peer_group["is_passive"] = False commands = generate_peer_group_commands(peer_group) - self.assertIn("config protocol bgp peer-group TEST confed-peer true", commands) + self.assertIn(f"{self.PREFIX} passive false", commands) def test_policies(self) -> None: """Policy names should generate correct commands.""" @@ -293,23 +321,22 @@ def test_policies(self) -> None: "egress_policy_name": "OUT_POLICY", } commands = generate_peer_group_commands(peer_group) - self.assertIn( - "config protocol bgp peer-group TEST ingress-policy IN_POLICY", commands - ) - self.assertIn( - "config protocol bgp peer-group TEST egress-policy OUT_POLICY", commands - ) + self.assertIn(f"{self.PREFIX} ingress-policy IN_POLICY", commands) + self.assertIn(f"{self.PREFIX} egress-policy OUT_POLICY", commands) - def test_v4_over_v6_nexthop(self) -> None: - """v4_over_v6_nexthop should generate v4-over-v6-nh command.""" - peer_group = {"name": "TEST", "v4_over_v6_nexthop": True} + def test_add_path(self) -> None: + """add_path BOTH should expand to both directions; RECEIVE to one.""" + peer_group = {"name": "TEST", "add_path": 3} commands = generate_peer_group_commands(peer_group) - self.assertIn( - "config protocol bgp peer-group TEST v4-over-v6-nh true", commands - ) + self.assertIn(f"{self.PREFIX} add-path receive true", commands) + self.assertIn(f"{self.PREFIX} add-path send true", commands) + peer_group["add_path"] = "RECEIVE" + commands = generate_peer_group_commands(peer_group) + self.assertIn(f"{self.PREFIX} add-path receive true", commands) + self.assertNotIn(f"{self.PREFIX} add-path send true", commands) def test_timers(self) -> None: - """Timer fields should generate correct commands.""" + """Timers, including zero values, should generate correct commands.""" peer_group = { "name": "TEST", "bgp_peer_timers": { @@ -317,44 +344,142 @@ def test_timers(self) -> None: "keep_alive_seconds": 10, "out_delay_seconds": 5, "withdraw_unprog_delay_seconds": 0, + "graceful_restart_seconds": 120, }, } commands = generate_peer_group_commands(peer_group) - self.assertIn( - "config protocol bgp peer-group TEST timers hold-time 30", commands - ) - self.assertIn( - "config protocol bgp peer-group TEST timers keepalive 10", commands - ) - self.assertIn( - "config protocol bgp peer-group TEST timers out-delay 5", commands - ) - self.assertIn( - "config protocol bgp peer-group TEST timers withdraw-unprog-delay 0", - commands, - ) + self.assertIn(f"{self.PREFIX} timers hold-time 30", commands) + self.assertIn(f"{self.PREFIX} timers keepalive 10", commands) + self.assertIn(f"{self.PREFIX} timers out-delay 5", commands) + self.assertIn(f"{self.PREFIX} timers withdraw-unprog-delay 0", commands) + self.assertIn(f"{self.PREFIX} graceful-restart restart-time 120", commands) + + def test_hold_time_zero_preserved(self) -> None: + """hold_time_seconds: 0 is a real value (no keepalives), not 'unset'.""" + peer_group = {"name": "TEST", "bgp_peer_timers": {"hold_time_seconds": 0}} + commands = generate_peer_group_commands(peer_group) + self.assertIn(f"{self.PREFIX} timers hold-time 0", commands) - def test_pre_filter_max_routes(self) -> None: - """pre_filter.max_routes should generate max-routes command.""" - peer_group = {"name": "TEST", "pre_filter": {"max_routes": 45000}} + def test_pre_filter(self) -> None: + """pre_filter maps onto the max-route pre-* attributes.""" + peer_group = { + "name": "TEST", + "pre_filter": { + "max_routes": 45000, + "warning_limit": 100, + "warning_only": True, + }, + } commands = generate_peer_group_commands(peer_group) - self.assertIn("config protocol bgp peer-group TEST max-routes 45000", commands) + self.assertIn(f"{self.PREFIX} max-route pre-filter 45000", commands) + self.assertIn(f"{self.PREFIX} max-route pre-warning-threshold 100", commands) + self.assertIn(f"{self.PREFIX} max-route pre-warning-only true", commands) - def test_pre_filter_warning_settings(self) -> None: - """pre_filter warning settings should generate correct commands.""" + def test_post_filter(self) -> None: + """post_filter maps onto the max-route post-* attributes.""" peer_group = { "name": "TEST", - "pre_filter": {"warning_limit": 100, "warning_only": True}, + "post_filter": { + "max_routes": 50000, + "warning_limit": 48000, + "warning_only": False, + }, } commands = generate_peer_group_commands(peer_group) - self.assertIn("config protocol bgp peer-group TEST warning-limit 100", commands) - self.assertIn("config protocol bgp peer-group TEST warning-only true", commands) + self.assertIn(f"{self.PREFIX} max-route post-filter 50000", commands) + self.assertIn(f"{self.PREFIX} max-route post-warning-threshold 48000", commands) + self.assertIn(f"{self.PREFIX} max-route post-warning-only false", commands) def test_peer_tag(self) -> None: """peer_tag should generate peer-tag command.""" peer_group = {"name": "TEST", "peer_tag": "FSW"} commands = generate_peer_group_commands(peer_group) - self.assertIn("config protocol bgp peer-group TEST peer-tag FSW", commands) + self.assertIn(f"{self.PREFIX} peer-tag FSW", commands) + + def test_complete_peer_group_loses_no_field(self) -> None: + """Every CLI-settable PeerGroup field produces exactly one command, in + the grammar the peer-group dispatcher accepts.""" + peer_group = { + "name": "TEST", + "remote_as_4_byte": 65000, + "local_as_4_byte": 64512, + "description": "spine group", + "peer_tag": "FSW", + "ingress_policy_name": "IN_POLICY", + "egress_policy_name": "OUT_POLICY", + "is_passive": True, + "is_rr_client": True, + "is_confed_peer": False, + "is_redistribute_peer": True, + "enhanced_route_refresh": True, + "disable_ipv4_afi": True, + "disable_ipv6_afi": False, + "v4_over_v6_nexthop": True, + "enable_stateful_ha": True, + "next_hop_self": True, + "add_path": 3, + "bgp_peer_timers": { + "hold_time_seconds": 90, + "keep_alive_seconds": 30, + "out_delay_seconds": 5, + "withdraw_unprog_delay_seconds": 10, + "graceful_restart_seconds": 120, + }, + "pre_filter": { + "max_routes": 45000, + "warning_limit": 40000, + "warning_only": True, + }, + "post_filter": { + "max_routes": 50000, + "warning_limit": 48000, + "warning_only": False, + }, + } + expected = [ + "remote-asn 65000", + "local-asn 64512", + "description 'spine group'", + "peer-tag FSW", + "ingress-policy IN_POLICY", + "egress-policy OUT_POLICY", + "passive true", + "rr-client true", + "confed-peer false", + "redistribute-peer true", + "enhanced-route-refresh true", + "afi disable-ipv4-afi true", + "afi disable-ipv6-afi false", + "afi ipv4-over-ipv6-nh true", + "graceful-restart stateful-ha true", + "next-hop-self true", + "add-path receive true", + "add-path send true", + "timers hold-time 90", + "timers keepalive 30", + "timers out-delay 5", + "timers withdraw-unprog-delay 10", + "graceful-restart restart-time 120", + "max-route pre-filter 45000", + "max-route pre-warning-threshold 40000", + "max-route pre-warning-only true", + "max-route post-filter 50000", + "max-route post-warning-threshold 48000", + "max-route post-warning-only false", + ] + commands = generate_peer_group_commands(peer_group) + self.assertEqual(commands, [f"{self.PREFIX} {e}" for e in expected]) + # One command per leaf field: nothing in the input was dropped. + leaf_fields = ( + len(peer_group) + - 1 # name is the key, not an attribute + - 3 # the three nested structs are counted by their leaves below + + 1 # add_path BOTH expands to two commands + + len(peer_group["bgp_peer_timers"]) + + len(peer_group["pre_filter"]) + + len(peer_group["post_filter"]) + ) + self.assertEqual(len(commands), leaf_fields) class GeneratePeerCommandsTest(unittest.TestCase): @@ -848,7 +973,9 @@ def test_full_rsw_config(self) -> None: # Verify peer group commands self.assertIn("peer-group RSW-FSW-V6 next-hop-self true", joined) self.assertIn("peer-group RSW-FSW-V6 confed-peer true", joined) - self.assertIn("peer-group RSW-RTSW-V6 disable-ipv4-afi true", joined) + self.assertIn("peer-group RSW-FSW-V6 afi ipv4-over-ipv6-nh true", joined) + self.assertIn("peer-group RSW-FSW-V6 timers hold-time 30", joined) + self.assertIn("peer-group RSW-RTSW-V6 afi disable-ipv4-afi true", joined) # Verify peer commands self.assertIn("neighbor 2401:db00:501c::/64 remote-asn 65000", joined) From f201dd50ac7c777540704ac4633a8ee70031a96c Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Thu, 10 Sep 2026 07:37:15 +0000 Subject: [PATCH 14/22] [Nexthop] Seed unlimited peer-group route limit, close the attribute gap, flag unconverted j2c fields Route limits: a group with no RouteLimit imposes none on its members (bgpd skips the check when the struct is absent), yet the first warning-only or threshold edit created the struct with the thrift default max_routes 12000, a hard cap with session teardown the operator never asked for. The struct is now seeded {max_routes 0, warning_only false, warning_limit 0} (0 = unlimited in bgpd) and only the requested field changes; a rejected value leaves no struct behind. Attributes: add every remaining PeerGroup field bgpd resolves peer > group: remove-private-as, enforce-first-as, route-refresh, ttl-security-hops (1..255), link-bandwidth, advertise-lbw, receive-lbw -- same spellings and validation as the neighbor command. Document the fields deliberately not exposed because bgpd never reads them from a group (local_addr, next_hop4/6, enabled, router_port_id, graceful_restart_end_of_rib_seconds). bgp_json_to_cli.py: the peer-group generator covers the new attributes via the shared tables, maps the deprecated i32 ASNs to the 4-byte attribute, and emits a `# WARNING` line (script + stderr) for any field with no CLI equivalent instead of dropping it. test_complete_peer_group_loses_no_field now asserts 36 commands. --- .../CmdConfigProtocolBgpPeerGroup.cpp | 133 ++++++++++++++++-- fboss/cli/fboss2/docs/BGP_CLI_TEST_PLAN.md | 10 +- .../test/config/CmdConfigBgpPeerGroupTest.cpp | 82 ++++++++++- .../ConfigBgpPeerGroupTest.cpp | 41 ++++++ fboss/cli/fboss2/tools/bgp_json_to_cli.py | 18 ++- .../tools/tests/test_bgp_json_to_cli.py | 74 ++++++++++ 6 files changed, 333 insertions(+), 25 deletions(-) diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp index 4a6da72e65737..6393f4f3a1e14 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp @@ -47,6 +47,13 @@ constexpr std::string_view kEgressPolicy = "egress-policy"; constexpr std::string_view kRrClient = "rr-client"; constexpr std::string_view kRedistributePeer = "redistribute-peer"; constexpr std::string_view kEnhancedRouteRefresh = "enhanced-route-refresh"; +constexpr std::string_view kRouteRefresh = "route-refresh"; +constexpr std::string_view kRemovePrivateAs = "remove-private-as"; +constexpr std::string_view kEnforceFirstAs = "enforce-first-as"; +constexpr std::string_view kTtlSecurityHops = "ttl-security-hops"; +constexpr std::string_view kLinkBandwidth = "link-bandwidth"; +constexpr std::string_view kAdvertiseLbw = "advertise-lbw"; +constexpr std::string_view kReceiveLbw = "receive-lbw"; constexpr std::string_view kPassive = "passive"; constexpr std::string_view kAddPathSend = "add-path send"; constexpr std::string_view kAddPathReceive = "add-path receive"; @@ -78,20 +85,30 @@ constexpr std::string_view kMaxRoutePreWarningOnly = "max-route pre-warning-only"; constexpr std::string_view kMaxRoutePostWarningOnly = "max-route post-warning-only"; +// PeerGroup fields deliberately NOT exposed, because bgpd never reads them +// from a group: local_addr, next_hop4, next_hop6 (per-peer only, the +// neighbor command owns them), `enabled` and `router_port_id` (read nowhere) +// and bgp_peer_timers.graceful_restart_end_of_rib_seconds (same). Staging +// them would persist config the daemon ignores. // Protocol ranges bgpd enforces (or the wire format silently truncates to): // the OPEN hold time is 16 bits and bgpd refuses 1-2s; the graceful-restart -// restart time is the 12-bit field of RFC 4724. +// restart time is the 12-bit field of RFC 4724; TTL security hops are +// bgpd's kMin/kMaxTtlSecurityHops (it throws on anything else at load). constexpr int32_t kHoldTimeMinSeconds = 3; constexpr int32_t kHoldTimeMaxSeconds = 65535; constexpr int32_t kGracefulRestartMaxSeconds = 4095; +constexpr int32_t kTtlSecurityHopsMin = 1; +constexpr int32_t kTtlSecurityHopsMax = 255; using BgpConfig = bgp::thrift::BgpConfig; using PeerGroup = bgp::thrift::PeerGroup; using bgpcli::asnAttr; using bgpcli::AttrHandler; +using bgpcli::bitRateAttr; using bgpcli::boolAttr; using bgpcli::err; +using bgpcli::intAttr; using bgpcli::joinedStringAttr; using bgpcli::ok; using bgpcli::parseBool; @@ -100,8 +117,11 @@ using bgpcli::routeCountAttr; using bgpcli::secondsAttr; using bgpcli::SecondsRange; using bgpcli::stringAttr; +using bgpcli::thriftEnumAttr; using bgpcli::Tokens; using facebook::neteng::fboss::bgp_attr::AddPath; +using facebook::neteng::fboss::bgp_attr::AdvertiseLinkBandwidth; +using facebook::neteng::fboss::bgp_attr::ReceiveLinkBandwidth; // ---- per-attribute setters ------------------------------------------------- // Pure thrift assignment: no validation, no messages. Parsing, bounds and the @@ -149,9 +169,39 @@ void setEnhancedRouteRefresh(PeerGroup& g, bool v) { g.enhanced_route_refresh() = v; } +void setRouteRefresh(PeerGroup& g, bool v) { + g.route_refresh() = v; +} + +void setRemovePrivateAs(PeerGroup& g, bool v) { + g.remove_private_as() = v; +} + +void setEnforceFirstAs(PeerGroup& g, bool v) { + g.enforce_first_as() = v; +} + +void setTtlSecurityHops(PeerGroup& g, int32_t v) { + g.ttl_security_hops() = v; +} + +void setLinkBandwidth(PeerGroup& g, const std::string& v) { + g.link_bandwidth_bps() = v; +} + +void setAdvertiseLbw(PeerGroup& g, AdvertiseLinkBandwidth v) { + g.advertise_link_bandwidth() = v; +} + +void setReceiveLbw(PeerGroup& g, ReceiveLinkBandwidth v) { + g.receive_link_bandwidth() = v; +} + // bgpd reads is_passive=true as PASSIVE_ONLY (listen) and false as -// PASSIVE_ACTIVE (listen and connect); there is no active-only mode, so the -// CLI exposes the flag itself rather than inventing connect-mode names. +// PASSIVE_ACTIVE (listen and connect). Its session code also knows an +// ACTIVE_ONLY mode, but the config model has no way to request it (the mode +// is derived from is_passive alone), so the CLI exposes the flag itself +// rather than inventing connect-mode names. void setPassive(PeerGroup& g, bool v) { g.is_passive() = v; } @@ -231,10 +281,15 @@ void setNextHopSelf(PeerGroup& g, bool v) { // the whole group. Before the first timer attribute is set on a group, the // struct is therefore seeded from what its members use today -- the global // hold time, with the conventional hold/3 keepalive -- and the requested field -// is then changed on top of it. pre_filter / post_filter need no seeding: a -// group has nothing above it to inherit a RouteLimit from, and the setter's -// ensure() yields the thrift defaults, which is also what bgpd applies to a -// peer without one. +// is then changed on top of it. +// +// pre_filter / post_filter are seeded too, though a group has nothing above +// it to inherit from: bgpd applies NO limit to a member without a RouteLimit +// (capRoutesPerPeer returns early) and reads max_routes == 0 as unlimited, +// whereas the thrift default the setter's ensure() would leave behind is a +// 12000-route hard cap with session teardown. A warning-only or threshold +// edit must not introduce a cap the operator never asked for, so the struct +// starts as {0, false, 0} and only the requested field changes. void seedTimers(const BgpConfig& cfg, PeerGroup& group) { if (group.bgp_peer_timers()) { @@ -246,9 +301,21 @@ void seedTimers(const BgpConfig& cfg, PeerGroup& group) { group.bgp_peer_timers() = std::move(timers); } +void seedRouteLimit(PeerGroup& group, bool pre) { + auto field = pre ? group.pre_filter() : group.post_filter(); + if (field) { + return; + } + bgp::thrift::RouteLimit unlimited; + unlimited.max_routes() = 0; + unlimited.warning_only() = false; + unlimited.warning_limit() = 0; + field = std::move(unlimited); +} + // A peer-group handler sees the whole config (for the global defaults) plus // the group being edited. The shared factories only know the group, so they -// are adapted; the adapter for timer attributes seeds first. +// are adapted; the adapters for struct-valued attributes seed first. using PeerGroupHandler = std::function; @@ -267,6 +334,14 @@ PeerGroupHandler withTimers(AttrHandler handler) { }; } +PeerGroupHandler withRouteLimit(bool pre, AttrHandler handler) { + return [pre, handler = std::move(handler)]( + const BgpConfig&, PeerGroup& group, const Tokens& values) { + seedRouteLimit(group, pre); + return handler(group, values); + }; +} + // ---- hand-written handlers ------------------------------------------------- // Only for value shapes no factory covers, because the shape is unique to this // attribute rather than reusable. Mirrors the neighbor dispatcher's version, @@ -322,6 +397,12 @@ const std::map>& attrHandlers() { auto timers = [](AttrHandler h) { return withTimers(std::move(h)); }; + auto preLimit = [](AttrHandler h) { + return withRouteLimit(/* pre */ true, std::move(h)); + }; + auto postLimit = [](AttrHandler h) { + return withRouteLimit(/* pre */ false, std::move(h)); + }; static const std::map> kHandlers = { {std::string(kRemoteAsn), @@ -350,6 +431,30 @@ const std::map>& attrHandlers() { plain( boolAttr( kEnhancedRouteRefresh, setEnhancedRouteRefresh))}, + {std::string(kRouteRefresh), + plain(boolAttr(kRouteRefresh, setRouteRefresh))}, + {std::string(kRemovePrivateAs), + plain(boolAttr(kRemovePrivateAs, setRemovePrivateAs))}, + {std::string(kEnforceFirstAs), + plain(boolAttr(kEnforceFirstAs, setEnforceFirstAs))}, + {std::string(kTtlSecurityHops), + plain( + intAttr( + kTtlSecurityHops, + "1-255", + kTtlSecurityHopsMin, + kTtlSecurityHopsMax, + setTtlSecurityHops))}, + {std::string(kLinkBandwidth), + plain(bitRateAttr(kLinkBandwidth, setLinkBandwidth))}, + {std::string(kAdvertiseLbw), + plain( + thriftEnumAttr( + kAdvertiseLbw, setAdvertiseLbw))}, + {std::string(kReceiveLbw), + plain( + thriftEnumAttr( + kReceiveLbw, setReceiveLbw))}, {std::string(kPassive), plain(boolAttr(kPassive, setPassive))}, {std::string(kAddPathSend), plain(addPathSend)}, @@ -371,29 +476,29 @@ const std::map>& attrHandlers() { boolAttr( kGracefulRestartStatefulHa, setGracefulRestartStatefulHa))}, {std::string(kMaxRoutePreFilter), - plain( + preLimit( routeCountAttr( kMaxRoutePreFilter, setMaxRoutePreFilter))}, {std::string(kMaxRoutePostFilter), - plain( + postLimit( routeCountAttr( kMaxRoutePostFilter, setMaxRoutePostFilter))}, {std::string(kMaxRoutePreWarningThreshold), - plain( + preLimit( routeCountAttr( kMaxRoutePreWarningThreshold, setMaxRoutePreWarningThreshold))}, {std::string(kMaxRoutePostWarningThreshold), - plain( + postLimit( routeCountAttr( kMaxRoutePostWarningThreshold, setMaxRoutePostWarningThreshold))}, {std::string(kMaxRoutePreWarningOnly), - plain( + preLimit( boolAttr( kMaxRoutePreWarningOnly, setMaxRoutePreWarningOnly))}, {std::string(kMaxRoutePostWarningOnly), - plain( + postLimit( boolAttr( kMaxRoutePostWarningOnly, setMaxRoutePostWarningOnly))}, {std::string(kTimersHoldTime), diff --git a/fboss/cli/fboss2/docs/BGP_CLI_TEST_PLAN.md b/fboss/cli/fboss2/docs/BGP_CLI_TEST_PLAN.md index ebb8ad6486105..3b995ef3cb4e1 100644 --- a/fboss/cli/fboss2/docs/BGP_CLI_TEST_PLAN.md +++ b/fboss/cli/fboss2/docs/BGP_CLI_TEST_PLAN.md @@ -73,10 +73,10 @@ fboss2 config protocol bgp peer-group RSW-FSW-V6 next-hop-self true fboss2 config protocol bgp peer-group RSW-FSW-V6 confed-peer true fboss2 config protocol bgp peer-group RSW-FSW-V6 ingress-policy PROPAGATE_RSW_FSW_IN fboss2 config protocol bgp peer-group RSW-FSW-V6 egress-policy PROPAGATE_RSW_FSW_OUT -fboss2 config protocol bgp peer-group RSW-FSW-V6 v4-over-v6-nh true +fboss2 config protocol bgp peer-group RSW-FSW-V6 afi ipv4-over-ipv6-nh true fboss2 config protocol bgp peer-group RSW-FSW-V6 timers hold-time 30 fboss2 config protocol bgp peer-group RSW-FSW-V6 timers keepalive 10 -fboss2 config protocol bgp peer-group RSW-RTSW-V6 disable-ipv4-afi true +fboss2 config protocol bgp peer-group RSW-RTSW-V6 afi disable-ipv4-afi true # Peer configuration (with explicit disable_ipv4_afi: false preserved) fboss2 config protocol bgp neighbor 2401:db00:501c::/64 remote-asn 65000 @@ -131,11 +131,11 @@ diff <(jq -S . /tmp/rsw_bgp_nao.txt) <(jq -S . ~/.fboss2/bgp_config.json) | Peer Group | `config protocol bgp peer-group remote-asn ` | Set remote AS | | Peer Group | `config protocol bgp peer-group description ` | Set description | | Peer Group | `config protocol bgp peer-group next-hop-self ` | Enable/disable next-hop-self | -| Peer Group | `config protocol bgp peer-group disable-ipv4-afi ` | Disable IPv4 AFI | -| Peer Group | `config protocol bgp peer-group v4-over-v6-nh ` | Enable v4-over-v6 nexthop | +| Peer Group | `config protocol bgp peer-group afi disable-ipv4-afi ` | Disable IPv4 AFI | +| Peer Group | `config protocol bgp peer-group afi ipv4-over-ipv6-nh ` | Enable v4-over-v6 nexthop | | Peer Group | `config protocol bgp peer-group timers hold-time ` | Set hold time | | Peer Group | `config protocol bgp peer-group timers keepalive ` | Set keepalive interval | -| Peer Group | `config protocol bgp peer-group max-routes ` | Set max routes | +| Peer Group | `config protocol bgp peer-group max-route pre-filter ` | Set max routes (pre-policy; 0 = unlimited) | | Neighbor | `config protocol bgp neighbor remote-asn ` | Set remote AS | | Neighbor | `config protocol bgp neighbor peer-group ` | Assign to peer group | | Neighbor | `config protocol bgp neighbor bind-addr address ` | Set local address | diff --git a/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp b/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp index 14ff26a2111ba..a80c525bb273e 100644 --- a/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp +++ b/fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp @@ -157,6 +157,9 @@ TEST_F(CmdConfigBgpPeerGroupTestFixture, boolAndStringAttributes) { run({"SPINE", "confed-peer", "false"}); run({"SPINE", "redistribute-peer", "true"}); run({"SPINE", "enhanced-route-refresh", "true"}); + run({"SPINE", "route-refresh", "true"}); + run({"SPINE", "remove-private-as", "true"}); + run({"SPINE", "enforce-first-as", "false"}); run({"SPINE", "afi", "disable-ipv4-afi", "true"}); run({"SPINE", "afi", "disable-ipv6-afi", "false"}); run({"SPINE", "afi", "ipv4-over-ipv6-nh", "true"}); @@ -174,6 +177,9 @@ TEST_F(CmdConfigBgpPeerGroupTestFixture, boolAndStringAttributes) { EXPECT_FALSE(group.is_confed_peer().value_or(true)); EXPECT_TRUE(group.is_redistribute_peer().value_or(false)); EXPECT_TRUE(group.enhanced_route_refresh().value_or(false)); + EXPECT_TRUE(group.route_refresh().value_or(false)); + EXPECT_TRUE(group.remove_private_as().value_or(false)); + EXPECT_FALSE(group.enforce_first_as().value_or(true)); EXPECT_TRUE(group.disable_ipv4_afi().value_or(false)); EXPECT_FALSE(group.disable_ipv6_afi().value_or(true)); EXPECT_TRUE(group.v4_over_v6_nexthop().value_or(false)); @@ -204,6 +210,52 @@ TEST_F(CmdConfigBgpPeerGroupTestFixture, routeLimits) { EXPECT_FALSE(*group.post_filter()->warning_only()); } +TEST_F(CmdConfigBgpPeerGroupTestFixture, linkBandwidthAttributes) { + // Same spellings and validation as the neighbor command: bgpd resolves the + // link-bandwidth trio peer > group. + run({"SPINE", "link-bandwidth", "10G"}); + EXPECT_EQ(groups()[0].link_bandwidth_bps().value_or(""), "10G"); + run({"SPINE", "link-bandwidth", "1.5G"}); + EXPECT_EQ(groups()[0].link_bandwidth_bps().value_or(""), "1.5G"); + run({"SPINE", "link-bandwidth", "auto"}); + EXPECT_EQ(groups()[0].link_bandwidth_bps().value_or(""), "auto"); + EXPECT_THAT(run({"SPINE", "link-bandwidth", "10Q"}), HasSubstr("Invalid")); + EXPECT_THAT(run({"SPINE", "link-bandwidth", "10T"}), HasSubstr("Invalid")); + EXPECT_EQ(groups()[0].link_bandwidth_bps().value_or(""), "auto"); + + run({"SPINE", "advertise-lbw", "1"}); + run({"SPINE", "receive-lbw", "ACCEPT"}); // enum value name accepted + const auto& group = groups()[0]; + EXPECT_EQ( + static_cast(group.advertise_link_bandwidth().value_or( + facebook::neteng::fboss::bgp_attr::AdvertiseLinkBandwidth::DISABLE)), + 1); + EXPECT_EQ( + static_cast(group.receive_link_bandwidth().value_or( + facebook::neteng::fboss::bgp_attr::ReceiveLinkBandwidth::DISABLE)), + 1); + EXPECT_THAT( + run({"SPINE", "advertise-lbw", "99"}), HasSubstr("not a valid mode")); + EXPECT_THAT( + run({"SPINE", "receive-lbw", "SOMETIMES"}), + HasSubstr("not a valid mode")); +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, ttlSecurityHopsRange) { + // bgpd throws at config load outside kMin/kMaxTtlSecurityHops (1..255). + EXPECT_THAT(run({"SPINE", "ttl-security-hops", "0"}), HasSubstr("Error")); + EXPECT_THAT(run({"SPINE", "ttl-security-hops", "256"}), HasSubstr("Error")); + EXPECT_THAT(run({"SPINE", "ttl-security-hops", "x"}), HasSubstr("Error")); + EXPECT_TRUE(groups().empty()) + << "rejected first attribute must not leave a half-created group"; + run({"SPINE", "ttl-security-hops", "1"}); + EXPECT_EQ(groups()[0].ttl_security_hops().value_or(0), 1); + run({"SPINE", "ttl-security-hops", "255"}); + EXPECT_EQ(groups()[0].ttl_security_hops().value_or(0), 255); + EXPECT_THAT(run({"SPINE", "ttl-security-hops", "256"}), HasSubstr("Error")); + EXPECT_EQ(groups()[0].ttl_security_hops().value_or(0), 255); +} + TEST_F(CmdConfigBgpPeerGroupTestFixture, valueValidation) { // ASN must fit in 4 bytes (RFC 6793). EXPECT_THAT(run({"SPINE", "remote-asn", "4294967296"}), HasSubstr("Invalid")); @@ -289,15 +341,30 @@ TEST_F(CmdConfigBgpPeerGroupTestFixture, timersNotReseededOncePresent) { EXPECT_EQ(*timers.out_delay_seconds(), 2); } -TEST_F(CmdConfigBgpPeerGroupTestFixture, routeLimitOtherFieldsKeepDefaults) { - // No level above a group to inherit a RouteLimit from: the untouched fields - // take the thrift defaults bgpd applies to a peer without one. +TEST_F(CmdConfigBgpPeerGroupTestFixture, routeLimitSeededUnlimited) { + // A group with no RouteLimit imposes none on its members (bgpd skips the + // check when the struct is absent), so the struct created for a + // warning-only/threshold edit must keep that: max_routes 0 = unlimited, + // not the 12000 thrift default, which would be a hard cap with teardown. run({"SPINE", "max-route", "pre-warning-only", "true"}); const auto& pre = *groups()[0].pre_filter(); - EXPECT_EQ(*pre.max_routes(), 12000); + EXPECT_EQ(*pre.max_routes(), 0); EXPECT_EQ(*pre.warning_limit(), 0); EXPECT_TRUE(*pre.warning_only()); EXPECT_FALSE(groups()[0].post_filter().has_value()); + + run({"SPINE", "max-route", "post-warning-threshold", "30000"}); + const auto& post = *groups()[0].post_filter(); + EXPECT_EQ(*post.max_routes(), 0); + EXPECT_EQ(*post.warning_limit(), 30000); + EXPECT_FALSE(*post.warning_only()); +} + +TEST_F(CmdConfigBgpPeerGroupTestFixture, routeLimitNotReseededOncePresent) { + run({"SPINE", "max-route", "pre-filter", "45000"}); + run({"SPINE", "max-route", "pre-warning-threshold", "30000"}); + EXPECT_EQ(*groups()[0].pre_filter()->max_routes(), 45000); + EXPECT_EQ(*groups()[0].pre_filter()->warning_limit(), 30000); } TEST_F(CmdConfigBgpPeerGroupTestFixture, timerRanges) { @@ -335,6 +402,13 @@ TEST_F(CmdConfigBgpPeerGroupTestFixture, rejectedValueLeavesGroupUntouched) { ASSERT_EQ(groups().size(), 1); EXPECT_FALSE(groups()[0].bgp_peer_timers().has_value()); EXPECT_EQ(groups()[0].remote_as_4_byte().value_or(0), 65000); + // Likewise a rejected route-limit value must not leave a seeded RouteLimit. + EXPECT_THAT( + run({"SPINE", "max-route", "pre-warning-threshold", "-1"}), + HasSubstr("Error")); + EXPECT_FALSE(groups()[0].pre_filter().has_value()); + EXPECT_THAT(run({"SPINE", "ttl-security-hops", "0"}), HasSubstr("Error")); + EXPECT_FALSE(groups()[0].ttl_security_hops().has_value()); } // ============================================================================== diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp index 92086d85316c7..f1ede5067661b 100644 --- a/fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp @@ -129,6 +129,47 @@ TEST_F(ConfigBgpPeerGroupTest, SetPassiveAndCommit) { EXPECT_TRUE((*group)["is_passive"].asBool()); } +TEST_F(ConfigBgpPeerGroupTest, SessionKnobsAndCommit) { + // The per-session knobs bgpd resolves peer > group (remove-private-as, + // ttl-security-hops) reach the daemon's running config via the same path. + discardSession(); + clearBgpSession(); + stagePeerGroup({kGroup, "remove-private-as", "true"}); + stagePeerGroup({kGroup, "ttl-security-hops", "2"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + auto running = readRunningBgpConfigViaRpc(); + const auto* group = findGroup(running, kGroup); + ASSERT_NE(group, nullptr) + << "bgpd's running config has no peer-group " << kGroup; + ASSERT_TRUE(group->count("remove_private_as")); + EXPECT_TRUE((*group)["remove_private_as"].asBool()); + ASSERT_TRUE(group->count("ttl_security_hops")); + EXPECT_EQ((*group)["ttl_security_hops"].asInt(), 2); +} + +TEST_F(ConfigBgpPeerGroupTest, RouteLimitWarningKeepsUnlimitedCap) { + // A warning threshold on a group with no route limit must not introduce the + // 12000-route thrift default: bgpd reads max_routes 0 as unlimited, which + // is what the group's members had before the edit. + discardSession(); + clearBgpSession(); + stagePeerGroup({kGroup, "max-route", "pre-warning-threshold", "30000"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + auto running = readRunningBgpConfigViaRpc(); + const auto* group = findGroup(running, kGroup); + ASSERT_NE(group, nullptr) + << "bgpd's running config has no peer-group " << kGroup; + ASSERT_TRUE(group->count("pre_filter")); + EXPECT_EQ((*group)["pre_filter"]["max_routes"].asInt(), 0); + EXPECT_EQ((*group)["pre_filter"]["warning_limit"].asInt(), 30000); +} + TEST_F(ConfigBgpPeerGroupTest, DeleteGroupAndCommit) { // Land a peer-group in the system config, then delete it through a second // commit and verify it is gone from bgpd's running config. diff --git a/fboss/cli/fboss2/tools/bgp_json_to_cli.py b/fboss/cli/fboss2/tools/bgp_json_to_cli.py index a5fcf94dc0ac4..82862060984e3 100644 --- a/fboss/cli/fboss2/tools/bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/bgp_json_to_cli.py @@ -366,16 +366,28 @@ def generate_peer_commands(peer: dict[str, Any]) -> list[str]: # (json field, CLI attribute) — PeerGroup-only fields whose value maps 1:1 -# onto a peer-group attribute token. +# onto a peer-group attribute token. Deprecated i32 ASNs map to the 4-byte +# attribute, as for neighbors. PeerGroup also carries local_addr, next_hop4, +# next_hop6, enabled and router_port_id, but bgpd reads none of them from a +# group (the first three per peer only, the last two nowhere), so the +# dispatcher has no attribute for them and they are flagged instead. _PEER_GROUP_SCALAR_FIELDS = [ ("remote_as_4_byte", "remote-asn"), + ("remote_as", "remote-asn"), ("local_as_4_byte", "local-asn"), + ("local_as", "local-asn"), ("description", "description"), ("peer_tag", "peer-tag"), ("ingress_policy_name", "ingress-policy"), ("egress_policy_name", "egress-policy"), ] +_PEER_GROUP_HANDLED_FIELDS = ( + {"name"} + | {field for field, _ in _PEER_GROUP_SCALAR_FIELDS} + | _SHARED_HANDLED_FIELDS +) + def generate_peer_group_commands(peer_group: dict[str, Any]) -> list[str]: """Generate `config protocol bgp peer-group` CLI commands for a peer group. @@ -389,7 +401,9 @@ def generate_peer_group_commands(peer_group: dict[str, Any]) -> list[str]: return [] prefix = f"config protocol bgp peer-group {escape_shell_arg(name)}" - commands = [] + commands = _unconverted_field_warnings( + peer_group, _PEER_GROUP_HANDLED_FIELDS, f"peer-group {_printable(name)}" + ) commands.extend( _generate_scalar_commands(peer_group, prefix, _PEER_GROUP_SCALAR_FIELDS) ) diff --git a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py index 4db9ae9d707e5..261a4a079560b 100644 --- a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py @@ -412,11 +412,18 @@ def test_complete_peer_group_loses_no_field(self) -> None: "is_confed_peer": False, "is_redistribute_peer": True, "enhanced_route_refresh": True, + "route_refresh": False, + "remove_private_as": True, + "enforce_first_as": True, "disable_ipv4_afi": True, "disable_ipv6_afi": False, "v4_over_v6_nexthop": True, "enable_stateful_ha": True, "next_hop_self": True, + "ttl_security_hops": 2, + "link_bandwidth_bps": "10G", + "advertise_link_bandwidth": 1, + "receive_link_bandwidth": "ACCEPT", "add_path": 3, "bgp_peer_timers": { "hold_time_seconds": 90, @@ -448,11 +455,18 @@ def test_complete_peer_group_loses_no_field(self) -> None: "confed-peer false", "redistribute-peer true", "enhanced-route-refresh true", + "route-refresh false", + "remove-private-as true", + "enforce-first-as true", "afi disable-ipv4-afi true", "afi disable-ipv6-afi false", "afi ipv4-over-ipv6-nh true", "graceful-restart stateful-ha true", "next-hop-self true", + "ttl-security-hops 2", + "link-bandwidth 10G", + "advertise-lbw 1", + "receive-lbw ACCEPT", "add-path receive true", "add-path send true", "timers hold-time 90", @@ -480,6 +494,66 @@ def test_complete_peer_group_loses_no_field(self) -> None: + len(peer_group["post_filter"]) ) self.assertEqual(len(commands), leaf_fields) + self.assertEqual(len(commands), 36) + + def test_link_bandwidth(self) -> None: + """The link-bandwidth trio bgpd resolves peer > group is generated.""" + peer_group = { + "name": "TEST", + "link_bandwidth_bps": 10_000_000_000, + "advertise_link_bandwidth": "ADVERTISE", + "receive_link_bandwidth": 1, + } + commands = generate_peer_group_commands(peer_group) + self.assertIn(f"{self.PREFIX} link-bandwidth 10G", commands) + self.assertIn(f"{self.PREFIX} advertise-lbw ADVERTISE", commands) + self.assertIn(f"{self.PREFIX} receive-lbw 1", commands) + + def test_deprecated_asn_fields_map_to_4_byte_attribute(self) -> None: + """The deprecated i32 ASNs are carried over, not dropped.""" + peer_group = {"name": "TEST", "remote_as": 65000, "local_as": 64512} + commands = generate_peer_group_commands(peer_group) + self.assertIn(f"{self.PREFIX} remote-asn 65000", commands) + self.assertIn(f"{self.PREFIX} local-asn 64512", commands) + self.assertFalse(any(c.startswith("# WARNING") for c in commands)) + + def test_unrepresentable_peer_group_fields_warn(self) -> None: + """PeerGroup fields bgpd never reads from a group are reported, not + silently dropped, and produce no config command.""" + peer_group = { + "name": "TEST", + "remote_as_4_byte": 65000, + "local_addr": "2001:db8::2", + "next_hop4": "10.1.1.1", + "next_hop6": "2001:db8::a", + "enabled": True, + "router_port_id": 7, + "bgp_peer_timers": { + "hold_time_seconds": 90, + "graceful_restart_end_of_rib_seconds": 30, + }, + } + commands = generate_peer_group_commands(peer_group) + warnings = [c for c in commands if c.startswith("# WARNING")] + self.assertEqual( + warnings, + [ + f"# WARNING: peer-group TEST: field {f} has no CLI equivalent, " + "not converted" + for f in ( + "local_addr", + "next_hop4", + "next_hop6", + "enabled", + "router_port_id", + "bgp_peer_timers.graceful_restart_end_of_rib_seconds", + ) + ], + ) + self.assertEqual( + [c for c in commands if not c.startswith("#")], + [f"{self.PREFIX} remote-asn 65000", f"{self.PREFIX} timers hold-time 90"], + ) class GeneratePeerCommandsTest(unittest.TestCase): From ca2a5696adfab8d0b030ad0c82c9949235c1c2fe Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Fri, 7 Aug 2026 18:31:22 +0000 Subject: [PATCH 15/22] [Nexthop] NOS-6643..NOS-6648: fboss2 config/delete protocol bgp policy as-path-list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the first BGP policy object-type command family, under a new `policy` grouping node beneath `protocol bgp` (sibling to global/neighbor/peer-group): config protocol bgp policy as-path-list [description ] delete protocol bgp policy as-path-list The list's `entry ` level is a CLI11 subcommand of its own and lands in the next commit, so this one owns only the list level. - Follows the dispatcher shape the neighbor/peer-group families use: one factory per value shape from the shared BgpCliAttrHandlers.h, named setters that do nothing but assign the thrift field, and a registry that is one line per attribute. Writes bgp_policy.BgpPolicies.aspath_lists[] through the typed ConfigSession — no new session plumbing, since BgpConfig.policies already exists. - Lookup/create helpers live in BgpAsPathListCliUtils.h, including the entry-level helpers, so the delete command and the entry subcommand in the next commit share them rather than re-deriving the scan. - A rejected value leaves nothing staged: a list implicitly created for the failed command is rolled back before returning. - There is no per-entry delete; `delete ... as-path-list ` removes the whole list. - unit tests (10): CmdConfigBgpPolicyAsPathListTest (6) covering arg validation, bare create, the description round-trip, named lists staying distinct, re-reference reporting the existing list and unknown-attribute rejection, plus CmdDeleteBgpPolicyAsPathListTest (4). - integration tests (2): ConfigBgpPolicyAsPathListTest stages and commits, then asserts against bgpd's running config via the getRunningConfig RPC, confirming bgpd accepts and adopts the .policies blob end to end. Co-Authored-By: Claude Fable 5 --- cmake/CliFboss2.cmake | 9 + cmake/CliFboss2TestConfig.cmake | 2 + cmake/CliFboss2TestIntegrationTest.cmake | 1 + fboss/cli/fboss2/BUCK | 9 + fboss/cli/fboss2/CmdListConfig.cpp | 32 ++++ .../bgp/policy/CmdConfigProtocolBgpPolicy.cpp | 30 ++++ .../bgp/policy/CmdConfigProtocolBgpPolicy.h | 40 +++++ .../as-path-list/BgpAsPathListCliUtils.h | 87 ++++++++++ .../CmdConfigProtocolBgpPolicyAsPathList.cpp | 157 ++++++++++++++++++ .../CmdConfigProtocolBgpPolicyAsPathList.h | 82 +++++++++ .../bgp/policy/CmdDeleteProtocolBgpPolicy.cpp | 32 ++++ .../bgp/policy/CmdDeleteProtocolBgpPolicy.h | 43 +++++ .../CmdDeleteProtocolBgpPolicyAsPathList.cpp | 80 +++++++++ .../CmdDeleteProtocolBgpPolicyAsPathList.h | 63 +++++++ fboss/cli/fboss2/test/config/BUCK | 2 + .../CmdConfigBgpPolicyAsPathListTest.cpp | 137 +++++++++++++++ .../CmdDeleteBgpPolicyAsPathListTest.cpp | 113 +++++++++++++ fboss/cli/fboss2/test/integration_test/BUCK | 1 + .../ConfigBgpPolicyAsPathListTest.cpp | 118 +++++++++++++ 19 files changed, 1038 insertions(+) create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.cpp create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.cpp create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h create mode 100644 fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.cpp create mode 100644 fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.h create mode 100644 fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.cpp create mode 100644 fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h create mode 100644 fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListTest.cpp create mode 100644 fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyAsPathListTest.cpp create mode 100644 fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListTest.cpp diff --git a/cmake/CliFboss2.cmake b/cmake/CliFboss2.cmake index 4df1a9a13e888..81c64ceb5cfba 100644 --- a/cmake/CliFboss2.cmake +++ b/cmake/CliFboss2.cmake @@ -951,6 +951,11 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h + fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.cpp + fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h + fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h + fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.cpp + fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.cpp fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.h fboss/cli/fboss2/commands/config/ptp/transparent_clock/CmdConfigPtpTransparentClock.cpp @@ -1069,6 +1074,10 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.cpp fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.h + fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.cpp + fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.h + fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.cpp + fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h fboss/cli/fboss2/commands/delete/protocol/CmdDeleteProtocol.h fboss/cli/fboss2/commands/delete/protocol/static/CmdDeleteProtocolStatic.cpp fboss/cli/fboss2/commands/delete/protocol/static/CmdDeleteProtocolStatic.h diff --git a/cmake/CliFboss2TestConfig.cmake b/cmake/CliFboss2TestConfig.cmake index 27a52a374adfb..c1526444ca844 100644 --- a/cmake/CliFboss2TestConfig.cmake +++ b/cmake/CliFboss2TestConfig.cmake @@ -13,6 +13,7 @@ add_executable(fboss2_cmd_config_test fboss/cli/fboss2/test/config/CmdConfigIcmpV4UnavailableSrcAddrTest.cpp fboss/cli/fboss2/test/config/CmdConfigBgpNeighborTest.cpp fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp + fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListTest.cpp fboss/cli/fboss2/test/config/CmdConfigCoppTest.cpp fboss/cli/fboss2/test/config/CmdConfigDhcpTest.cpp fboss/cli/fboss2/test/config/CmdConfigHostnameTest.cpp @@ -47,6 +48,7 @@ add_executable(fboss2_cmd_config_test fboss/cli/fboss2/test/config/CmdDeleteArpTest.cpp fboss/cli/fboss2/test/config/CmdDeleteBgpNeighborTest.cpp fboss/cli/fboss2/test/config/CmdDeleteBgpPeerGroupTest.cpp + fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyAsPathListTest.cpp fboss/cli/fboss2/test/config/CmdDeleteConfigInterfaceTest.cpp fboss/cli/fboss2/test/config/CmdDeleteCoppQueueTest.cpp fboss/cli/fboss2/test/config/CmdDeleteCoppReasonTest.cpp diff --git a/cmake/CliFboss2TestIntegrationTest.cmake b/cmake/CliFboss2TestIntegrationTest.cmake index d8ade0e72e6ae..e0d174f42f197 100644 --- a/cmake/CliFboss2TestIntegrationTest.cmake +++ b/cmake/CliFboss2TestIntegrationTest.cmake @@ -16,6 +16,7 @@ add_executable(fboss2_integration_test fboss/cli/fboss2/test/integration_test/ConfigBgpGlobalTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpNeighborTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp + fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp fboss/cli/fboss2/test/integration_test/ConfigConcurrentSessionsTest.cpp fboss/cli/fboss2/test/integration_test/ConfigHostnameTest.cpp diff --git a/fboss/cli/fboss2/BUCK b/fboss/cli/fboss2/BUCK index ee63b3a3d2e86..f42fe1babc02a 100644 --- a/fboss/cli/fboss2/BUCK +++ b/fboss/cli/fboss2/BUCK @@ -1166,6 +1166,8 @@ cpp_library( "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.cpp", "commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.cpp", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp", + "commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.cpp", + "commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.cpp", "commands/config/protocol/static/CmdConfigProtocolStatic.cpp", "commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.cpp", "commands/config/ptp/CmdConfigPtp.cpp", @@ -1224,6 +1226,8 @@ cpp_library( "commands/delete/protocol/bgp/CmdDeleteProtocolBgp.cpp", "commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.cpp", "commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.cpp", + "commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.cpp", + "commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.cpp", "commands/delete/protocol/static/CmdDeleteProtocolStatic.cpp", "commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.cpp", "commands/delete/qos/CmdDeleteQos.cpp", @@ -1291,6 +1295,9 @@ cpp_library( "commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h", "commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h", "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h", + "commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h", + "commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h", + "commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h", "commands/config/protocol/static/CmdConfigProtocolStatic.h", "commands/config/protocol/static/route/StaticRouteUtils.h", "commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.h", @@ -1350,6 +1357,8 @@ cpp_library( "commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h", "commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h", "commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.h", + "commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.h", + "commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h", "commands/delete/protocol/static/CmdDeleteProtocolStatic.h", "commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.h", "commands/delete/qos/CmdDeleteQos.h", diff --git a/fboss/cli/fboss2/CmdListConfig.cpp b/fboss/cli/fboss2/CmdListConfig.cpp index 1174f7bc7caaf..f2605de922b51 100644 --- a/fboss/cli/fboss2/CmdListConfig.cpp +++ b/fboss/cli/fboss2/CmdListConfig.cpp @@ -41,6 +41,8 @@ #include "fboss/cli/fboss2/commands/config/protocol/bgp/global/CmdConfigProtocolBgpGlobal.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/neighbor/CmdConfigProtocolBgpNeighbor.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h" #include "fboss/cli/fboss2/commands/config/protocol/static/CmdConfigProtocolStatic.h" #include "fboss/cli/fboss2/commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.h" #include "fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.h" @@ -96,6 +98,8 @@ #include "fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h" #include "fboss/cli/fboss2/commands/delete/protocol/bgp/neighbor/CmdDeleteProtocolBgpNeighbor.h" #include "fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h" #include "fboss/cli/fboss2/commands/delete/protocol/static/CmdDeleteProtocolStatic.h" #include "fboss/cli/fboss2/commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.h" #include "fboss/cli/fboss2/commands/delete/qos/CmdDeleteQos.h" @@ -387,6 +391,21 @@ const CommandTree& kConfigCommandTree() { commandHandler, argRegistrar, }, + { + "policy", + "Configure BGP policy objects", + commandHandler, + argRegistrar, + {{ + "as-path-list", + "Configure BGP AS-path list: " + "[ ...] (description)", + commandHandler< + CmdConfigProtocolBgpPolicyAsPathList>, + argRegistrar< + CmdConfigProtocolBgpPolicyAsPathListTraits>, + }}, + }, }, }, { @@ -728,6 +747,19 @@ const CommandTree& kConfigCommandTree() { "Delete a BGP peer-group: ", commandHandler, argRegistrar, + }, + { + "policy", + "Delete BGP policy objects", + commandHandler, + argTypeHandler, + {{ + "as-path-list", + "Delete a BGP AS-path list: ", + commandHandler, + argRegistrar< + CmdDeleteProtocolBgpPolicyAsPathListTraits>, + }}, }}, }, { diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.cpp new file mode 100644 index 0000000000000..ace33a2b0c2ce --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.cpp @@ -0,0 +1,30 @@ +/* + * 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/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h" + +#include "fboss/cli/fboss2/CmdHandler.cpp" + +namespace facebook::fboss { + +CmdConfigProtocolBgpPolicyTraits::RetType +CmdConfigProtocolBgpPolicy::queryClient(const HostInfo& /* hostInfo */) { + return "BGP policy configuration. Use subcommands: as-path-list"; +} + +void CmdConfigProtocolBgpPolicy::printOutput(const RetType& output) { + std::cout << output << std::endl; +} + +// Explicit template instantiation +template void +CmdHandler::run(); + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h new file mode 100644 index 0000000000000..4830fe77f5545 --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h @@ -0,0 +1,40 @@ +/* + * 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/commands/config/protocol/bgp/CmdConfigProtocolBgp.h" + +namespace facebook::fboss { + +// Grouping node for `config protocol bgp policy ...`. Holds no +// state of its own; the per-object-type dispatchers (as-path-list, and later +// community-list/prefix-list/routing-policy) are its subcommands. Mirrors +// CmdConfigProtocolBgp. +struct CmdConfigProtocolBgpPolicyTraits : public WriteCommandTraits { + using ParentCmd = CmdConfigProtocolBgp; + using ObjectArgType = std::monostate; + using RetType = std::string; +}; + +class CmdConfigProtocolBgpPolicy : public CmdHandler< + CmdConfigProtocolBgpPolicy, + CmdConfigProtocolBgpPolicyTraits> { + public: + using ObjectArgType = CmdConfigProtocolBgpPolicyTraits::ObjectArgType; + using RetType = CmdConfigProtocolBgpPolicyTraits::RetType; + + RetType queryClient(const HostInfo& hostInfo); + + void printOutput(const RetType& output); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h new file mode 100644 index 0000000000000..5c627006a629f --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h @@ -0,0 +1,87 @@ +/* + * 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 +#include +#include +#include +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" + +/** + * Lookup/create helpers for the as-path-list CLI family, shared between the + * list-level dispatcher (CmdConfigProtocolBgpPolicyAsPathList), the entry + * subcommand (CmdConfigProtocolBgpPolicyAsPathListEntry), and the delete + * counterparts. An AsPathList is keyed by name; an AsPathListEntry (in + * as_path_list[]) is keyed by sequence_number. + */ +namespace facebook::fboss::bgpcli { + +inline bool asPathListExists( + const bgp::thrift::BgpConfig& cfg, + const std::string& name) { + if (!cfg.policies().has_value()) { + return false; + } + const auto& lists = *cfg.policies()->aspath_lists(); + return std::any_of(lists.begin(), lists.end(), [&](const auto& list) { + return *list.name() == name; + }); +} + +// Find the as-path-list keyed by name, creating it if absent. Setting an +// attribute on a not-yet-created list implicitly creates it, so command +// ordering stays forgiving; a bare `as-path-list ` creates one +// explicitly. AsPathList's only key field is `name`. +inline bgp::bgp_policy::AsPathList& findOrCreateAsPathList( + bgp::thrift::BgpConfig& cfg, + const std::string& name) { + auto& lists = *cfg.policies().ensure().aspath_lists(); + for (auto& list : lists) { + if (*list.name() == name) { + return list; + } + } + lists.emplace_back(); + auto& list = lists.back(); + list.name() = name; + return list; +} + +inline bool asPathListEntryExists( + const bgp::bgp_policy::AsPathList& list, + int64_t seqNum) { + const auto& entries = *list.as_path_list(); + return std::any_of(entries.begin(), entries.end(), [&](const auto& entry) { + return entry.sequence_number().has_value() && + *entry.sequence_number() == seqNum; + }); +} + +// Find the entry keyed by sequence_number within a list, creating it if +// absent. sequence_number is the entry's identity. +inline bgp::bgp_policy::AsPathListEntry& findOrCreateAsPathListEntry( + bgp::bgp_policy::AsPathList& list, + int64_t seqNum) { + auto& entries = *list.as_path_list(); + for (auto& entry : entries) { + if (entry.sequence_number().has_value() && + *entry.sequence_number() == seqNum) { + return entry; + } + } + entries.emplace_back(); + auto& entry = entries.back(); + entry.sequence_number() = seqNum; + return entry; +} + +} // namespace facebook::fboss::bgpcli diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.cpp new file mode 100644 index 0000000000000..e4c6edbc7c0ec --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.cpp @@ -0,0 +1,157 @@ +/* + * 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/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h" + +#include "fboss/cli/fboss2/CmdHandler.cpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliAttrHandlers.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" +#include "fmt/format.h" + +namespace facebook::fboss { + +namespace { + +// The attribute names, exactly as documented. Kept here so the +// valid-attribute set and the handler table stay in sync. The list's entries +// are their own subcommand, not attributes. +constexpr std::string_view kDescription = "description"; + +using AsPathList = bgp::bgp_policy::AsPathList; +using bgpcli::AttrHandler; +using bgpcli::joinedStringAttr; +using bgpcli::ok; +using bgpcli::Result; + +// ---- setters ---------------------------------------------------------------- +// Each writes one already-parsed, already-validated value. Parsing and message +// text belong to the shared factories in BgpCliAttrHandlers.h. + +void setListDescription(AsPathList& list, const std::string& description) { + list.description() = description; +} + +// ---- list-level attribute registry ------------------------------------------ +// One line per documented attribute: its dispatch key, its value shape, and +// the setter that stores it. +const std::map, std::less<>>& +listAttrHandlers() { + static const std::map, std::less<>> + kHandlers = { + {std::string(kDescription), + joinedStringAttr(kDescription, setListDescription)}, + }; + return kHandlers; +} + +std::string validAttrList() { + std::string out; + for (const auto& [name, _] : listAttrHandlers()) { + if (!out.empty()) { + out += ", "; + } + out += name; + } + return out; +} + +} // namespace + +// Parse + validate at construction so queryClient stays a thin dispatch. +// Throwing std::invalid_argument is how the framework surfaces arg parse +// errors (same mechanism as BgpPeerGroupConfig). +BgpAsPathListConfig::BgpAsPathListConfig(std::vector v) + : utils::BaseObjectArgType(v) { + if (v.empty()) { + throw std::invalid_argument( + "Error: as-path-list is required, optionally followed by an " + " "); + } + if (v[0].empty()) { + throw std::invalid_argument("Error: as-path-list name must not be empty"); + } + listName_ = v[0]; + + if (v.size() == 1) { + return; // bare `as-path-list `: create the list + } + + attr_ = v[1]; + values_.assign(v.begin() + 2, v.end()); + + if (listAttrHandlers().find(attr_) == listAttrHandlers().end()) { + throw std::invalid_argument( + fmt::format( + "Error: unknown as-path-list attribute '{}'. Valid attributes: {}", + attr_, + validAttrList())); + } +} + +CmdConfigProtocolBgpPolicyAsPathListTraits::RetType +CmdConfigProtocolBgpPolicyAsPathList::queryClient( + const HostInfo& /* hostInfo */, + const ObjectArgType& args) { + auto& session = ConfigSession::getInstance(); + auto& cfg = session.getBgpConfig(); + const bool listCreated = !bgpcli::asPathListExists(cfg, args.listName()); + auto& list = bgpcli::findOrCreateAsPathList(cfg, args.listName()); + + Result result = args.attr().empty() + ? ok(listCreated + ? fmt::format( + "Successfully created BGP as-path-list {}", + args.listName()) + : fmt::format( + "BGP as-path-list {} already exists", args.listName())) + // The attribute is guaranteed valid: BgpAsPathListConfig's constructor + // rejects an unknown attribute before we get here. + : listAttrHandlers().find(args.attr())->second(list, args.values()); + + if (result.ok) { + if (!args.attr().empty()) { + result.message += fmt::format(" for as-path-list {}", args.listName()); + } + session.saveBgpConfig(); + result.message += + fmt::format("\nConfig saved to: {}", session.getBgpSessionConfigPath()); + } else if (listCreated) { + // Drop the phantom list so a rejected value is not visible to later + // lookups in the same process. + cfg.policies()->aspath_lists()->pop_back(); + } + return result.message; +} + +void CmdConfigProtocolBgpPolicyAsPathList::printOutput(const RetType& output) { + std::cout << output << std::endl; +} + +// Explicit template instantiation +template void CmdHandler< + CmdConfigProtocolBgpPolicyAsPathList, + CmdConfigProtocolBgpPolicyAsPathListTraits>::run(); + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h new file mode 100644 index 0000000000000..7fe25d5f548de --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h @@ -0,0 +1,82 @@ +/* + * 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 +#include +#include "CLI/App.hpp" +#include "fboss/cli/fboss2/CmdHandler.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +namespace facebook::fboss { + +// Parsed `as-path-list [ ...]`, validated at +// construction. An AsPathList is keyed by . The list's entries are their +// own subcommand (CmdConfigProtocolBgpPolicyAsPathListEntry), not attributes. +// +// Grammar (from the FBOSS proposed syntax): +// as-path-list (create/select list) +// as-path-list description (list attribute) +class BgpAsPathListConfig : public utils::BaseObjectArgType { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + /* implicit */ BgpAsPathListConfig(std::vector v); + const std::string& listName() const { + return listName_; + } + const std::string& attr() const { + return attr_; + } + const std::vector& values() const { + return values_; + } + const static utils::ObjectArgTypeId id = + utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_MESSAGE; + + private: + std::string listName_; + std::string attr_; // matched dispatch key ("" = bare create) + std::vector values_; +}; + +// The list level of the `config protocol bgp policy as-path-list` family; +// dispatch and per-attribute parsing live in the .cpp so a new tunable is a +// one-entry change. Mirrors CmdConfigProtocolBgpPolicyRoutingPolicy, whose +// nested level (term / entry) is likewise its own subcommand. +struct CmdConfigProtocolBgpPolicyAsPathListTraits : public WriteCommandTraits { + using ParentCmd = CmdConfigProtocolBgpPolicy; + static void addCliArg(CLI::App& cmd, std::vector& args) { + // No positionals_at_end() here: CLI11 must stay free to classify the + // `entry` token as this command's subcommand rather than swallowing it + // into args. See CmdConfigProtocolBgpPolicyRoutingPolicyTraits. + cmd.add_option("args", args, " [ ...]"); + } + using ObjectArgType = BgpAsPathListConfig; + using RetType = std::string; +}; + +class CmdConfigProtocolBgpPolicyAsPathList + : public CmdHandler< + CmdConfigProtocolBgpPolicyAsPathList, + CmdConfigProtocolBgpPolicyAsPathListTraits> { + public: + using ObjectArgType = + CmdConfigProtocolBgpPolicyAsPathListTraits::ObjectArgType; + using RetType = CmdConfigProtocolBgpPolicyAsPathListTraits::RetType; + + RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& args); + + void printOutput(const RetType& output); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.cpp b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.cpp new file mode 100644 index 0000000000000..bc15db0a326f4 --- /dev/null +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.cpp @@ -0,0 +1,32 @@ +/* + * 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/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.h" +#include +#include + +#include "fboss/cli/fboss2/CmdHandler.cpp" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +namespace facebook::fboss { + +CmdDeleteProtocolBgpPolicyTraits::RetType +CmdDeleteProtocolBgpPolicy::queryClient(const HostInfo& /* hostInfo */) { + return "Delete BGP policy objects. Use subcommands: as-path-list"; +} + +void CmdDeleteProtocolBgpPolicy::printOutput(const RetType& output) { + std::cout << output << std::endl; +} + +template void +CmdHandler::run(); + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.h b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.h new file mode 100644 index 0000000000000..d658fee78a4ba --- /dev/null +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.h @@ -0,0 +1,43 @@ +/* + * 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 +#include "fboss/cli/fboss2/CmdHandler.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/CmdDeleteProtocolBgp.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +namespace facebook::fboss { + +// Grouping node for `delete protocol bgp policy `. Holds no +// state of its own; the per-object-type delete commands are its subcommands. +struct CmdDeleteProtocolBgpPolicyTraits : public WriteCommandTraits { + using ParentCmd = CmdDeleteProtocolBgp; + static constexpr utils::ObjectArgTypeId ObjectArgTypeId = + utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_NONE; + using ObjectArgType = std::monostate; + using RetType = std::string; +}; + +class CmdDeleteProtocolBgpPolicy : public CmdHandler< + CmdDeleteProtocolBgpPolicy, + CmdDeleteProtocolBgpPolicyTraits> { + public: + using ObjectArgType = CmdDeleteProtocolBgpPolicyTraits::ObjectArgType; + using RetType = CmdDeleteProtocolBgpPolicyTraits::RetType; + + RetType queryClient(const HostInfo& hostInfo); + + void printOutput(const RetType& output); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.cpp b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.cpp new file mode 100644 index 0000000000000..b9f3bfb68dab5 --- /dev/null +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.cpp @@ -0,0 +1,80 @@ +/* + * 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/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h" + +#include "fboss/cli/fboss2/CmdHandler.cpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" +#include "fmt/format.h" + +namespace facebook::fboss { + +// Parse + validate at construction so queryClient stays a thin dispatch. +BgpAsPathListRef::BgpAsPathListRef(std::vector v) + : utils::BaseObjectArgType(v) { + if (v.size() != 1) { + throw std::invalid_argument( + "Error: delete protocol bgp policy as-path-list requires exactly one " + ""); + } + if (v[0].empty()) { + throw std::invalid_argument("Error: as-path-list name must not be empty"); + } + listName_ = std::move(v[0]); +} + +CmdDeleteProtocolBgpPolicyAsPathListTraits::RetType +CmdDeleteProtocolBgpPolicyAsPathList::queryClient( + const HostInfo& /* hostInfo */, + const ObjectArgType& args) { + auto& session = ConfigSession::getInstance(); + auto& cfg = session.getBgpConfig(); + if (!cfg.policies().has_value()) { + // Nothing is persisted for an unknown list, so a typo'd delete can't stage + // an unrelated session change. + return fmt::format("Error: BGP as-path-list {} not found", args.listName()); + } + auto& lists = *cfg.policies()->aspath_lists(); + auto it = std::find_if(lists.begin(), lists.end(), [&](const auto& list) { + return *list.name() == args.listName(); + }); + if (it == lists.end()) { + return fmt::format("Error: BGP as-path-list {} not found", args.listName()); + } + lists.erase(it); + session.saveBgpConfig(); + return fmt::format( + "Successfully deleted BGP as-path-list {}\nConfig saved to: {}", + args.listName(), + session.getBgpSessionConfigPath()); +} + +void CmdDeleteProtocolBgpPolicyAsPathList::printOutput(const RetType& output) { + std::cout << output << std::endl; +} + +// Explicit template instantiation +template void CmdHandler< + CmdDeleteProtocolBgpPolicyAsPathList, + CmdDeleteProtocolBgpPolicyAsPathListTraits>::run(); + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h new file mode 100644 index 0000000000000..4249bdd3546a4 --- /dev/null +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h @@ -0,0 +1,63 @@ +/* + * 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 +#include +#include "CLI/App.hpp" +#include "fboss/cli/fboss2/CmdHandler.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +namespace facebook::fboss { + +// Parsed `delete protocol bgp policy as-path-list `, validated at +// construction. The name is the list's identity, matched exactly the same way +// the config command stores it. Mirrors BgpPeerGroupRef. +class BgpAsPathListRef : public utils::BaseObjectArgType { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + /* implicit */ BgpAsPathListRef(std::vector v); + const std::string& listName() const { + return listName_; + } + const static utils::ObjectArgTypeId id = + utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_MESSAGE; + + private: + std::string listName_; +}; + +struct CmdDeleteProtocolBgpPolicyAsPathListTraits : public WriteCommandTraits { + using ParentCmd = CmdDeleteProtocolBgpPolicy; + static void addCliArg(CLI::App& cmd, std::vector& args) { + cmd.add_option("args", args, ""); + } + using ObjectArgType = BgpAsPathListRef; + using RetType = std::string; +}; + +class CmdDeleteProtocolBgpPolicyAsPathList + : public CmdHandler< + CmdDeleteProtocolBgpPolicyAsPathList, + CmdDeleteProtocolBgpPolicyAsPathListTraits> { + public: + using ObjectArgType = + CmdDeleteProtocolBgpPolicyAsPathListTraits::ObjectArgType; + using RetType = CmdDeleteProtocolBgpPolicyAsPathListTraits::RetType; + + RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& args); + + void printOutput(const RetType& output); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/config/BUCK b/fboss/cli/fboss2/test/config/BUCK index 0ae31a32db654..2e8ea13d667e0 100644 --- a/fboss/cli/fboss2/test/config/BUCK +++ b/fboss/cli/fboss2/test/config/BUCK @@ -18,6 +18,7 @@ cpp_unittest( "CmdConfigBgpGlobalTest.cpp", "CmdConfigBgpNeighborTest.cpp", "CmdConfigBgpPeerGroupTest.cpp", + "CmdConfigBgpPolicyAsPathListTest.cpp", "CmdConfigCoppTest.cpp", "CmdConfigDhcpTest.cpp", "CmdConfigHistoryTest.cpp", @@ -66,6 +67,7 @@ cpp_unittest( "CmdDeleteVlanTest.cpp", "ConfigSessionSystemdTest.cpp", "CmdDeleteBgpPeerGroupTest.cpp", + "CmdDeleteBgpPolicyAsPathListTest.cpp", "ProfileValidationTest.cpp", ], headers = [ diff --git a/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListTest.cpp b/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListTest.cpp new file mode 100644 index 0000000000000..c0e96f09a023d --- /dev/null +++ b/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListTest.cpp @@ -0,0 +1,137 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include +#include +#include + +#include +#include +#include +#include + +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/test/config/CmdConfigTestBase.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +using namespace ::testing; + +namespace facebook::fboss { + +// The as-path-list dispatcher only touches the BGP side of ConfigSession, +// which seeds from thrift schema defaults when neither a staged session nor a +// system bgpcpp.conf exists — so no seed agent config is needed (mirrors +// CmdConfigBgpPeerGroupTest). +class CmdConfigBgpPolicyAsPathListTestFixture : public CmdConfigTestBase { + public: + CmdConfigBgpPolicyAsPathListTestFixture() + : CmdConfigTestBase("bgp_aspath_list_test_%%%%-%%%%-%%%%", "") {} + + void SetUp() override { + CmdConfigTestBase::SetUp(); + setupTestableConfigSession(); + } + + std::string run(const std::vector& tokens) { + CmdConfigProtocolBgpPolicyAsPathList cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpAsPathListConfig(tokens)); + } + + const std::vector& lists() { + return *ConfigSession::getInstance() + .getBgpConfig() + .policies() + .ensure() + .aspath_lists(); + } + + bool sessionFileExists() { + return std::filesystem::exists( + ConfigSession::getInstance().getBgpSessionConfigPath()); + } +}; + +// ============================================================================== +// BgpAsPathListConfig (arg) validation +// ============================================================================== + +TEST_F(CmdConfigBgpPolicyAsPathListTestFixture, argValidation) { + // Bare create list. + auto bareList = BgpAsPathListConfig({"AS100"}); + EXPECT_EQ(bareList.listName(), "AS100"); + EXPECT_TRUE(bareList.attr().empty()); + + // List-level attribute. + auto listAttr = BgpAsPathListConfig({"AS100", "description", "my", "list"}); + EXPECT_EQ(listAttr.attr(), "description"); + EXPECT_EQ(listAttr.values(), std::vector({"my", "list"})); + + // Invalid: empty, empty name, unknown attribute. + EXPECT_THROW(BgpAsPathListConfig({}), std::invalid_argument); + EXPECT_THROW(BgpAsPathListConfig({""}), std::invalid_argument); + EXPECT_THROW( + BgpAsPathListConfig({"AS100", "no-such-attr", "1"}), + std::invalid_argument); + // asn-regexp is an entry attribute, not a list attribute. + EXPECT_THROW( + BgpAsPathListConfig({"AS100", "asn-regexp", "^65000_"}), + std::invalid_argument); +} + +// ============================================================================== +// List-level handlers +// ============================================================================== + +TEST_F(CmdConfigBgpPolicyAsPathListTestFixture, bareCreateList) { + auto result = run({"AS100"}); + EXPECT_THAT(result, HasSubstr("Successfully created BGP as-path-list AS100")); + ASSERT_EQ(lists().size(), 1); + EXPECT_EQ(*lists()[0].name(), "AS100"); + EXPECT_TRUE(lists()[0].as_path_list()->empty()); + EXPECT_TRUE(sessionFileExists()); + + run({"AS200"}); + ASSERT_EQ(lists().size(), 2); + EXPECT_EQ(*lists()[1].name(), "AS200"); +} + +TEST_F(CmdConfigBgpPolicyAsPathListTestFixture, setListDescription) { + auto result = run({"AS100", "description", "spine", "as-paths"}); + EXPECT_THAT(result, HasSubstr("Successfully set description")); + ASSERT_EQ(lists().size(), 1); + // Multi-token description is re-joined. + EXPECT_EQ(*lists()[0].description(), "spine as-paths"); +} + +TEST_F(CmdConfigBgpPolicyAsPathListTestFixture, namedListsAreDistinct) { + run({"AS100", "description", "one"}); + run({"AS200", "description", "two"}); + ASSERT_EQ(lists().size(), 2); + // Re-referencing an existing list by name updates it, not appends. + run({"AS100", "description", "one-updated"}); + ASSERT_EQ(lists().size(), 2); + EXPECT_EQ(*lists()[0].description(), "one-updated"); +} + +// ============================================================================== +// Reject paths — the error is surfaced and nothing is persisted +// ============================================================================== + +TEST_F(CmdConfigBgpPolicyAsPathListTestFixture, reReferenceReportsExisting) { + run({"AS100"}); + // A second bare reference must not claim to have created it again. + EXPECT_THAT(run({"AS100"}), HasSubstr("already exists")); + ASSERT_EQ(lists().size(), 1); +} + +TEST_F(CmdConfigBgpPolicyAsPathListTestFixture, unknownAttributeRejected) { + EXPECT_THROW(run({"AS100", "no-such-attr", "1"}), std::invalid_argument); + // The rejected command must not leave a phantom list behind. + EXPECT_TRUE(lists().empty()); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyAsPathListTest.cpp b/fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyAsPathListTest.cpp new file mode 100644 index 0000000000000..7c575a4dd51e0 --- /dev/null +++ b/fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyAsPathListTest.cpp @@ -0,0 +1,113 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include +#include +#include + +#include +#include +#include +#include + +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/test/config/CmdConfigTestBase.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +using namespace ::testing; + +namespace facebook::fboss { + +// Deleting only touches the BGP side of ConfigSession, which seeds from thrift +// schema defaults when neither a staged session nor a system bgpcpp.conf +// exists — so no seed agent config is needed (mirrors +// CmdConfigBgpPeerGroupTest). +class CmdDeleteBgpPolicyAsPathListTestFixture : public CmdConfigTestBase { + public: + CmdDeleteBgpPolicyAsPathListTestFixture() + : CmdConfigTestBase("bgp_aspath_list_delete_test_%%%%-%%%%-%%%%", "") {} + + void SetUp() override { + CmdConfigTestBase::SetUp(); + setupTestableConfigSession(); + } + + std::string configure(const std::vector& tokens) { + CmdConfigProtocolBgpPolicyAsPathList cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpAsPathListConfig(tokens)); + } + + std::string del(const std::vector& tokens) { + CmdDeleteProtocolBgpPolicyAsPathList cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpAsPathListRef(tokens)); + } + + const std::vector& lists() { + return *ConfigSession::getInstance() + .getBgpConfig() + .policies() + .ensure() + .aspath_lists(); + } + + bool sessionFileExists() { + return std::filesystem::exists( + ConfigSession::getInstance().getBgpSessionConfigPath()); + } +}; + +// ============================================================================== +// BgpAsPathListRef (arg) validation +// ============================================================================== + +TEST_F(CmdDeleteBgpPolicyAsPathListTestFixture, argValidation) { + EXPECT_EQ(BgpAsPathListRef({"AS100"}).listName(), "AS100"); + + // Invalid: empty, empty name, extra tokens. + EXPECT_THROW(BgpAsPathListRef({}), std::invalid_argument); + EXPECT_THROW(BgpAsPathListRef({""}), std::invalid_argument); + EXPECT_THROW(BgpAsPathListRef({"AS100", "AS200"}), std::invalid_argument); +} + +// ============================================================================== +// queryClient +// ============================================================================== + +TEST_F(CmdDeleteBgpPolicyAsPathListTestFixture, deleteExistingList) { + configure({"AS100", "description", "delete-me"}); + configure({"AS200", "description", "keep"}); + ASSERT_EQ(lists().size(), 2); + + auto result = del({"AS100"}); + EXPECT_THAT(result, HasSubstr("Successfully deleted BGP as-path-list AS100")); + // The other list survives untouched. + ASSERT_EQ(lists().size(), 1); + EXPECT_EQ(*lists()[0].name(), "AS200"); + EXPECT_EQ(*lists()[0].description(), "keep"); + EXPECT_TRUE(sessionFileExists()); +} + +TEST_F(CmdDeleteBgpPolicyAsPathListTestFixture, deleteUnknownListRejected) { + auto result = del({"NO-SUCH-LIST"}); + EXPECT_THAT( + result, HasSubstr("Error: BGP as-path-list NO-SUCH-LIST not found")); + // Nothing was persisted for the failed delete. + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected delete"; +} + +TEST_F( + CmdDeleteBgpPolicyAsPathListTestFixture, + deleteUnknownLeavesOthersIntact) { + configure({"AS100", "description", "one"}); + auto result = del({"AS200"}); + EXPECT_THAT(result, HasSubstr("not found")); + ASSERT_EQ(lists().size(), 1); + EXPECT_EQ(*lists()[0].name(), "AS100"); +} + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/integration_test/BUCK b/fboss/cli/fboss2/test/integration_test/BUCK index 6986b1c6d4618..87108b4295425 100644 --- a/fboss/cli/fboss2/test/integration_test/BUCK +++ b/fboss/cli/fboss2/test/integration_test/BUCK @@ -23,6 +23,7 @@ cpp_binary( "ConfigBgpGlobalTest.cpp", "ConfigBgpNeighborTest.cpp", "ConfigBgpPeerGroupTest.cpp", + "ConfigBgpPolicyAsPathListTest.cpp", "ConfigBgpSessionTest.cpp", "ConfigConcurrentSessionsTest.cpp", "ConfigCoppTest.cpp", diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListTest.cpp new file mode 100644 index 0000000000000..62fb1bc07cb11 --- /dev/null +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListTest.cpp @@ -0,0 +1,118 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +/** + * End-to-end tests for `fboss2-dev config protocol bgp policy as-path-list + * [ ...]`. + * + * Scope: the AS-path list level (its entries are covered by + * ConfigBgpPolicyAsPathListEntryTest). Every test stages the change AND + * commits it, then asserts the value landed at the correct thrift field path + * inside the matching .policies.aspath_lists[] entry of bgpd's running config + * (via getRunningConfig RPC) — which also confirms bgpd accepts and adopts a + * `.policies` blob at all. Stage-only behavior (attribute parsing, + * validation, rejection) is covered by the unit tests; an integration test + * that never commits exercises no daemon. + * + * Requirements: + * - The fboss2-dev binary under test (config subcommand tree). + * - HOME is set (the session file lives under $HOME/.fboss2). + * - bgpd is installed/active (commit restarts it). + */ + +#include +#include +#include + +#include "fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h" +#include "folly/json/dynamic.h" +#include "gmock/gmock.h" + +using namespace facebook::fboss; +using ::testing::HasSubstr; +using ::testing::Not; + +namespace { +// Test-only AS-path-list names, unlikely to collide with a real list in the +// device's running BGP config. +const std::string kList = "FBOSS2-TEST-ASPATH"; +} // namespace + +class ConfigBgpPolicyAsPathListTest : public ConfigBgpTestBase { + protected: + // Stage `config protocol bgp policy as-path-list ` WITHOUT + // clearing the staged session, so attributes can accumulate across + // invocations. Returns the staged session JSON. + folly::dynamic stageAsPathList(const std::vector& tokens) { + std::vector args = { + "config", "protocol", "bgp", "policy", "as-path-list"}; + args.insert(args.end(), tokens.begin(), tokens.end()); + auto result = runCli(args); + EXPECT_EQ(result.exitCode, 0) + << "stdout=" << result.stdout << " stderr=" << result.stderr; + EXPECT_THAT(result.stdout, Not(HasSubstr("Error:"))); + return readBgpSessionConfig(); + } + + // The .policies.aspath_lists[] entry named `name`, or nullptr. + static const folly::dynamic* findList( + const folly::dynamic& config, + const std::string& name) { + if (config.count("policies") == 0 || + config["policies"].count("aspath_lists") == 0) { + return nullptr; + } + for (const auto& list : config["policies"]["aspath_lists"]) { + if (list.count("name") && list["name"].asString() == name) { + return &list; + } + } + return nullptr; + } +}; + +TEST_F(ConfigBgpPolicyAsPathListTest, SetListDescriptionAndCommit) { + discardSession(); + clearBgpSession(); + stageAsPathList({kList, "description", "test", "spine", "as-paths"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + // Two layers: the promoted file proves what the commit wrote; the daemon's + // getRunningConfig RPC proves bgpd parsed and adopted the .policies blob. + auto config = readSystemBgpConfig(); + const auto* list = findList(config, kList); + ASSERT_NE(list, nullptr) << "committed config has no as-path-list " << kList; + EXPECT_EQ((*list)["description"].asString(), "test spine as-paths"); + + auto running = readRunningBgpConfigViaRpc(); + const auto* runningList = findList(running, kList); + ASSERT_NE(runningList, nullptr) + << "bgpd's running config has no as-path-list " << kList; + EXPECT_EQ((*runningList)["description"].asString(), "test spine as-paths"); +} + +TEST_F(ConfigBgpPolicyAsPathListTest, DeleteListAndCommit) { + // Land an as-path-list in the system config, then delete it through a second + // commit and verify it is gone from bgpd's running config. + discardSession(); + clearBgpSession(); + stageAsPathList({kList, "description", "delete-me"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + ASSERT_NE(findList(readRunningBgpConfigViaRpc(), kList), nullptr) + << "setup commit did not land the as-path-list in bgpd's running config"; + + clearBgpSession(); + auto result = + runCli({"delete", "protocol", "bgp", "policy", "as-path-list", kList}); + EXPECT_THAT(result.stdout, HasSubstr("Successfully deleted")); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after delete commit; state=" + << bgpDaemonActiveState(); + EXPECT_EQ(findList(readRunningBgpConfigViaRpc(), kList), nullptr) + << "deleted as-path-list still present in bgpd's running config"; +} From ac2db811c5b0b019f19ecdf354d81c4ccce5a4a4 Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Tue, 15 Sep 2026 12:13:27 +0000 Subject: [PATCH 16/22] Guard as-path-list delete, add regex/boolean-operator, delete one regex Deleting an as-path-list that a routing-policy term still names committed a dangling as_path_list_names reference; bgpd fails the config load on it and crash-loops. The whole-list delete now refuses and names every referencing `policy

term `, the same rule the peer-group delete applies to neighbors. findTermsReferencingAsPathList lives in BgpAsPathListCliUtils.h because the boolean-operator setter uses the same walk. The list gained the two fields bgpd actually matches on: `regex ` appends one boost::regex to as_paths (compiled here so a pattern bgpd would reject at load never reaches the session; whitespace is refused since bgpd renders the AS path `_`-joined), and `boolean-operator ` (NOT is not offered: bgpd treats anything but OR as AND). The operator is written through to every referencing term's inline copy, which bgpd requires to agree with the list ("Conflicting boolean_operator"). `delete ... as-path-list regex ` removes a single pattern and is allowed on a referenced list. Adds boost_regex to the fboss2 config lib (BUCK: boost_regex; cmake: Boost::regex, regex component) and the unit/integration coverage for each path. Co-Authored-By: Claude Fable 5.1 --- CMakeLists.txt | 2 +- cmake/CliFboss2.cmake | 1 + fboss/cli/fboss2/BUCK | 3 + fboss/cli/fboss2/CmdListConfig.cpp | 6 +- .../as-path-list/BgpAsPathListCliUtils.h | 65 ++++++++ .../CmdConfigProtocolBgpPolicyAsPathList.cpp | 86 ++++++++++ .../CmdConfigProtocolBgpPolicyAsPathList.h | 5 + .../CmdDeleteProtocolBgpPolicyAsPathList.cpp | 75 ++++++++- .../CmdDeleteProtocolBgpPolicyAsPathList.h | 18 ++- .../CmdConfigBgpPolicyAsPathListTest.cpp | 127 +++++++++++++++ .../CmdDeleteBgpPolicyAsPathListTest.cpp | 152 +++++++++++++++++- .../ConfigBgpPolicyAsPathListTest.cpp | 44 +++++ 12 files changed, 572 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d3a6eebae23c9..e27b0e537830b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -291,7 +291,7 @@ find_package(Glog REQUIRED) include_directories(${GLOG_INCLUDE_DIRS}) find_package(folly CONFIG REQUIRED) include_directories(${FOLLY_INCLUDE_DIR}) -find_package(Boost REQUIRED COMPONENTS container filesystem) +find_package(Boost REQUIRED COMPONENTS container filesystem regex) include_directories(${Boost_INCLUDE_DIR}) find_package(fb303 CONFIG REQUIRED) find_package(fizz CONFIG REQUIRED) diff --git a/cmake/CliFboss2.cmake b/cmake/CliFboss2.cmake index 81c64ceb5cfba..aef3dfb7fd62d 100644 --- a/cmake/CliFboss2.cmake +++ b/cmake/CliFboss2.cmake @@ -1135,6 +1135,7 @@ target_link_libraries(fboss2_config_lib bgp_config_cpp2 switchinfo_utils platform_mapping + Boost::regex Folly::folly ) diff --git a/fboss/cli/fboss2/BUCK b/fboss/cli/fboss2/BUCK index f42fe1babc02a..ba769b47b86d9 100644 --- a/fboss/cli/fboss2/BUCK +++ b/fboss/cli/fboss2/BUCK @@ -1427,6 +1427,9 @@ cpp_library( "//folly/json:dynamic", "//thrift/lib/cpp2:field_ref", ], + external_deps = [ + ("boost", None, "boost_regex"), + ], ) export_file( diff --git a/fboss/cli/fboss2/CmdListConfig.cpp b/fboss/cli/fboss2/CmdListConfig.cpp index f2605de922b51..71554cdddd780 100644 --- a/fboss/cli/fboss2/CmdListConfig.cpp +++ b/fboss/cli/fboss2/CmdListConfig.cpp @@ -399,7 +399,8 @@ const CommandTree& kConfigCommandTree() { {{ "as-path-list", "Configure BGP AS-path list: " - "[ ...] (description)", + "[ ...] (description, " + "regex, boolean-operator)", commandHandler< CmdConfigProtocolBgpPolicyAsPathList>, argRegistrar< @@ -755,7 +756,8 @@ const CommandTree& kConfigCommandTree() { argTypeHandler, {{ "as-path-list", - "Delete a BGP AS-path list: ", + "Delete a BGP AS-path list, or one of its regexes: " + " [regex ]", commandHandler, argRegistrar< CmdDeleteProtocolBgpPolicyAsPathListTraits>, diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h index 5c627006a629f..cb0f40eb5a0b2 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h @@ -10,10 +10,12 @@ #pragma once +#include #include #include #include #include +#include #include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" /** @@ -84,4 +86,67 @@ inline bgp::bgp_policy::AsPathListEntry& findOrCreateAsPathListEntry( return entry; } +inline const bgp::bgp_policy::AsPathList* findAsPathList( + const bgp::thrift::BgpConfig& cfg, + const std::string& name) { + if (!cfg.policies().has_value()) { + return nullptr; + } + const auto& lists = *cfg.policies()->aspath_lists(); + auto it = std::find_if(lists.begin(), lists.end(), [&](const auto& list) { + return *list.name() == name; + }); + return it == lists.end() ? nullptr : &*it; +} + +// A routing-policy term whose AS_PATH match names an as-path-list. `label` +// identifies the term the way the CLI addresses it (`policy

term `; +// the term name when it has no sequence number) so a rejection can be acted +// on directly. +struct AsPathListReference { + bgp::bgp_policy::BgpPolicyAtomicMatch* match; + std::string label; +}; + +// Every AS_PATH match whose as_path_filters.as_path_list_names carries +// `listName`. bgpd resolves those names at config load and fails the load on +// a dangling one, so the delete command refuses while this is non-empty, and +// the boolean-operator setter writes through to each match (bgpd requires the +// referencing copy to carry the same operator as the list). +inline std::vector findTermsReferencingAsPathList( + bgp::thrift::BgpConfig& cfg, + const std::string& listName) { + std::vector refs; + if (!cfg.policies().has_value()) { + return refs; + } + for (auto& policy : *cfg.policies()->bgp_policy_statements()) { + for (auto& term : *policy.policy_entries()) { + if (!term.policy_match_entries().has_value()) { + continue; + } + for (auto& match : *term.policy_match_entries()->match_entries()) { + if (!match.as_path_filters().has_value() || + !match.as_path_filters()->as_path_list_names().has_value()) { + continue; + } + const auto& names = *match.as_path_filters()->as_path_list_names(); + if (std::find(names.begin(), names.end(), listName) == names.end()) { + continue; + } + refs.push_back( + {&match, + term.sequence_number().has_value() + ? fmt::format( + "policy {} term {}", + *policy.name(), + *term.sequence_number()) + : fmt::format( + "policy {} term {}", *policy.name(), *term.name())}); + } + } + } + return refs; +} + } // namespace facebook::fboss::bgpcli diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.cpp index e4c6edbc7c0ec..4392e824d916d 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.cpp +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.cpp @@ -12,17 +12,22 @@ #include "fboss/cli/fboss2/CmdHandler.cpp" +#include #include #include +#include +#include #include #include #include +#include #include #include #include #include #include #include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/routing_policy_types.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliAttrHandlers.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h" #include "fboss/cli/fboss2/session/ConfigSession.h" @@ -37,31 +42,102 @@ namespace { // The attribute names, exactly as documented. Kept here so the // valid-attribute set and the handler table stay in sync. The list's entries // are their own subcommand, not attributes. +constexpr std::string_view kBooleanOperator = "boolean-operator"; constexpr std::string_view kDescription = "description"; +constexpr std::string_view kRegex = "regex"; + +constexpr std::string_view kBooleanOperatorAnd = "AND"; +constexpr std::string_view kBooleanOperatorOr = "OR"; using AsPathList = bgp::bgp_policy::AsPathList; +using BooleanOperator = bgp::routing_policy::BooleanOperator; using bgpcli::AttrHandler; +using bgpcli::enumAttr; +using bgpcli::err; using bgpcli::joinedStringAttr; using bgpcli::ok; using bgpcli::Result; +using bgpcli::Tokens; + +// NOT is deliberately not offered: bgpd treats every operator other than OR +// as "all must match", so NOT would silently behave as AND. +std::optional lookupBooleanOperator(const std::string& s) { + if (s == kBooleanOperatorAnd) { + return BooleanOperator::AND; + } + if (s == kBooleanOperatorOr) { + return BooleanOperator::OR; + } + return std::nullopt; +} // ---- setters ---------------------------------------------------------------- // Each writes one already-parsed, already-validated value. Parsing and message // text belong to the shared factories in BgpCliAttrHandlers.h. +void setListBooleanOperator(AsPathList& list, BooleanOperator op) { + list.boolean_operator() = op; +} + void setListDescription(AsPathList& list, const std::string& description) { list.description() = description; } +// Hand-written because no factory covers an accumulating, validated set: +// each `regex` appends one boost::regex to as_paths (the field bgpd matches +// against the `_`-joined AS path), compiled here so a pattern bgpd would +// reject at load never reaches the session. Idempotent on repeats. +Result regex(AsPathList& list, const Tokens& values) { + if (values.size() != 1 || values[0].empty()) { + return err( + fmt::format( + "Error: {} requires (one token; join AS numbers with `_`, " + "e.g. ^65000_65001$)", + kRegex)); + } + const auto& pattern = values[0]; + if (std::any_of(pattern.begin(), pattern.end(), [](unsigned char c) { + return std::isspace(c) != 0; + })) { + return err( + fmt::format( + "Error: {} '{}' must not contain whitespace; bgpd renders the AS " + "path with `_` between AS numbers", + kRegex, + pattern)); + } + try { + boost::regex compiled(pattern); + } catch (const boost::regex_error& e) { + return err( + fmt::format("Error: Malformed {} '{}': {}", kRegex, pattern, e.what())); + } + auto& paths = list.as_paths().ensure(); + if (std::find(paths.begin(), paths.end(), pattern) != paths.end()) { + return ok(fmt::format("{} {} already present", kRegex, pattern)); + } + paths.push_back(pattern); + return ok(fmt::format("Successfully added {} {}", kRegex, pattern)); +} + // ---- list-level attribute registry ------------------------------------------ // One line per documented attribute: its dispatch key, its value shape, and // the setter that stores it. const std::map, std::less<>>& listAttrHandlers() { + static const std::string kBooleanOperatorValues = + fmt::format("{}|{}", kBooleanOperatorAnd, kBooleanOperatorOr); static const std::map, std::less<>> kHandlers = { + {std::string(kBooleanOperator), + enumAttr( + kBooleanOperator, + kBooleanOperatorValues, + lookupBooleanOperator, + setListBooleanOperator)}, {std::string(kDescription), joinedStringAttr(kDescription, setListDescription)}, + {std::string(kRegex), regex}, }; return kHandlers; } @@ -134,6 +210,16 @@ CmdConfigProtocolBgpPolicyAsPathList::queryClient( if (!args.attr().empty()) { result.message += fmt::format(" for as-path-list {}", args.listName()); } + if (args.attr() == kBooleanOperator) { + // bgpd rejects a term whose inline copy of the list carries a different + // operator ("Conflicting boolean_operator"), so keep every referencing + // match in step with the list. + for (auto& ref : + bgpcli::findTermsReferencingAsPathList(cfg, args.listName())) { + ref.match->as_path_filters()->boolean_operator() = + *list.boolean_operator(); + } + } session.saveBgpConfig(); result.message += fmt::format("\nConfig saved to: {}", session.getBgpSessionConfigPath()); diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h index 7fe25d5f548de..c3f574ae0693e 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h @@ -27,6 +27,11 @@ namespace facebook::fboss { // Grammar (from the FBOSS proposed syntax): // as-path-list (create/select list) // as-path-list description (list attribute) +// as-path-list regex (append to as_paths, the +// field bgpd matches on) +// as-path-list boolean-operator (also written through to +// every term referencing +// the list) class BgpAsPathListConfig : public utils::BaseObjectArgType { public: // NOLINTNEXTLINE(google-explicit-constructor) diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.cpp b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.cpp index b9f3bfb68dab5..9b025078f29dd 100644 --- a/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.cpp +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.cpp @@ -13,14 +13,17 @@ #include "fboss/cli/fboss2/CmdHandler.cpp" #include +#include #include #include #include #include #include #include +#include #include #include +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h" #include "fboss/cli/fboss2/session/ConfigSession.h" #include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" #include "fboss/cli/fboss2/utils/HostInfo.h" @@ -28,18 +31,41 @@ namespace facebook::fboss { +namespace { +constexpr std::string_view kRegexKeyword = "regex"; +} // namespace + // Parse + validate at construction so queryClient stays a thin dispatch. BgpAsPathListRef::BgpAsPathListRef(std::vector v) : utils::BaseObjectArgType(v) { - if (v.size() != 1) { + if (v.empty()) { throw std::invalid_argument( - "Error: delete protocol bgp policy as-path-list requires exactly one " - ""); + "Error: delete protocol bgp policy as-path-list requires , " + "optionally followed by `regex `"); } if (v[0].empty()) { throw std::invalid_argument("Error: as-path-list name must not be empty"); } - listName_ = std::move(v[0]); + listName_ = v[0]; + if (v.size() == 1) { + return; + } + if (v[1] != kRegexKeyword) { + throw std::invalid_argument( + fmt::format( + "Error: unexpected token '{}'. Usage: delete protocol bgp policy " + "as-path-list [regex ]", + v[1])); + } + if (v.size() < 3 || v[2].empty()) { + throw std::invalid_argument("Error: `regex` requires a "); + } + if (v.size() > 3) { + throw std::invalid_argument( + fmt::format("Error: unexpected token '{}' after regex ", v[3])); + } + hasRegex_ = true; + regex_ = v[2]; } CmdDeleteProtocolBgpPolicyAsPathListTraits::RetType @@ -60,6 +86,47 @@ CmdDeleteProtocolBgpPolicyAsPathList::queryClient( if (it == lists.end()) { return fmt::format("Error: BGP as-path-list {} not found", args.listName()); } + if (args.hasRegex()) { + // Removing one pattern from a referenced list is safe: the name the terms + // resolve stays defined. + auto& list = *it; + if (list.as_paths().has_value()) { + auto& paths = *list.as_paths(); + auto pathIt = std::find(paths.begin(), paths.end(), args.regex()); + if (pathIt != paths.end()) { + paths.erase(pathIt); + if (paths.empty()) { + list.as_paths().reset(); + } + session.saveBgpConfig(); + return fmt::format( + "Successfully deleted BGP as-path-list {} regex {}\n" + "Config saved to: {}", + args.listName(), + args.regex(), + session.getBgpSessionConfigPath()); + } + } + return fmt::format( + "Error: BGP as-path-list {} regex {} not found", + args.listName(), + args.regex()); + } + // A term's as_path_list_names resolves against this list by name at daemon + // load; erasing the list while a term still names it would commit a + // dangling reference and fail the load. Refuse and name the terms instead. + std::vector referrers; + for (const auto& ref : + bgpcli::findTermsReferencingAsPathList(cfg, args.listName())) { + referrers.push_back(ref.label); + } + if (!referrers.empty()) { + return fmt::format( + "Error: BGP as-path-list {} is still referenced by {}; remove those " + "matches first", + args.listName(), + folly::join(", ", referrers)); + } lists.erase(it); session.saveBgpConfig(); return fmt::format( diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h index 4249bdd3546a4..874460157ef52 100644 --- a/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h @@ -20,9 +20,11 @@ namespace facebook::fboss { -// Parsed `delete protocol bgp policy as-path-list `, validated at -// construction. The name is the list's identity, matched exactly the same way -// the config command stores it. Mirrors BgpPeerGroupRef. +// Parsed `delete protocol bgp policy as-path-list [regex ]`, +// validated at construction. The name is the list's identity, matched exactly +// the same way the config command stores it; the optional `regex` selector +// removes one as_paths pattern instead of the whole list. Mirrors +// BgpPeerGroupRef. class BgpAsPathListRef : public utils::BaseObjectArgType { public: // NOLINTNEXTLINE(google-explicit-constructor) @@ -30,17 +32,25 @@ class BgpAsPathListRef : public utils::BaseObjectArgType { const std::string& listName() const { return listName_; } + bool hasRegex() const { + return hasRegex_; + } + const std::string& regex() const { + return regex_; + } const static utils::ObjectArgTypeId id = utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_MESSAGE; private: std::string listName_; + bool hasRegex_{false}; + std::string regex_; }; struct CmdDeleteProtocolBgpPolicyAsPathListTraits : public WriteCommandTraits { using ParentCmd = CmdDeleteProtocolBgpPolicy; static void addCliArg(CLI::App& cmd, std::vector& args) { - cmd.add_option("args", args, ""); + cmd.add_option("args", args, " [regex ]"); } using ObjectArgType = BgpAsPathListRef; using RetType = std::string; diff --git a/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListTest.cpp b/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListTest.cpp index c0e96f09a023d..2d6ef6bc4fa16 100644 --- a/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListTest.cpp +++ b/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListTest.cpp @@ -10,6 +10,8 @@ #include #include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/routing_policy_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h" #include "fboss/cli/fboss2/session/ConfigSession.h" #include "fboss/cli/fboss2/test/config/CmdConfigTestBase.h" @@ -47,6 +49,30 @@ class CmdConfigBgpPolicyAsPathListTestFixture : public CmdConfigTestBase { .aspath_lists(); } + // Seed a routing-policy term whose AS_PATH match names `listName` (the + // term/match CLIs live in higher PRs), the way bgpd reads the reference: + // as_path_filters.as_path_list_names. Returns the seeded atomic match. + bgp::bgp_policy::BgpPolicyAtomicMatch& addPolicyTermMatching( + const std::string& policy, + int64_t seq, + const std::string& listName) { + auto& cfg = ConfigSession::getInstance().getBgpConfig(); + auto& policies = *cfg.policies().ensure().bgp_policy_statements(); + policies.emplace_back(); + policies.back().name() = policy; + auto& terms = *policies.back().policy_entries(); + terms.emplace_back(); + terms.back().sequence_number() = seq; + auto& matches = + *terms.back().policy_match_entries().ensure().match_entries(); + matches.emplace_back(); + matches.back().type() = bgp::bgp_policy::BgpPolicyAtomicMatchType::AS_PATH; + matches.back().as_path_filters().ensure().as_path_list_names().ensure() = { + listName}; + ConfigSession::getInstance().saveBgpConfig(); + return matches.back(); + } + bool sessionFileExists() { return std::filesystem::exists( ConfigSession::getInstance().getBgpSessionConfigPath()); @@ -78,6 +104,12 @@ TEST_F(CmdConfigBgpPolicyAsPathListTestFixture, argValidation) { EXPECT_THROW( BgpAsPathListConfig({"AS100", "asn-regexp", "^65000_"}), std::invalid_argument); + + // List-level regex and boolean-operator are recognized. + EXPECT_EQ(BgpAsPathListConfig({"AS100", "regex", "^65000_"}).attr(), "regex"); + EXPECT_EQ( + BgpAsPathListConfig({"AS100", "boolean-operator", "AND"}).attr(), + "boolean-operator"); } // ============================================================================== @@ -134,4 +166,99 @@ TEST_F(CmdConfigBgpPolicyAsPathListTestFixture, unknownAttributeRejected) { << "session file should not exist after rejected input"; } +// ============================================================================== +// regex — as_paths, the field bgpd matches against +// ============================================================================== + +TEST_F(CmdConfigBgpPolicyAsPathListTestFixture, regexAppends) { + auto result = run({"AS100", "regex", "^65000_"}); + EXPECT_THAT(result, HasSubstr("Successfully added regex ^65000_")); + EXPECT_THAT(result, HasSubstr("for as-path-list AS100")); + run({"AS100", "regex", "_65001$"}); + ASSERT_EQ(lists().size(), 1); + ASSERT_TRUE(lists()[0].as_paths().has_value()); + EXPECT_EQ( + *lists()[0].as_paths(), std::vector({"^65000_", "_65001$"})); + // The dead entry list is left alone. + EXPECT_TRUE(lists()[0].as_path_list()->empty()); + EXPECT_TRUE(sessionFileExists()); +} + +TEST_F(CmdConfigBgpPolicyAsPathListTestFixture, regexAddAgainIsIdempotent) { + run({"AS100", "regex", "^65000_"}); + auto result = run({"AS100", "regex", "^65000_"}); + EXPECT_THAT(result, HasSubstr("already present")); + ASSERT_EQ(lists().size(), 1); + EXPECT_EQ(lists()[0].as_paths()->size(), 1); +} + +TEST_F(CmdConfigBgpPolicyAsPathListTestFixture, malformedRegexRejected) { + auto result = run({"AS100", "regex", "^65000_("}); + EXPECT_THAT(result, HasSubstr("Error: Malformed regex")); + // A rejected first attribute leaves no phantom list and nothing on disk. + EXPECT_TRUE(lists().empty()); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +TEST_F(CmdConfigBgpPolicyAsPathListTestFixture, regexWithWhitespaceRejected) { + run({"AS100"}); + auto result = run({"AS100", "regex", "65000 65001"}); + EXPECT_THAT(result, HasSubstr("must not contain whitespace")); + EXPECT_THAT(result, HasSubstr("_")); + ASSERT_EQ(lists().size(), 1); + EXPECT_FALSE(lists()[0].as_paths().has_value()); + + EXPECT_THAT(run({"AS100", "regex"}), HasSubstr("requires ")); + EXPECT_THAT( + run({"AS100", "regex", "^65000_", "_65001$"}), + HasSubstr("requires ")); +} + +// ============================================================================== +// boolean-operator — AND|OR, written through to referencing terms +// ============================================================================== + +TEST_F(CmdConfigBgpPolicyAsPathListTestFixture, booleanOperatorAccepted) { + auto result = run({"AS100", "boolean-operator", "AND"}); + EXPECT_THAT(result, HasSubstr("Successfully set boolean-operator to: AND")); + ASSERT_EQ(lists().size(), 1); + EXPECT_EQ( + *lists()[0].boolean_operator(), + bgp::routing_policy::BooleanOperator::AND); + run({"AS100", "boolean-operator", "OR"}); + EXPECT_EQ( + *lists()[0].boolean_operator(), bgp::routing_policy::BooleanOperator::OR); +} + +TEST_F(CmdConfigBgpPolicyAsPathListTestFixture, booleanOperatorNotRejected) { + auto result = run({"AS100", "boolean-operator", "NOT"}); + EXPECT_THAT(result, HasSubstr("Invalid boolean-operator value 'NOT'")); + EXPECT_THAT(result, HasSubstr("AND|OR")); + EXPECT_TRUE(lists().empty()); + EXPECT_FALSE(sessionFileExists()); +} + +TEST_F( + CmdConfigBgpPolicyAsPathListTestFixture, + booleanOperatorPropagatesToReferencingTerms) { + run({"AS100"}); + run({"AS200"}); + const auto& match = addPolicyTermMatching("RM100", 10, "AS100"); + const auto& other = addPolicyTermMatching("RM100", 20, "AS200"); + EXPECT_EQ( + *match.as_path_filters()->boolean_operator(), + bgp::routing_policy::BooleanOperator::OR); + + run({"AS100", "boolean-operator", "AND"}); + // bgpd requires the term's inline copy to carry the list's operator. + EXPECT_EQ( + *match.as_path_filters()->boolean_operator(), + bgp::routing_policy::BooleanOperator::AND); + // A term referencing a different list is untouched. + EXPECT_EQ( + *other.as_path_filters()->boolean_operator(), + bgp::routing_policy::BooleanOperator::OR); +} + } // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyAsPathListTest.cpp b/fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyAsPathListTest.cpp index 7c575a4dd51e0..a8337a0ba9a6c 100644 --- a/fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyAsPathListTest.cpp +++ b/fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyAsPathListTest.cpp @@ -4,7 +4,9 @@ #include #include +#include #include +#include #include #include #include @@ -54,6 +56,35 @@ class CmdDeleteBgpPolicyAsPathListTestFixture : public CmdConfigTestBase { .aspath_lists(); } + // Seed a routing-policy term whose AS_PATH match names `listName` (the + // term/match CLIs live in higher PRs), the way bgpd reads the reference: + // as_path_filters.as_path_list_names. + void addPolicyTermMatching( + const std::string& policy, + int64_t seq, + const std::string& listName) { + auto& cfg = ConfigSession::getInstance().getBgpConfig(); + auto& policies = *cfg.policies().ensure().bgp_policy_statements(); + auto it = std::find_if(policies.begin(), policies.end(), [&](auto& p) { + return *p.name() == policy; + }); + if (it == policies.end()) { + policies.emplace_back(); + policies.back().name() = policy; + it = std::prev(policies.end()); + } + auto& terms = *it->policy_entries(); + terms.emplace_back(); + terms.back().sequence_number() = seq; + auto& matches = + *terms.back().policy_match_entries().ensure().match_entries(); + matches.emplace_back(); + matches.back().type() = bgp::bgp_policy::BgpPolicyAtomicMatchType::AS_PATH; + matches.back().as_path_filters().ensure().as_path_list_names().ensure() = { + listName}; + ConfigSession::getInstance().saveBgpConfig(); + } + bool sessionFileExists() { return std::filesystem::exists( ConfigSession::getInstance().getBgpSessionConfigPath()); @@ -66,11 +97,23 @@ class CmdDeleteBgpPolicyAsPathListTestFixture : public CmdConfigTestBase { TEST_F(CmdDeleteBgpPolicyAsPathListTestFixture, argValidation) { EXPECT_EQ(BgpAsPathListRef({"AS100"}).listName(), "AS100"); + EXPECT_FALSE(BgpAsPathListRef({"AS100"}).hasRegex()); - // Invalid: empty, empty name, extra tokens. + // Optional `regex ` selector. + auto withRegex = BgpAsPathListRef({"AS100", "regex", "^65000_"}); + EXPECT_EQ(withRegex.listName(), "AS100"); + EXPECT_TRUE(withRegex.hasRegex()); + EXPECT_EQ(withRegex.regex(), "^65000_"); + + // Invalid: empty, empty name, extra tokens, incomplete or trailing selector. EXPECT_THROW(BgpAsPathListRef({}), std::invalid_argument); EXPECT_THROW(BgpAsPathListRef({""}), std::invalid_argument); EXPECT_THROW(BgpAsPathListRef({"AS100", "AS200"}), std::invalid_argument); + EXPECT_THROW(BgpAsPathListRef({"AS100", "regex"}), std::invalid_argument); + EXPECT_THROW(BgpAsPathListRef({"AS100", "regex", ""}), std::invalid_argument); + EXPECT_THROW( + BgpAsPathListRef({"AS100", "regex", "^65000_", "extra"}), + std::invalid_argument); } // ============================================================================== @@ -110,4 +153,111 @@ TEST_F( EXPECT_EQ(*lists()[0].name(), "AS100"); } +// ============================================================================== +// Reference guard — a list a routing-policy term still names is not deletable +// ============================================================================== + +TEST_F(CmdDeleteBgpPolicyAsPathListTestFixture, deleteReferencedListRejected) { + configure({"AS100", "description", "in-use"}); + addPolicyTermMatching("RM100", 10, "AS100"); + + auto result = del({"AS100"}); + EXPECT_THAT(result, HasSubstr("still referenced")); + // The refusal names the policy and term so the user can act on it. + EXPECT_THAT(result, HasSubstr("policy RM100 term 10")); + EXPECT_THAT(result, HasSubstr("remove those matches first")); + ASSERT_EQ(lists().size(), 1); + EXPECT_EQ(*lists()[0].name(), "AS100"); + EXPECT_EQ(*lists()[0].description(), "in-use"); +} + +TEST_F( + CmdDeleteBgpPolicyAsPathListTestFixture, + deleteReferencedListNamesEveryReferrer) { + configure({"AS100"}); + addPolicyTermMatching("RM100", 10, "AS100"); + addPolicyTermMatching("RM200", 20, "AS100"); + + auto result = del({"AS100"}); + EXPECT_THAT(result, HasSubstr("policy RM100 term 10")); + EXPECT_THAT(result, HasSubstr("policy RM200 term 20")); + ASSERT_EQ(lists().size(), 1); +} + +TEST_F( + CmdDeleteBgpPolicyAsPathListTestFixture, + referenceToOtherListDoesNotBlockDelete) { + configure({"AS100"}); + configure({"AS200"}); + // A term referencing AS100 must not block deleting AS200. + addPolicyTermMatching("RM100", 10, "AS100"); + + auto result = del({"AS200"}); + EXPECT_THAT(result, HasSubstr("Successfully deleted BGP as-path-list AS200")); + ASSERT_EQ(lists().size(), 1); + EXPECT_EQ(*lists()[0].name(), "AS100"); +} + +TEST_F( + CmdDeleteBgpPolicyAsPathListTestFixture, + rejectedDeleteDoesNotStageWhenNothingStaged) { + // Seed the reference without going through the session file, then confirm + // the refused delete writes nothing. + auto& cfg = ConfigSession::getInstance().getBgpConfig(); + auto& list = cfg.policies().ensure().aspath_lists()->emplace_back(); + list.name() = "AS100"; + auto& policies = *cfg.policies()->bgp_policy_statements(); + policies.emplace_back(); + policies.back().name() = "RM100"; + auto& term = policies.back().policy_entries()->emplace_back(); + term.sequence_number() = 10; + auto& match = + term.policy_match_entries().ensure().match_entries()->emplace_back(); + match.type() = bgp::bgp_policy::BgpPolicyAtomicMatchType::AS_PATH; + match.as_path_filters().ensure().as_path_list_names().ensure() = {"AS100"}; + ASSERT_FALSE(sessionFileExists()); + + auto result = del({"AS100"}); + EXPECT_THAT(result, HasSubstr("still referenced")); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected delete"; +} + +// ============================================================================== +// regex selector — remove one pattern, keep the list +// ============================================================================== + +TEST_F(CmdDeleteBgpPolicyAsPathListTestFixture, deleteOneRegex) { + configure({"AS100", "regex", "^65000_"}); + configure({"AS100", "regex", "_65001$"}); + // Removing a pattern from a referenced list is fine: the name stays defined. + addPolicyTermMatching("RM100", 10, "AS100"); + + auto result = del({"AS100", "regex", "^65000_"}); + EXPECT_THAT( + result, + HasSubstr("Successfully deleted BGP as-path-list AS100 regex ^65000_")); + ASSERT_EQ(lists().size(), 1); + EXPECT_EQ(*lists()[0].as_paths(), std::vector({"_65001$"})); + + // Deleting the last pattern clears as_paths but keeps the list. + del({"AS100", "regex", "_65001$"}); + ASSERT_EQ(lists().size(), 1); + EXPECT_FALSE(lists()[0].as_paths().has_value()); +} + +TEST_F(CmdDeleteBgpPolicyAsPathListTestFixture, deleteUnknownRegexRejected) { + configure({"AS100", "regex", "^65000_"}); + auto result = del({"AS100", "regex", "^65002_"}); + EXPECT_THAT( + result, + HasSubstr("Error: BGP as-path-list AS100 regex ^65002_ not found")); + ASSERT_EQ(lists().size(), 1); + EXPECT_EQ(lists()[0].as_paths()->size(), 1); + + EXPECT_THAT( + del({"NO-SUCH-LIST", "regex", "^65000_"}), + HasSubstr("Error: BGP as-path-list NO-SUCH-LIST not found")); +} + } // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListTest.cpp index 62fb1bc07cb11..7597b59716247 100644 --- a/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListTest.cpp +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListTest.cpp @@ -20,6 +20,7 @@ */ #include +#include #include #include @@ -92,6 +93,49 @@ TEST_F(ConfigBgpPolicyAsPathListTest, SetListDescriptionAndCommit) { EXPECT_EQ((*runningList)["description"].asString(), "test spine as-paths"); } +TEST_F(ConfigBgpPolicyAsPathListTest, SetRegexAndOperatorAndCommit) { + discardSession(); + clearBgpSession(); + stageAsPathList({kList, "regex", "^65000_"}); + stageAsPathList({kList, "regex", "_65001$"}); + stageAsPathList({kList, "boolean-operator", "AND"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + // bgpd matches on as_paths + boolean_operator (not the entry list); the + // running config proves it parsed and adopted both. + auto running = readRunningBgpConfigViaRpc(); + const auto* list = findList(running, kList); + ASSERT_NE(list, nullptr) << "bgpd's running config has no as-path-list " + << kList; + ASSERT_TRUE(list->count("as_paths")); + std::vector paths; + for (const auto& p : (*list)["as_paths"]) { + paths.push_back(p.asString()); + } + EXPECT_EQ(paths, std::vector({"^65000_", "_65001$"})); + ASSERT_TRUE(list->count("boolean_operator")); + // routing_policy.BooleanOperator.AND == 1 + EXPECT_EQ((*list)["boolean_operator"].asInt(), 1); +} + +TEST_F(ConfigBgpPolicyAsPathListTest, MalformedRegexRejected) { + clearBgpSession(); + auto result = runCli( + {"config", + "protocol", + "bgp", + "policy", + "as-path-list", + kList, + "regex", + "^65000_("}); + EXPECT_THAT(result.stdout, HasSubstr("Malformed regex")); + EXPECT_FALSE(std::filesystem::exists(bgpSessionPath())) + << "session file should not exist after rejected input"; +} + TEST_F(ConfigBgpPolicyAsPathListTest, DeleteListAndCommit) { // Land an as-path-list in the system config, then delete it through a second // commit and verify it is gone from bgpd's running config. From 523fed8523bdac56d6b7dd332c76fb5dfd85e7e7 Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Tue, 15 Sep 2026 12:13:48 +0000 Subject: [PATCH 17/22] Emit as-path-list commands from bgp_json_to_cli.py bgp_json_to_cli.py rebuilt global, peer-group and peer settings only; every policy object in the input was dropped, so a replayed script could commit peers naming policies that no longer exist. This adds generate_as_path_list_commands (description, one `regex` line per as_paths pattern, `boolean-operator` when not the OR default) and a generate_policy_commands block that the nested families hook into. `--raw` and the script wrapper now share one generate_commands sequence: global, policy objects, then the peer-groups and peers that reference them, so the emitted order never stages a dangling name. Fields the CLI cannot express (as_path_list_names, the unread as_path_list entries, NOT) are emitted as `# WARNING:` comment lines, which the script wrapper passes through unprefixed instead of silently dropping the data. Co-Authored-By: Claude Fable 5.1 --- fboss/cli/fboss2/tools/bgp_json_to_cli.py | 86 +++++++++++ .../tools/tests/test_bgp_json_to_cli.py | 136 ++++++++++++++++++ 2 files changed, 222 insertions(+) diff --git a/fboss/cli/fboss2/tools/bgp_json_to_cli.py b/fboss/cli/fboss2/tools/bgp_json_to_cli.py index 82862060984e3..36ccf7909d360 100644 --- a/fboss/cli/fboss2/tools/bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/bgp_json_to_cli.py @@ -411,6 +411,90 @@ def generate_peer_group_commands(peer_group: dict[str, Any]) -> list[str]: return commands +# Emitted verbatim (never prefixed with the binary) for JSON the CLI cannot +# express, so a replayed script neither fails nor silently drops the field. +_WARNING_PREFIX = "# WARNING:" + + +def _warning(text: str) -> str: + return f"{_WARNING_PREFIX} {text}" + + +_BOOLEAN_OPERATOR_NAMES = {1: "AND", 2: "OR", 3: "NOT"} + + +def _boolean_operator_name(raw: Any) -> str: + """routing_policy.BooleanOperator as its name, from the int or the name.""" + if isinstance(raw, str): + return raw + return _BOOLEAN_OPERATOR_NAMES.get(int(raw), str(raw)) + + +def generate_as_path_list_commands(as_path_list: dict[str, Any]) -> list[str]: + """Generate `config protocol bgp policy as-path-list` commands for one list. + + bgpd matches on `as_paths` (one `regex` line each) and `boolean_operator` + (emitted only when it differs from the OR default). `as_path_list_names` + and the `as_path_list` entries are never read by bgpd and have no CLI + spelling here, so they surface as warnings rather than vanishing. + """ + name = as_path_list.get("name", "") + if not name: + return [] + + prefix = f"config protocol bgp policy as-path-list {escape_shell_arg(name)}" + commands = [] + if as_path_list.get("description"): + commands.append( + f"{prefix} description {escape_shell_arg(as_path_list['description'])}" + ) + for regex in as_path_list.get("as_paths") or []: + commands.append(f"{prefix} regex {escape_shell_arg(regex)}") + if "boolean_operator" in as_path_list: + operator = _boolean_operator_name(as_path_list["boolean_operator"]) + if operator == "NOT": + commands.append( + _warning( + f"as-path-list {name}: boolean_operator NOT is not " + "accepted by the CLI (bgpd treats it as AND); not emitted" + ) + ) + elif operator != "OR": + commands.append(f"{prefix} boolean-operator {escape_shell_arg(operator)}") + if as_path_list.get("as_path_list_names"): + commands.append( + _warning( + f"as-path-list {name}: as_path_list_names is not read by bgpd " + "and has no CLI equivalent; not emitted" + ) + ) + if as_path_list.get("as_path_list"): + commands.append( + _warning( + f"as-path-list {name}: as_path_list entries are not read by " + "bgpd (bgpd matches as_paths); not emitted" + ) + ) + if not commands: + # Nothing to set: still recreate the (empty) list by name. + commands.append(prefix) + return commands + + +def generate_policy_commands(config: dict[str, Any]) -> list[str]: + """Generate the `config protocol bgp policy ...` commands. + + Lists come before the routing-policies that reference them, and the whole + block precedes the peer-group/peer commands that name a policy, so a + replayed script never stages a dangling reference. + """ + policies = config.get("policies", {}) + commands = [] + for as_path_list in policies.get("aspath_lists", []): + commands.extend(generate_as_path_list_commands(as_path_list)) + return commands + + def generate_exec_commands(commands: list[str], binary: str = "fboss2") -> list[str]: """Generate executable commands with custom binary.""" exec_commands = [ @@ -440,6 +524,8 @@ def generate_commands(config: dict[str, Any]) -> list[str]: for fields that have no CLI equivalent).""" commands = [] commands.extend(generate_global_commands(config)) + # Policy objects before the peer-group/peer lines that reference them. + commands.extend(generate_policy_commands(config)) for peer_group in config.get("peer_groups", []): commands.extend(generate_peer_group_commands(peer_group)) for peer in config.get("peers", []): diff --git a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py index 261a4a079560b..4262d4a523cfe 100644 --- a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py @@ -20,11 +20,13 @@ from fboss.cli.fboss2.tools.bgp_json_to_cli import ( escape_shell_arg, format_bandwidth, + generate_as_path_list_commands, generate_commands, generate_exec_commands, generate_global_commands, generate_peer_commands, generate_peer_group_commands, + generate_policy_commands, json_to_cli, ) @@ -943,6 +945,100 @@ def peer_config(peer: dict) -> dict: return {"peers": [peer]} +class GenerateAsPathListCommandsTest(unittest.TestCase): + """Tests for generate_as_path_list_commands (as-path-list grammar).""" + + PREFIX = "config protocol bgp policy as-path-list ASPL" + + def test_empty_name_returns_empty(self) -> None: + self.assertEqual(generate_as_path_list_commands({"as_paths": ["^1_"]}), []) + + def test_bare_list_is_recreated(self) -> None: + """A list with nothing to set still comes back by name.""" + self.assertEqual( + generate_as_path_list_commands({"name": "ASPL"}), [self.PREFIX] + ) + + def test_description(self) -> None: + commands = generate_as_path_list_commands( + {"name": "ASPL", "description": "spine as paths"} + ) + self.assertEqual(commands, [f"{self.PREFIX} description 'spine as paths'"]) + + def test_empty_description_not_emitted(self) -> None: + """bgpd's running config carries the "" default; nothing to replay.""" + commands = generate_as_path_list_commands({"name": "ASPL", "description": ""}) + self.assertEqual(commands, [self.PREFIX]) + + def test_regex_per_as_path(self) -> None: + """Each as_paths pattern is one quoted `regex` line, in order.""" + commands = generate_as_path_list_commands( + {"name": "ASPL", "as_paths": ["^65000_", "_65001$"]} + ) + self.assertEqual( + commands, + [f"{self.PREFIX} regex '^65000_'", f"{self.PREFIX} regex '_65001$'"], + ) + + def test_boolean_operator_and(self) -> None: + for raw in (1, "AND"): + commands = generate_as_path_list_commands( + {"name": "ASPL", "boolean_operator": raw} + ) + self.assertEqual(commands, [f"{self.PREFIX} boolean-operator AND"], raw) + + def test_boolean_operator_or_is_default(self) -> None: + """OR is the thrift default and the CLI's; nothing to emit.""" + for raw in (2, "OR"): + commands = generate_as_path_list_commands( + {"name": "ASPL", "boolean_operator": raw} + ) + self.assertEqual(commands, [self.PREFIX], raw) + + def test_boolean_operator_not_warns(self) -> None: + commands = generate_as_path_list_commands( + {"name": "ASPL", "boolean_operator": 3} + ) + self.assertEqual(len(commands), 1) + self.assertTrue(commands[0].startswith("# WARNING:"), commands[0]) + self.assertIn("NOT", commands[0]) + + def test_dead_fields_warn(self) -> None: + """Fields bgpd never reads surface as warnings, not silently dropped.""" + commands = generate_as_path_list_commands( + { + "name": "ASPL", + "as_path_list_names": ["OTHER"], + "as_path_list": [{"sequence_number": 10}], + } + ) + self.assertEqual(len(commands), 2) + self.assertIn("as_path_list_names", commands[0]) + self.assertIn("as_path_list entries", commands[1]) + for c in commands: + self.assertTrue(c.startswith("# WARNING:"), c) + + def test_injection_in_regex_neutralized(self) -> None: + commands = generate_as_path_list_commands( + {"name": "ASPL", "as_paths": ["$(reboot)"]} + ) + self.assertEqual(commands, [f"{self.PREFIX} regex '$(reboot)'"]) + + def test_policy_block_orders_lists_first(self) -> None: + config = {"policies": {"aspath_lists": [{"name": "A"}, {"name": "B"}]}} + commands = generate_policy_commands(config) + self.assertEqual( + commands, + [ + "config protocol bgp policy as-path-list A", + "config protocol bgp policy as-path-list B", + ], + ) + + def test_no_policies_key(self) -> None: + self.assertEqual(generate_policy_commands({}), []) + + class GenerateScriptCommandsTest(unittest.TestCase): """Tests for generate_exec_commands.""" @@ -954,6 +1050,15 @@ def test_exec_commands_contains_header(self) -> None: # Check for set -e with any comment self.assertTrue(any("set -e" in cmd for cmd in result)) + def test_exec_commands_pass_warnings_through(self) -> None: + """Warning comments are never prefixed with the binary.""" + result = generate_exec_commands( + ["config protocol bgp global hold-time 90", "# WARNING: x"], "fboss2" + ) + self.assertIn("fboss2 config protocol bgp global hold-time 90", result) + self.assertIn("# WARNING: x", result) + self.assertNotIn("fboss2 # WARNING: x", result) + def test_exec_commands_prepends_binary(self) -> None: """Exec commands should prepend binary to each command.""" commands = ["config protocol bgp global router-id 10.0.0.1"] @@ -979,6 +1084,37 @@ def test_generate_commands_carries_warnings_in_raw_output(self) -> None: self.assertTrue(commands[0].startswith("# WARNING: neighbor 2001:db8::1")) +class GenerateCommandsOrderTest(unittest.TestCase): + """generate_commands is the single generator sequence for --raw and the + script: global, then policy objects, then the peer-groups and peers that + reference them.""" + + def test_policies_between_global_and_peer_groups(self) -> None: + config = { + "router_id": "10.0.0.1", + "policies": {"aspath_lists": [{"name": "ASPL", "as_paths": ["^1_"]}]}, + "peer_groups": [{"name": "PG", "remote_as_4_byte": 65000}], + "peers": [{"peer_addr": "10.0.0.2", "peer_group_name": "PG"}], + } + commands = generate_commands(config) + idx = { + "global": next(i for i, c in enumerate(commands) if "global" in c), + "policy": next(i for i, c in enumerate(commands) if "as-path-list" in c), + "peer_group": next( + i for i, c in enumerate(commands) if "peer-group PG" in c + ), + "peer": next(i for i, c in enumerate(commands) if "neighbor" in c), + } + self.assertLess(idx["global"], idx["policy"]) + self.assertLess(idx["policy"], idx["peer_group"]) + self.assertLess(idx["peer_group"], idx["peer"]) + + def test_script_wraps_same_sequence(self) -> None: + config = {"policies": {"aspath_lists": [{"name": "ASPL"}]}} + script = json_to_cli(config, binary="fboss2") + self.assertIn("fboss2 config protocol bgp policy as-path-list ASPL", script) + + class JsonToCliIntegrationTest(unittest.TestCase): """Integration tests for json_to_cli function.""" From 7f02eb3bc3cf2d843942da32f6c8412c9d8e9b38 Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Fri, 7 Aug 2026 18:48:09 +0000 Subject: [PATCH 18/22] [Nexthop] NOS-6643..NOS-6648: fboss2 bgp policy as-path-list entry subcommand Splits the `entry` level of `config protocol bgp policy as-path-list` into its own CLI11 subcommand. The previous commit ships the list level; this one ships everything keyed by `entry `: as-path-list entry -> AsPathList.as_path_list[], keyed by sequence_number as-path-list entry asn-regexp -> AsPathListEntry.as_path.as_path.asn_regexp as-path-list entry description -> AsPathListEntry.description as-path-list entry match-logic -> AsPathListEntry.match_logic_type - `entry` is a real CLI11 subcommand rather than tokens parsed inside the parent's arg type: the list name arrives through the ancestor-args tuple, and lookup/create is shared with the parent through BgpAsPathListCliUtils.h. Adding an entry attribute is a one-line registry change, same as at the list level. - A list attribute typed alongside `entry` is rejected rather than silently dropped, because only the leaf command runs. - asn-regexp sets the AsPathType union's inline AsPath arm. The pattern may contain spaces, since AS-path regexes separate ASNs with spaces (e.g. `^65000 65001$`), so it takes the joined-string value shape rather than requiring a single token. - A rejected value leaves nothing staged: a list or entry implicitly created for the failed command is rolled back. - unit tests (10): CmdConfigBgpPolicyAsPathListEntryTest covering arg validation, bare entry create, attribute round-trips, seq-num keying, asn-regexp accepting spaces, the match-logic default, and the rejection paths. - integration tests (2): ConfigBgpPolicyAsPathListEntryTest stages and commits each attribute, then reads it back out of bgpd's running config via the getRunningConfig RPC. Enums come back over SimpleJSON as integers, so match_logic_type is asserted as MatchValueLogicOperator's numeric value rather than its name. Co-Authored-By: Claude Fable 5 --- cmake/CliFboss2.cmake | 2 + cmake/CliFboss2TestConfig.cmake | 1 + cmake/CliFboss2TestIntegrationTest.cmake | 1 + fboss/cli/fboss2/BUCK | 2 + fboss/cli/fboss2/CmdListConfig.cpp | 11 + ...ConfigProtocolBgpPolicyAsPathListEntry.cpp | 229 ++++++++++++++++++ ...mdConfigProtocolBgpPolicyAsPathListEntry.h | 89 +++++++ fboss/cli/fboss2/test/config/BUCK | 1 + .../CmdConfigBgpPolicyAsPathListEntryTest.cpp | 210 ++++++++++++++++ fboss/cli/fboss2/test/integration_test/BUCK | 1 + .../ConfigBgpPolicyAsPathListEntryTest.cpp | 127 ++++++++++ 11 files changed, 674 insertions(+) create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.cpp create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.h create mode 100644 fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListEntryTest.cpp create mode 100644 fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListEntryTest.cpp diff --git a/cmake/CliFboss2.cmake b/cmake/CliFboss2.cmake index aef3dfb7fd62d..fef17069ed32c 100644 --- a/cmake/CliFboss2.cmake +++ b/cmake/CliFboss2.cmake @@ -956,6 +956,8 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.cpp fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h + fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.cpp + fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.h fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.cpp fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.h fboss/cli/fboss2/commands/config/ptp/transparent_clock/CmdConfigPtpTransparentClock.cpp diff --git a/cmake/CliFboss2TestConfig.cmake b/cmake/CliFboss2TestConfig.cmake index c1526444ca844..f4c531621f651 100644 --- a/cmake/CliFboss2TestConfig.cmake +++ b/cmake/CliFboss2TestConfig.cmake @@ -14,6 +14,7 @@ add_executable(fboss2_cmd_config_test fboss/cli/fboss2/test/config/CmdConfigBgpNeighborTest.cpp fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListTest.cpp + fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListEntryTest.cpp fboss/cli/fboss2/test/config/CmdConfigCoppTest.cpp fboss/cli/fboss2/test/config/CmdConfigDhcpTest.cpp fboss/cli/fboss2/test/config/CmdConfigHostnameTest.cpp diff --git a/cmake/CliFboss2TestIntegrationTest.cmake b/cmake/CliFboss2TestIntegrationTest.cmake index e0d174f42f197..1c8097b7612e8 100644 --- a/cmake/CliFboss2TestIntegrationTest.cmake +++ b/cmake/CliFboss2TestIntegrationTest.cmake @@ -17,6 +17,7 @@ add_executable(fboss2_integration_test fboss/cli/fboss2/test/integration_test/ConfigBgpNeighborTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListTest.cpp + fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListEntryTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp fboss/cli/fboss2/test/integration_test/ConfigConcurrentSessionsTest.cpp fboss/cli/fboss2/test/integration_test/ConfigHostnameTest.cpp diff --git a/fboss/cli/fboss2/BUCK b/fboss/cli/fboss2/BUCK index ba769b47b86d9..5161382e11a68 100644 --- a/fboss/cli/fboss2/BUCK +++ b/fboss/cli/fboss2/BUCK @@ -1168,6 +1168,7 @@ cpp_library( "commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.cpp", "commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.cpp", "commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.cpp", + "commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.cpp", "commands/config/protocol/static/CmdConfigProtocolStatic.cpp", "commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.cpp", "commands/config/ptp/CmdConfigPtp.cpp", @@ -1298,6 +1299,7 @@ cpp_library( "commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h", "commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h", "commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h", + "commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.h", "commands/config/protocol/static/CmdConfigProtocolStatic.h", "commands/config/protocol/static/route/StaticRouteUtils.h", "commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.h", diff --git a/fboss/cli/fboss2/CmdListConfig.cpp b/fboss/cli/fboss2/CmdListConfig.cpp index 71554cdddd780..0b5e3d3289aa5 100644 --- a/fboss/cli/fboss2/CmdListConfig.cpp +++ b/fboss/cli/fboss2/CmdListConfig.cpp @@ -43,6 +43,7 @@ #include "fboss/cli/fboss2/commands/config/protocol/bgp/peer-group/CmdConfigProtocolBgpPeerGroup.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.h" #include "fboss/cli/fboss2/commands/config/protocol/static/CmdConfigProtocolStatic.h" #include "fboss/cli/fboss2/commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.h" #include "fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.h" @@ -405,6 +406,16 @@ const CommandTree& kConfigCommandTree() { CmdConfigProtocolBgpPolicyAsPathList>, argRegistrar< CmdConfigProtocolBgpPolicyAsPathListTraits>, + {{ + "entry", + "Configure an AS-path list entry: " + " [ ...] " + "(asn-regexp, description, match-logic)", + commandHandler< + CmdConfigProtocolBgpPolicyAsPathListEntry>, + argRegistrar< + CmdConfigProtocolBgpPolicyAsPathListEntryTraits>, + }}, }}, }, }, diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.cpp new file mode 100644 index 0000000000000..846ffa28fe29a --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.cpp @@ -0,0 +1,229 @@ +/* + * 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/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.h" + +#include "fboss/cli/fboss2/CmdHandler.cpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/routing_policy_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliAttrHandlers.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" +#include "fmt/format.h" + +namespace facebook::fboss { + +namespace { + +// The attribute names, exactly as documented. Kept here so the +// valid-attribute set and the handler table stay in sync. +constexpr std::string_view kDescription = "description"; +constexpr std::string_view kAsnRegexp = "asn-regexp"; +constexpr std::string_view kMatchLogic = "match-logic"; + +// match-logic values (routing_policy.MatchValueLogicOperator names). +constexpr std::string_view kMatchLogicEqual = "EQUAL"; +constexpr std::string_view kMatchLogicNotEqual = "NOT_EQUAL"; + +using AsPathListEntry = bgp::bgp_policy::AsPathListEntry; +using MatchValueLogicOperator = bgp::routing_policy::MatchValueLogicOperator; +using bgpcli::AttrHandler; +using bgpcli::enumAttr; +using bgpcli::joinedStringAttr; +using bgpcli::ok; +using bgpcli::Result; + +// ---- value lookups ---------------------------------------------------------- + +std::optional lookupMatchLogic(const std::string& s) { + if (s == kMatchLogicEqual) { + return MatchValueLogicOperator::EQUAL; + } + if (s == kMatchLogicNotEqual) { + return MatchValueLogicOperator::NOT_EQUAL; + } + return std::nullopt; +} + +// ---- entry-level setters ---------------------------------------------------- +// Each writes one already-parsed, already-validated value. Parsing and message +// text belong to the shared factories in BgpCliAttrHandlers.h. + +void setDescription(AsPathListEntry& entry, const std::string& description) { + entry.description() = description; +} + +// The AS path is modelled as an AsPathType union; the CLI sets the inline +// `as_path` (AsPath) arm and its regexp, matching the thrift path +// .as_path.as_path.asn_regexp. +void setAsnRegexp(AsPathListEntry& entry, const std::string& regexp) { + entry.as_path()->set_as_path().asn_regexp() = regexp; +} + +void setMatchLogic(AsPathListEntry& entry, MatchValueLogicOperator op) { + entry.match_logic_type() = op; +} + +// ---- entry-level attribute registry ----------------------------------------- +// One line per documented attribute: its dispatch key, its value shape, and +// the setter that stores it. asn-regexp is a joined string because an AS-path +// regex separates ASNs with spaces (e.g. `^65000 65001$`), so its tokens must +// be re-joined rather than required to be single. +const std::map, std::less<>>& +entryAttrHandlers() { + static const std::string kMatchLogicValues = + fmt::format("{}|{}", kMatchLogicEqual, kMatchLogicNotEqual); + static const std::map, std::less<>> + kHandlers = { + {std::string(kAsnRegexp), + joinedStringAttr(kAsnRegexp, setAsnRegexp)}, + {std::string(kDescription), + joinedStringAttr(kDescription, setDescription)}, + {std::string(kMatchLogic), + enumAttr( + kMatchLogic, + kMatchLogicValues, + lookupMatchLogic, + setMatchLogic)}, + }; + return kHandlers; +} + +std::string validAttrList() { + std::string out; + for (const auto& [name, _] : entryAttrHandlers()) { + if (!out.empty()) { + out += ", "; + } + out += name; + } + return out; +} + +} // namespace + +// Parse + validate at construction so queryClient stays a thin dispatch. +// Throwing std::invalid_argument is how the framework surfaces arg parse +// errors (same mechanism as BgpAsPathListConfig). +BgpAsPathListEntryConfig::BgpAsPathListEntryConfig(std::vector v) + : utils::BaseObjectArgType(v) { + if (v.empty()) { + throw std::invalid_argument( + "Error: entry is required, optionally followed by an " + " "); + } + auto seq = bgpcli::parseNonNegInt64(v[0]); + if (!seq) { + throw std::invalid_argument( + fmt::format( + "Error: entry must be a non-negative integer, got '{}'", + v[0])); + } + seqNum_ = *seq; + if (v.size() == 1) { + return; // bare `entry `: create it + } + + attr_ = v[1]; + values_.assign(v.begin() + 2, v.end()); + + if (entryAttrHandlers().find(attr_) == entryAttrHandlers().end()) { + throw std::invalid_argument( + fmt::format( + "Error: unknown as-path-list entry attribute '{}'. Valid " + "attributes: {}", + attr_, + validAttrList())); + } +} + +CmdConfigProtocolBgpPolicyAsPathListEntryTraits::RetType +CmdConfigProtocolBgpPolicyAsPathListEntry::queryClient( + const HostInfo& /* hostInfo */, + const BgpAsPathListConfig& listArgs, + const ObjectArgType& args) { + // The parent parse accepts `as-path-list ... entry + // ...`, but only the leaf (this command) runs — silently dropping the + // list-level attribute would look like it was staged. Reject the mix. + if (!listArgs.attr().empty()) { + return fmt::format( + "Error: configure as-path-list attributes and entry in separate " + "commands (got as-path-list attribute '{}' alongside entry {})", + listArgs.attr(), + args.seqNum()); + } + + auto& session = ConfigSession::getInstance(); + auto& cfg = session.getBgpConfig(); + const bool listCreated = !bgpcli::asPathListExists(cfg, listArgs.listName()); + auto& list = bgpcli::findOrCreateAsPathList(cfg, listArgs.listName()); + const bool entryCreated = !bgpcli::asPathListEntryExists(list, args.seqNum()); + auto& entry = bgpcli::findOrCreateAsPathListEntry(list, args.seqNum()); + + Result result = args.attr().empty() + ? ok(entryCreated + ? fmt::format( + "Successfully created BGP as-path-list {} entry {}", + listArgs.listName(), + args.seqNum()) + : fmt::format( + "BGP as-path-list {} entry {} already exists", + listArgs.listName(), + args.seqNum())) + // The attribute is guaranteed valid: BgpAsPathListEntryConfig's + // constructor rejects an unknown attribute before we get here. + : entryAttrHandlers().find(args.attr())->second(entry, args.values()); + + if (result.ok) { + if (!args.attr().empty()) { + result.message += fmt::format( + " for as-path-list {} entry {}", listArgs.listName(), args.seqNum()); + } + session.saveBgpConfig(); + result.message += + fmt::format("\nConfig saved to: {}", session.getBgpSessionConfigPath()); + } else { + // A rejected value must not leave a phantom entry (or a phantom list + // implicitly created for it) visible to later lookups in this process. + if (entryCreated) { + list.as_path_list()->pop_back(); + } + if (listCreated) { + cfg.policies()->aspath_lists()->pop_back(); + } + } + return result.message; +} + +void CmdConfigProtocolBgpPolicyAsPathListEntry::printOutput( + const RetType& output) { + std::cout << output << std::endl; +} + +// Explicit template instantiation +template void CmdHandler< + CmdConfigProtocolBgpPolicyAsPathListEntry, + CmdConfigProtocolBgpPolicyAsPathListEntryTraits>::run(); + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.h b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.h new file mode 100644 index 0000000000000..d2718c4d59b1e --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.h @@ -0,0 +1,89 @@ +/* + * 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 +#include +#include +#include "CLI/App.hpp" +#include "fboss/cli/fboss2/CmdHandler.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +namespace facebook::fboss { + +// Parsed `entry [ ...]`, validated at +// construction. An entry (bgp_policy.AsPathListEntry in +// AsPathList.as_path_list[]) is keyed by ; the list it belongs to is +// supplied by the parent command's args. +// +// Grammar (from the FBOSS proposed syntax): +// ... as-path-list entry (create/select) +// ... as-path-list entry asn-regexp +// ... as-path-list entry description +// ... as-path-list entry match-logic +class BgpAsPathListEntryConfig : public utils::BaseObjectArgType { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + /* implicit */ BgpAsPathListEntryConfig(std::vector v); + int64_t seqNum() const { + return seqNum_; + } + const std::string& attr() const { + return attr_; + } + const std::vector& values() const { + return values_; + } + const static utils::ObjectArgTypeId id = + utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_MESSAGE; + + private: + int64_t seqNum_{0}; + std::string attr_; // matched dispatch key ("" = bare create) + std::vector values_; +}; + +// The entry level of the as-path-list family as its own CLI11 subcommand; the +// parent's parsed args arrive through the ancestor-args tuple, mirroring +// CmdConfigProtocolBgpPolicyRoutingPolicyTerm. +struct CmdConfigProtocolBgpPolicyAsPathListEntryTraits + : public WriteCommandTraits { + using ParentCmd = CmdConfigProtocolBgpPolicyAsPathList; + static void addCliArg(CLI::App& cmd, std::vector& args) { + // Entry has no nested subcommands; stop CLI11's parent-chain fallthrough + // from stealing a value token that spells `entry` (e.g. in a description). + cmd.positionals_at_end(); + cmd.add_option("args", args, " [ ...]"); + } + using ObjectArgType = BgpAsPathListEntryConfig; + using RetType = std::string; +}; + +class CmdConfigProtocolBgpPolicyAsPathListEntry + : public CmdHandler< + CmdConfigProtocolBgpPolicyAsPathListEntry, + CmdConfigProtocolBgpPolicyAsPathListEntryTraits> { + public: + using ObjectArgType = + CmdConfigProtocolBgpPolicyAsPathListEntryTraits::ObjectArgType; + using RetType = CmdConfigProtocolBgpPolicyAsPathListEntryTraits::RetType; + + RetType queryClient( + const HostInfo& hostInfo, + const BgpAsPathListConfig& listArgs, + const ObjectArgType& args); + + void printOutput(const RetType& output); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/config/BUCK b/fboss/cli/fboss2/test/config/BUCK index 2e8ea13d667e0..d92a096554cb4 100644 --- a/fboss/cli/fboss2/test/config/BUCK +++ b/fboss/cli/fboss2/test/config/BUCK @@ -18,6 +18,7 @@ cpp_unittest( "CmdConfigBgpGlobalTest.cpp", "CmdConfigBgpNeighborTest.cpp", "CmdConfigBgpPeerGroupTest.cpp", + "CmdConfigBgpPolicyAsPathListEntryTest.cpp", "CmdConfigBgpPolicyAsPathListTest.cpp", "CmdConfigCoppTest.cpp", "CmdConfigDhcpTest.cpp", diff --git a/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListEntryTest.cpp b/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListEntryTest.cpp new file mode 100644 index 0000000000000..85fa203e819b8 --- /dev/null +++ b/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListEntryTest.cpp @@ -0,0 +1,210 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include +#include +#include + +#include +#include +#include +#include + +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/routing_policy_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/test/config/CmdConfigTestBase.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +using namespace ::testing; +using facebook::bgp::routing_policy::MatchValueLogicOperator; + +namespace facebook::fboss { + +// The entry dispatcher only touches the BGP side of ConfigSession, which seeds +// from thrift schema defaults when neither a staged session nor a system +// bgpcpp.conf exists — so no seed agent config is needed (mirrors +// CmdConfigBgpPolicyAsPathListTest). +class CmdConfigBgpPolicyAsPathListEntryTestFixture : public CmdConfigTestBase { + public: + CmdConfigBgpPolicyAsPathListEntryTestFixture() + : CmdConfigTestBase("bgp_aspath_list_entry_test_%%%%-%%%%-%%%%", "") {} + + void SetUp() override { + CmdConfigTestBase::SetUp(); + setupTestableConfigSession(); + } + + // Invoke the entry handler the way the framework does: the parent's parsed + // args plus the entry's own tokens. + std::string runEntry( + const std::vector& listTokens, + const std::vector& entryTokens) { + CmdConfigProtocolBgpPolicyAsPathListEntry cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient( + hostInfo, + BgpAsPathListConfig(listTokens), + BgpAsPathListEntryConfig(entryTokens)); + } + + std::string runList(const std::vector& tokens) { + CmdConfigProtocolBgpPolicyAsPathList cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpAsPathListConfig(tokens)); + } + + const std::vector& lists() { + return *ConfigSession::getInstance() + .getBgpConfig() + .policies() + .ensure() + .aspath_lists(); + } + + const std::vector& entries(size_t listIdx) { + return *lists()[listIdx].as_path_list(); + } + + bool sessionFileExists() { + return std::filesystem::exists( + ConfigSession::getInstance().getBgpSessionConfigPath()); + } +}; + +// ============================================================================== +// BgpAsPathListEntryConfig (arg) validation +// ============================================================================== + +TEST_F(CmdConfigBgpPolicyAsPathListEntryTestFixture, argValidation) { + // Bare create. + auto bare = BgpAsPathListEntryConfig({"10"}); + EXPECT_EQ(bare.seqNum(), 10); + EXPECT_TRUE(bare.attr().empty()); + + // Attribute with values. + auto attr = BgpAsPathListEntryConfig({"10", "asn-regexp", "^65000_"}); + EXPECT_EQ(attr.seqNum(), 10); + EXPECT_EQ(attr.attr(), "asn-regexp"); + EXPECT_EQ(attr.values(), std::vector({"^65000_"})); + + // Invalid: empty, non-numeric or negative seq-num, unknown attribute. + EXPECT_THROW(BgpAsPathListEntryConfig({}), std::invalid_argument); + EXPECT_THROW(BgpAsPathListEntryConfig({"not-a-num"}), std::invalid_argument); + EXPECT_THROW(BgpAsPathListEntryConfig({"-1"}), std::invalid_argument); + EXPECT_THROW( + BgpAsPathListEntryConfig({"10", "no-such-attr", "1"}), + std::invalid_argument); +} + +// ============================================================================== +// Entry-level handlers +// ============================================================================== + +TEST_F(CmdConfigBgpPolicyAsPathListEntryTestFixture, bareCreateEntry) { + auto result = runEntry({"AS100"}, {"10"}); + EXPECT_THAT( + result, + HasSubstr("Successfully created BGP as-path-list AS100 entry 10")); + ASSERT_EQ(lists().size(), 1); + ASSERT_EQ(entries(0).size(), 1); + EXPECT_EQ(*entries(0)[0].sequence_number(), 10); +} + +TEST_F(CmdConfigBgpPolicyAsPathListEntryTestFixture, setEntryAttributes) { + runEntry({"AS100"}, {"10", "asn-regexp", "^65000_"}); + runEntry({"AS100"}, {"10", "description", "match", "65000"}); + runEntry({"AS100"}, {"10", "match-logic", "NOT_EQUAL"}); + + ASSERT_EQ(lists().size(), 1); + ASSERT_EQ(entries(0).size(), 1); + const auto& entry = entries(0)[0]; + EXPECT_EQ(*entry.sequence_number(), 10); + EXPECT_EQ(entry.as_path()->as_path_ref()->asn_regexp().value(), "^65000_"); + EXPECT_EQ(*entry.description(), "match 65000"); + EXPECT_EQ(*entry.match_logic_type(), MatchValueLogicOperator::NOT_EQUAL); +} + +TEST_F( + CmdConfigBgpPolicyAsPathListEntryTestFixture, + entriesAccumulateAndAreKeyed) { + runEntry({"AS100"}, {"10", "asn-regexp", "^65000_"}); + runEntry({"AS100"}, {"20", "asn-regexp", "^65001_"}); + ASSERT_EQ(lists().size(), 1); + ASSERT_EQ(entries(0).size(), 2); + + // Re-referencing an existing entry by seq-num updates it, not appends. + runEntry({"AS100"}, {"10", "match-logic", "EQUAL"}); + EXPECT_EQ(entries(0).size(), 2); +} + +TEST_F(CmdConfigBgpPolicyAsPathListEntryTestFixture, asnRegexpAllowsSpaces) { + // AS-path regexes separate ASNs with spaces; the pattern must survive as a + // single re-joined value. + runEntry({"AS100"}, {"10", "asn-regexp", "^65000", "65001$"}); + EXPECT_EQ( + entries(0)[0].as_path()->as_path_ref()->asn_regexp().value(), + "^65000 65001$"); +} + +TEST_F( + CmdConfigBgpPolicyAsPathListEntryTestFixture, + reReferenceReportsExisting) { + runEntry({"AS100"}, {"10"}); + EXPECT_THAT( + runEntry({"AS100"}, {"10"}), HasSubstr("entry 10 already exists")); + EXPECT_EQ(entries(0).size(), 1); +} + +TEST_F(CmdConfigBgpPolicyAsPathListEntryTestFixture, matchLogicDefaultIsEqual) { + runEntry({"AS100"}, {"10"}); + EXPECT_EQ(*entries(0)[0].match_logic_type(), MatchValueLogicOperator::EQUAL); +} + +// ============================================================================== +// Reject paths — the error is surfaced and nothing is persisted +// ============================================================================== + +TEST_F( + CmdConfigBgpPolicyAsPathListEntryTestFixture, + invalidMatchLogicRejected) { + auto result = runEntry({"AS100"}, {"10", "match-logic", "MAYBE"}); + // The full message matters: the accepted-values suffix once printed garbage + // (a string_view capture dangling over a fmt::format temporary). + EXPECT_THAT( + result, + HasSubstr("Invalid match-logic value 'MAYBE'; expected EQUAL|NOT_EQUAL")); + // The rejected value must not leave a phantom list/entry behind. + EXPECT_TRUE(lists().empty()); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +TEST_F( + CmdConfigBgpPolicyAsPathListEntryTestFixture, + rejectedEntryOnExistingListKeepsList) { + // Create the list first, then reject an entry value on it. + runList({"AS100", "description", "keep-me"}); + ASSERT_EQ(lists().size(), 1); + + auto result = runEntry({"AS100"}, {"10", "match-logic", "MAYBE"}); + EXPECT_THAT(result, HasSubstr("Invalid")); + // The pre-existing list survives; only the phantom entry is rolled back. + ASSERT_EQ(lists().size(), 1); + EXPECT_TRUE(entries(0).empty()); + EXPECT_EQ(*lists()[0].description(), "keep-me"); +} + +// A list-level attribute alongside an entry must be rejected: only the leaf +// command runs, so the list attribute would be silently dropped. +TEST_F( + CmdConfigBgpPolicyAsPathListEntryTestFixture, + listAttributeMixedWithEntryRejected) { + auto result = runEntry({"AS100", "description", "mixed"}, {"10"}); + EXPECT_THAT(result, HasSubstr("separate commands")); + EXPECT_TRUE(lists().empty()); + EXPECT_FALSE(sessionFileExists()); +} + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/integration_test/BUCK b/fboss/cli/fboss2/test/integration_test/BUCK index 87108b4295425..9551b63fe776b 100644 --- a/fboss/cli/fboss2/test/integration_test/BUCK +++ b/fboss/cli/fboss2/test/integration_test/BUCK @@ -23,6 +23,7 @@ cpp_binary( "ConfigBgpGlobalTest.cpp", "ConfigBgpNeighborTest.cpp", "ConfigBgpPeerGroupTest.cpp", + "ConfigBgpPolicyAsPathListEntryTest.cpp", "ConfigBgpPolicyAsPathListTest.cpp", "ConfigBgpSessionTest.cpp", "ConfigConcurrentSessionsTest.cpp", diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListEntryTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListEntryTest.cpp new file mode 100644 index 0000000000000..13c374ef60894 --- /dev/null +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListEntryTest.cpp @@ -0,0 +1,127 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +/** + * End-to-end tests for `fboss2-dev config protocol bgp policy as-path-list + * entry [ ...]`. + * + * Scope: the entry level. Every test stages the change AND commits it, then + * asserts the value landed at the correct thrift field path inside the + * matching .policies.aspath_lists[].as_path_list[] entry of bgpd's running + * config (via getRunningConfig RPC). Stage-only behavior (attribute parsing, + * validation, rejection) is covered by the unit tests; an integration test + * that never commits exercises no daemon. + * + * Requirements: + * - The fboss2-dev binary under test (config subcommand tree). + * - HOME is set (the session file lives under $HOME/.fboss2). + * - bgpd is installed/active (commit restarts it). + */ + +#include +#include +#include +#include + +#include "fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h" +#include "folly/json/dynamic.h" +#include "gmock/gmock.h" + +using namespace facebook::fboss; +using ::testing::HasSubstr; +using ::testing::Not; + +namespace { +// Test-only AS-path-list name, unlikely to collide with a real list in the +// device's running BGP config. +const std::string kList = "FBOSS2-TEST-ASPATH-ENTRY"; +} // namespace + +class ConfigBgpPolicyAsPathListEntryTest : public ConfigBgpTestBase { + protected: + // Stage `config protocol bgp policy as-path-list entry ` + // WITHOUT clearing the staged session, so attributes can accumulate across + // invocations. Returns the staged session JSON. + folly::dynamic stageEntry(const std::vector& tokens) { + std::vector args = { + "config", "protocol", "bgp", "policy", "as-path-list", kList, "entry"}; + args.insert(args.end(), tokens.begin(), tokens.end()); + auto result = runCli(args); + EXPECT_EQ(result.exitCode, 0) + << "stdout=" << result.stdout << " stderr=" << result.stderr; + EXPECT_THAT(result.stdout, Not(HasSubstr("Error:"))); + return readBgpSessionConfig(); + } + + // The .policies.aspath_lists[] entry named `name`, or nullptr. + static const folly::dynamic* findList( + const folly::dynamic& config, + const std::string& name) { + if (config.count("policies") == 0 || + config["policies"].count("aspath_lists") == 0) { + return nullptr; + } + for (const auto& list : config["policies"]["aspath_lists"]) { + if (list.count("name") && list["name"].asString() == name) { + return &list; + } + } + return nullptr; + } + + // The .as_path_list[] entry with sequence_number `seq` inside `list`, or + // nullptr. + static const folly::dynamic* findEntry( + const folly::dynamic& list, + int64_t seq) { + if (list.count("as_path_list") == 0) { + return nullptr; + } + for (const auto& entry : list["as_path_list"]) { + if (entry.count("sequence_number") && + entry["sequence_number"].asInt() == seq) { + return &entry; + } + } + return nullptr; + } +}; + +TEST_F(ConfigBgpPolicyAsPathListEntryTest, SetEntryAsnRegexpAndCommit) { + discardSession(); + clearBgpSession(); + stageEntry({"10", "asn-regexp", "^65000_"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + // Verify the nested union path .as_path.as_path.asn_regexp through the + // daemon's own view of its config — which also confirms the entry + // subcommand's implicit creation of its parent list reaches bgpd. + auto running = readRunningBgpConfigViaRpc(); + const auto* list = findList(running, kList); + ASSERT_NE(list, nullptr) << "bgpd's running config has no as-path-list " + << kList; + const auto* entry = findEntry(*list, 10); + ASSERT_NE(entry, nullptr) << "running config has no entry 10"; + EXPECT_EQ((*entry)["as_path"]["as_path"]["asn_regexp"].asString(), "^65000_"); +} + +TEST_F(ConfigBgpPolicyAsPathListEntryTest, SetEntryMatchLogicAndCommit) { + discardSession(); + clearBgpSession(); + stageEntry({"20", "asn-regexp", "^65001_"}); + stageEntry({"20", "match-logic", "NOT_EQUAL"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + auto running = readRunningBgpConfigViaRpc(); + const auto* list = findList(running, kList); + ASSERT_NE(list, nullptr) << "bgpd's running config has no as-path-list " + << kList; + const auto* entry = findEntry(*list, 20); + ASSERT_NE(entry, nullptr) << "running config has no entry 20"; + // Enums ride the SimpleJSON wire format as integers: + // routing_policy.MatchValueLogicOperator.NOT_EQUAL = 1. + EXPECT_EQ((*entry)["match_logic_type"].asInt(), 1); +} From 32990f2de242786d81bbce5de40f0fd2e26eddcc Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Tue, 28 Jul 2026 06:26:32 +0000 Subject: [PATCH 19/22] [Nexthop] NOS-6649..NOS-6656: fboss2 config/delete protocol bgp policy community-list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `config protocol bgp policy community-list [community ] [ ...]` and `delete protocol bgp policy community-list ` — the second BGP policy object-type command family, beneath the `policy` grouping node alongside as-path-list. - community-list dispatcher with a two-level key (list name + inline community member name) writing bgp_policy.BgpPolicies.community_lists[] through the typed ConfigSession; boolean-operator maps to routing_policy.BooleanOperator, exact-match to the optional bool, and the member attributes (description/type/value) set the CommunityRefType union's inline Community arm, keyed by Community.name. - new boolAttr factory in the shared BgpCliAttrHandlers.h (generalizing the neighbor dispatcher's), reused by upcoming prefix-list. - fix a latent dangling-string_view in the shared enumAttr factory: the lambda captured a string_view over the caller's fmt::format temporary, so every invalid-enum-value rejection printed garbage after "expected". valueDesc is now taken (and captured) by value; the as-path-list and community-list rejection tests pin the full message. - unit tests (19) + commit-path integration tests mirroring ConfigBgpPolicyAsPathListTest (not yet run on a DUT). Co-Authored-By: Claude Fable 5 --- cmake/CliFboss2.cmake | 5 + cmake/CliFboss2TestConfig.cmake | 2 + cmake/CliFboss2TestIntegrationTest.cmake | 1 + fboss/cli/fboss2/BUCK | 5 + fboss/cli/fboss2/CmdListConfig.cpp | 72 ++++-- .../config/protocol/bgp/BgpCliValueParsers.h | 52 ++++ .../bgp/policy/CmdConfigProtocolBgpPolicy.h | 9 +- .../community-list/BgpCommunityListCliUtils.h | 90 +++++++ ...mdConfigProtocolBgpPolicyCommunityList.cpp | 204 ++++++++++++++++ .../CmdConfigProtocolBgpPolicyCommunityList.h | 86 +++++++ ...mdDeleteProtocolBgpPolicyCommunityList.cpp | 138 +++++++++++ .../CmdDeleteProtocolBgpPolicyCommunityList.h | 74 ++++++ fboss/cli/fboss2/test/config/BUCK | 2 + .../CmdConfigBgpPolicyCommunityListTest.cpp | 181 ++++++++++++++ .../CmdDeleteBgpPolicyCommunityListTest.cpp | 223 ++++++++++++++++++ fboss/cli/fboss2/test/integration_test/BUCK | 1 + .../ConfigBgpPolicyCommunityListTest.cpp | 126 ++++++++++ 17 files changed, 1242 insertions(+), 29 deletions(-) create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/BgpCommunityListCliUtils.h create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.cpp create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h create mode 100644 fboss/cli/fboss2/commands/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.cpp create mode 100644 fboss/cli/fboss2/commands/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.h create mode 100644 fboss/cli/fboss2/test/config/CmdConfigBgpPolicyCommunityListTest.cpp create mode 100644 fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyCommunityListTest.cpp create mode 100644 fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyCommunityListTest.cpp diff --git a/cmake/CliFboss2.cmake b/cmake/CliFboss2.cmake index fef17069ed32c..fa59063e3c870 100644 --- a/cmake/CliFboss2.cmake +++ b/cmake/CliFboss2.cmake @@ -958,6 +958,9 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.cpp fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.h + fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/BgpCommunityListCliUtils.h + fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.cpp + fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.cpp fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.h fboss/cli/fboss2/commands/config/ptp/transparent_clock/CmdConfigPtpTransparentClock.cpp @@ -1080,6 +1083,8 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.h fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.cpp fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h + fboss/cli/fboss2/commands/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.cpp + fboss/cli/fboss2/commands/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.h fboss/cli/fboss2/commands/delete/protocol/CmdDeleteProtocol.h fboss/cli/fboss2/commands/delete/protocol/static/CmdDeleteProtocolStatic.cpp fboss/cli/fboss2/commands/delete/protocol/static/CmdDeleteProtocolStatic.h diff --git a/cmake/CliFboss2TestConfig.cmake b/cmake/CliFboss2TestConfig.cmake index f4c531621f651..166910e746936 100644 --- a/cmake/CliFboss2TestConfig.cmake +++ b/cmake/CliFboss2TestConfig.cmake @@ -15,6 +15,7 @@ add_executable(fboss2_cmd_config_test fboss/cli/fboss2/test/config/CmdConfigBgpPeerGroupTest.cpp fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListTest.cpp fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListEntryTest.cpp + fboss/cli/fboss2/test/config/CmdConfigBgpPolicyCommunityListTest.cpp fboss/cli/fboss2/test/config/CmdConfigCoppTest.cpp fboss/cli/fboss2/test/config/CmdConfigDhcpTest.cpp fboss/cli/fboss2/test/config/CmdConfigHostnameTest.cpp @@ -50,6 +51,7 @@ add_executable(fboss2_cmd_config_test fboss/cli/fboss2/test/config/CmdDeleteBgpNeighborTest.cpp fboss/cli/fboss2/test/config/CmdDeleteBgpPeerGroupTest.cpp fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyAsPathListTest.cpp + fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyCommunityListTest.cpp fboss/cli/fboss2/test/config/CmdDeleteConfigInterfaceTest.cpp fboss/cli/fboss2/test/config/CmdDeleteCoppQueueTest.cpp fboss/cli/fboss2/test/config/CmdDeleteCoppReasonTest.cpp diff --git a/cmake/CliFboss2TestIntegrationTest.cmake b/cmake/CliFboss2TestIntegrationTest.cmake index 1c8097b7612e8..ced95dadbfa32 100644 --- a/cmake/CliFboss2TestIntegrationTest.cmake +++ b/cmake/CliFboss2TestIntegrationTest.cmake @@ -18,6 +18,7 @@ add_executable(fboss2_integration_test fboss/cli/fboss2/test/integration_test/ConfigBgpPeerGroupTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListEntryTest.cpp + fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyCommunityListTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp fboss/cli/fboss2/test/integration_test/ConfigConcurrentSessionsTest.cpp fboss/cli/fboss2/test/integration_test/ConfigHostnameTest.cpp diff --git a/fboss/cli/fboss2/BUCK b/fboss/cli/fboss2/BUCK index 5161382e11a68..8667039122d4c 100644 --- a/fboss/cli/fboss2/BUCK +++ b/fboss/cli/fboss2/BUCK @@ -1169,6 +1169,7 @@ cpp_library( "commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.cpp", "commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.cpp", "commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.cpp", + "commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.cpp", "commands/config/protocol/static/CmdConfigProtocolStatic.cpp", "commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.cpp", "commands/config/ptp/CmdConfigPtp.cpp", @@ -1229,6 +1230,7 @@ cpp_library( "commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.cpp", "commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.cpp", "commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.cpp", + "commands/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.cpp", "commands/delete/protocol/static/CmdDeleteProtocolStatic.cpp", "commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.cpp", "commands/delete/qos/CmdDeleteQos.cpp", @@ -1300,6 +1302,8 @@ cpp_library( "commands/config/protocol/bgp/policy/as-path-list/BgpAsPathListCliUtils.h", "commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h", "commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.h", + "commands/config/protocol/bgp/policy/community-list/BgpCommunityListCliUtils.h", + "commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h", "commands/config/protocol/static/CmdConfigProtocolStatic.h", "commands/config/protocol/static/route/StaticRouteUtils.h", "commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.h", @@ -1361,6 +1365,7 @@ cpp_library( "commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.h", "commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.h", "commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h", + "commands/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.h", "commands/delete/protocol/static/CmdDeleteProtocolStatic.h", "commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.h", "commands/delete/qos/CmdDeleteQos.h", diff --git a/fboss/cli/fboss2/CmdListConfig.cpp b/fboss/cli/fboss2/CmdListConfig.cpp index 0b5e3d3289aa5..6e25133b69800 100644 --- a/fboss/cli/fboss2/CmdListConfig.cpp +++ b/fboss/cli/fboss2/CmdListConfig.cpp @@ -44,6 +44,7 @@ #include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h" #include "fboss/cli/fboss2/commands/config/protocol/static/CmdConfigProtocolStatic.h" #include "fboss/cli/fboss2/commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.h" #include "fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.h" @@ -101,6 +102,7 @@ #include "fboss/cli/fboss2/commands/delete/protocol/bgp/peer-group/CmdDeleteProtocolBgpPeerGroup.h" #include "fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.h" #include "fboss/cli/fboss2/commands/delete/protocol/bgp/policy/as-path-list/CmdDeleteProtocolBgpPolicyAsPathList.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.h" #include "fboss/cli/fboss2/commands/delete/protocol/static/CmdDeleteProtocolStatic.h" #include "fboss/cli/fboss2/commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.h" #include "fboss/cli/fboss2/commands/delete/qos/CmdDeleteQos.h" @@ -398,25 +400,35 @@ const CommandTree& kConfigCommandTree() { commandHandler, argRegistrar, {{ - "as-path-list", - "Configure BGP AS-path list: " - "[ ...] (description, " - "regex, boolean-operator)", - commandHandler< - CmdConfigProtocolBgpPolicyAsPathList>, - argRegistrar< - CmdConfigProtocolBgpPolicyAsPathListTraits>, - {{ - "entry", - "Configure an AS-path list entry: " - " [ ...] " - "(asn-regexp, description, match-logic)", - commandHandler< - CmdConfigProtocolBgpPolicyAsPathListEntry>, - argRegistrar< - CmdConfigProtocolBgpPolicyAsPathListEntryTraits>, - }}, - }}, + "as-path-list", + "Configure BGP AS-path list: " + "[ ...] (description, " + "regex, boolean-operator)", + commandHandler< + CmdConfigProtocolBgpPolicyAsPathList>, + argRegistrar< + CmdConfigProtocolBgpPolicyAsPathListTraits>, + {{ + "entry", + "Configure an AS-path list entry: " + " [ ...] " + "(asn-regexp, description, match-logic)", + commandHandler< + CmdConfigProtocolBgpPolicyAsPathListEntry>, + argRegistrar< + CmdConfigProtocolBgpPolicyAsPathListEntryTraits>, + }}, + }, + { + "community-list", + "Configure BGP community-list: " + "[ ...] " + "(boolean-operator, description, exact-match)", + commandHandler< + CmdConfigProtocolBgpPolicyCommunityList>, + argRegistrar< + CmdConfigProtocolBgpPolicyCommunityListTraits>, + }}, }, }, }, @@ -766,13 +778,21 @@ const CommandTree& kConfigCommandTree() { commandHandler, argTypeHandler, {{ - "as-path-list", - "Delete a BGP AS-path list, or one of its regexes: " - " [regex ]", - commandHandler, - argRegistrar< - CmdDeleteProtocolBgpPolicyAsPathListTraits>, - }}, + "as-path-list", + "Delete a BGP AS-path list, or one of its regexes: " + " [regex ]", + commandHandler, + argRegistrar< + CmdDeleteProtocolBgpPolicyAsPathListTraits>, + }, + { + "community-list", + "Delete a BGP community-list: ", + commandHandler< + CmdDeleteProtocolBgpPolicyCommunityList>, + argRegistrar< + CmdDeleteProtocolBgpPolicyCommunityListTraits>, + }}, }}, }, { diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h index 6e80deec368b4..5d610e328cae5 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h @@ -10,13 +10,18 @@ #pragma once +#include #include +#include #include #include #include #include +#include #include +#include #include +#include /** * Value parsing helpers shared by the BGP config dispatchers @@ -93,4 +98,51 @@ inline std::optional parseAsn4Byte(const std::string& value) { return static_cast(*parsed); } +// ---- constructor-time token parsing ---------------------------------------- +// Unlike the value parsers above, this runs while constructing a command's +// ObjectArgType, where throwing std::invalid_argument IS the framework's +// error channel — the message is surfaced to the user as the parse error. + +// The ` [ ]` prefix shared by the policy +// list commands (config and delete): the list name, the optional named +// member selected by `keyword`, and where any remaining tokens begin. +struct ListMemberSelector { + std::string listName; + std::optional memberName; + // Index of the first token after the parsed prefix (== tokens.size() when + // nothing follows). The config dispatcher reads an ... + // tail from here; the delete dispatcher rejects any tail. + size_t restStart; +}; + +// Parse the shared prefix. `objectName` is the list flavor for messages +// (e.g. "community-list"), `memberKeyword` selects the nested member (e.g. +// `community`), and `usage` is the whole-command usage line thrown when no +// tokens were given. A second token other than `memberKeyword` is left to +// the caller (restStart == 1): the config grammar treats it as a list-level +// attribute, delete as an unexpected token. +inline ListMemberSelector parseListMemberSelector( + const std::vector& tokens, + std::string_view objectName, + std::string_view memberKeyword, + std::string_view usage) { + if (tokens.empty()) { + throw std::invalid_argument(std::string(usage)); + } + if (tokens[0].empty()) { + throw std::invalid_argument( + fmt::format("Error: {} name must not be empty", objectName)); + } + ListMemberSelector selector{tokens[0], std::nullopt, 1}; + if (tokens.size() > 1 && tokens[1] == memberKeyword) { + if (tokens.size() < 3 || tokens[2].empty()) { + throw std::invalid_argument( + fmt::format("Error: `{}` requires a ", memberKeyword)); + } + selector.memberName = tokens[2]; + selector.restStart = 3; + } + return selector; +} + } // namespace facebook::fboss::bgpcli diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h index 4830fe77f5545..6d0115fb5aa0f 100644 --- a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h @@ -10,15 +10,18 @@ #pragma once +#include +#include #include "fboss/cli/fboss2/CmdHandler.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/CmdConfigProtocolBgp.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" namespace facebook::fboss { // Grouping node for `config protocol bgp policy ...`. Holds no -// state of its own; the per-object-type dispatchers (as-path-list, and later -// community-list/prefix-list/routing-policy) are its subcommands. Mirrors -// CmdConfigProtocolBgp. +// state of its own; the per-object-type dispatchers (as-path-list, +// community-list, and later prefix-list/routing-policy) are its subcommands. +// Mirrors CmdConfigProtocolBgp. struct CmdConfigProtocolBgpPolicyTraits : public WriteCommandTraits { using ParentCmd = CmdConfigProtocolBgp; using ObjectArgType = std::monostate; diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/BgpCommunityListCliUtils.h b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/BgpCommunityListCliUtils.h new file mode 100644 index 0000000000000..5b6f5c60394d4 --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/BgpCommunityListCliUtils.h @@ -0,0 +1,90 @@ +/* + * 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 +#include +#include +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" + +/** + * Lookup/create helpers for the community-list CLI family, shared between the + * list-level dispatcher (CmdConfigProtocolBgpPolicyCommunityList), the + * community subcommand (CmdConfigProtocolBgpPolicyCommunityListCommunity), and + * the delete counterparts. A CommunityList is keyed by name; the inline + * Community members it holds are keyed by their own name. + */ +namespace facebook::fboss::bgpcli { + +inline bool communityListExists( + const bgp::thrift::BgpConfig& cfg, + const std::string& name) { + if (!cfg.policies().has_value()) { + return false; + } + const auto& lists = *cfg.policies()->community_lists(); + return std::any_of(lists.begin(), lists.end(), [&](const auto& list) { + return *list.name() == name; + }); +} + +// Find the community-list keyed by name, creating it if absent. Setting an +// attribute on a not-yet-created list implicitly creates it, so command +// ordering stays forgiving; a bare `community-list ` creates one +// explicitly. CommunityList's only key field is `name`. +inline bgp::bgp_policy::CommunityList& findOrCreateCommunityList( + bgp::thrift::BgpConfig& cfg, + const std::string& name) { + auto& lists = *cfg.policies().ensure().community_lists(); + for (auto& list : lists) { + if (*list.name() == name) { + return list; + } + } + lists.emplace_back(); + auto& list = lists.back(); + list.name() = name; + return list; +} + +inline bool communityMemberExists( + const bgp::bgp_policy::CommunityList& list, + const std::string& name) { + if (!list.members().has_value()) { + return false; + } + const auto& members = *list.members(); + return std::any_of(members.begin(), members.end(), [&](const auto& member) { + return member.community_ref().has_value() && + *member.community_ref()->name() == name; + }); +} + +// Find the inline Community member keyed by name within a list, creating it +// if absent. The CLI always defines members inline (the CommunityRefType +// union's `community` arm); the member's name is its identity. +inline bgp::bgp_policy::Community& findOrCreateCommunityMember( + bgp::bgp_policy::CommunityList& list, + const std::string& name) { + auto& members = list.members().ensure(); + for (auto& member : members) { + if (member.community_ref().has_value() && + *member.community_ref()->name() == name) { + return *member.community_ref(); + } + } + members.emplace_back(); + auto& community = members.back().set_community(); + community.name() = name; + return community; +} + +} // namespace facebook::fboss::bgpcli diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.cpp new file mode 100644 index 0000000000000..fcf7f8dd73725 --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.cpp @@ -0,0 +1,204 @@ +/* + * 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/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h" + +#include "fboss/cli/fboss2/CmdHandler.cpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/routing_policy_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliAttrHandlers.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/BgpCommunityListCliUtils.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" +#include "fmt/format.h" + +namespace facebook::fboss { + +namespace { + +// The attribute names, exactly as documented. Kept here so the +// valid-attribute set and the handler table stay in sync. The list's inline +// Community members are their own subcommand, not attributes. +constexpr std::string_view kBooleanOperator = "boolean-operator"; +constexpr std::string_view kDescription = "description"; +constexpr std::string_view kExactMatch = "exact-match"; + +// boolean-operator values (routing_policy.BooleanOperator names). +constexpr std::string_view kBooleanOperatorAnd = "AND"; +constexpr std::string_view kBooleanOperatorOr = "OR"; +constexpr std::string_view kBooleanOperatorNot = "NOT"; + +using BooleanOperator = bgp::routing_policy::BooleanOperator; +using CommunityList = bgp::bgp_policy::CommunityList; +using bgpcli::AttrHandler; +using bgpcli::boolAttr; +using bgpcli::enumAttr; +using bgpcli::joinedStringAttr; +using bgpcli::ok; +using bgpcli::Result; + +// ---- value lookups ---------------------------------------------------------- + +std::optional lookupBooleanOperator(const std::string& s) { + if (s == kBooleanOperatorAnd) { + return BooleanOperator::AND; + } + if (s == kBooleanOperatorOr) { + return BooleanOperator::OR; + } + if (s == kBooleanOperatorNot) { + return BooleanOperator::NOT; + } + return std::nullopt; +} + +// ---- list-level setters ----------------------------------------------------- +// Each writes one already-parsed, already-validated value. Parsing and message +// text belong to the shared factories in BgpCliAttrHandlers.h. + +void setListBooleanOperator(CommunityList& list, BooleanOperator op) { + list.boolean_operator() = op; +} + +void setListDescription(CommunityList& list, const std::string& description) { + list.description() = description; +} + +void setListExactMatch(CommunityList& list, bool exactMatch) { + list.exact_match() = exactMatch; +} + +// ---- list-level attribute registry ------------------------------------------ +// One line per documented attribute: its dispatch key, its value shape, and +// the setter that stores it. +const std::map, std::less<>>& +listAttrHandlers() { + static const std::string kBooleanOperatorValues = fmt::format( + "{}|{}|{}", kBooleanOperatorAnd, kBooleanOperatorOr, kBooleanOperatorNot); + static const std::map, std::less<>> + kHandlers = { + {std::string(kBooleanOperator), + enumAttr( + kBooleanOperator, + kBooleanOperatorValues, + lookupBooleanOperator, + setListBooleanOperator)}, + {std::string(kDescription), + joinedStringAttr(kDescription, setListDescription)}, + {std::string(kExactMatch), + boolAttr(kExactMatch, setListExactMatch)}, + }; + return kHandlers; +} + +std::string validAttrList() { + std::string out; + for (const auto& [name, _] : listAttrHandlers()) { + if (!out.empty()) { + out += ", "; + } + out += name; + } + return out; +} + +} // namespace + +// Parse + validate at construction so queryClient stays a thin dispatch. +// Throwing std::invalid_argument is how the framework surfaces arg parse +// errors (same mechanism as BgpAsPathListConfig). +BgpCommunityListConfig::BgpCommunityListConfig(std::vector v) + : utils::BaseObjectArgType(v) { + if (v.empty()) { + throw std::invalid_argument( + "Error: community-list is required, optionally followed by an " + " "); + } + if (v[0].empty()) { + throw std::invalid_argument("Error: community-list name must not be empty"); + } + listName_ = v[0]; + + if (v.size() == 1) { + return; // bare `community-list `: create the list + } + + attr_ = v[1]; + values_.assign(v.begin() + 2, v.end()); + + if (listAttrHandlers().find(attr_) == listAttrHandlers().end()) { + throw std::invalid_argument( + fmt::format( + "Error: unknown community-list attribute '{}'. Valid attributes: " + "{}", + attr_, + validAttrList())); + } +} + +CmdConfigProtocolBgpPolicyCommunityListTraits::RetType +CmdConfigProtocolBgpPolicyCommunityList::queryClient( + const HostInfo& /* hostInfo */, + const ObjectArgType& args) { + auto& session = ConfigSession::getInstance(); + auto& cfg = session.getBgpConfig(); + const bool listCreated = !bgpcli::communityListExists(cfg, args.listName()); + auto& list = bgpcli::findOrCreateCommunityList(cfg, args.listName()); + + Result result = args.attr().empty() + ? ok(listCreated + ? fmt::format( + "Successfully created BGP community-list {}", + args.listName()) + : fmt::format( + "BGP community-list {} already exists", args.listName())) + // The attribute is guaranteed valid: BgpCommunityListConfig's + // constructor rejects an unknown attribute before we get here. + : listAttrHandlers().find(args.attr())->second(list, args.values()); + + if (result.ok) { + if (!args.attr().empty()) { + result.message += fmt::format(" for community-list {}", args.listName()); + } + session.saveBgpConfig(); + result.message += + fmt::format("\nConfig saved to: {}", session.getBgpSessionConfigPath()); + } else if (listCreated) { + // Drop the phantom list so a rejected value is not visible to later + // lookups in the same process. + cfg.policies()->community_lists()->pop_back(); + } + return result.message; +} + +void CmdConfigProtocolBgpPolicyCommunityList::printOutput( + const RetType& output) { + std::cout << output << std::endl; +} + +// Explicit template instantiation +template void CmdHandler< + CmdConfigProtocolBgpPolicyCommunityList, + CmdConfigProtocolBgpPolicyCommunityListTraits>::run(); + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h new file mode 100644 index 0000000000000..e8113a09cbe6d --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h @@ -0,0 +1,86 @@ +/* + * 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 +#include +#include "CLI/App.hpp" +#include "fboss/cli/fboss2/CmdHandler.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/CmdConfigProtocolBgpPolicy.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +namespace facebook::fboss { + +// Parsed `community-list [ ...]`, validated at +// construction. A CommunityList is keyed by . Its inline Community +// members are their own subcommand +// (CmdConfigProtocolBgpPolicyCommunityListCommunity), not attributes. +// Mirrors BgpAsPathListConfig. +// +// Grammar (from the FBOSS proposed syntax): +// community-list (create/select list) +// community-list boolean-operator (list attribute) +// community-list description (list attribute) +// community-list exact-match (list attribute) +class BgpCommunityListConfig : public utils::BaseObjectArgType { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + /* implicit */ BgpCommunityListConfig(std::vector v); + const std::string& listName() const { + return listName_; + } + const std::string& attr() const { + return attr_; + } + const std::vector& values() const { + return values_; + } + const static utils::ObjectArgTypeId id = + utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_MESSAGE; + + private: + std::string listName_; + std::string attr_; // matched dispatch key ("" = bare create) + std::vector values_; +}; + +// Single handler for the whole `config protocol bgp policy community-list` +// family; dispatch and per-attribute parsing live in the .cpp so a new tunable +// is a one-entry change. Mirrors CmdConfigProtocolBgpPolicyAsPathList. +struct CmdConfigProtocolBgpPolicyCommunityListTraits + : public WriteCommandTraits { + using ParentCmd = CmdConfigProtocolBgpPolicy; + static void addCliArg(CLI::App& cmd, std::vector& args) { + // No positionals_at_end() here: CLI11 must stay free to classify the + // `community` token as this command's subcommand rather than swallowing it + // into args. See CmdConfigProtocolBgpPolicyAsPathListTraits. + cmd.add_option("args", args, " [ ...]"); + } + using ObjectArgType = BgpCommunityListConfig; + using RetType = std::string; +}; + +class CmdConfigProtocolBgpPolicyCommunityList + : public CmdHandler< + CmdConfigProtocolBgpPolicyCommunityList, + CmdConfigProtocolBgpPolicyCommunityListTraits> { + public: + using ObjectArgType = + CmdConfigProtocolBgpPolicyCommunityListTraits::ObjectArgType; + using RetType = CmdConfigProtocolBgpPolicyCommunityListTraits::RetType; + + RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& args); + + void printOutput(const RetType& output); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.cpp b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.cpp new file mode 100644 index 0000000000000..7788aa590c96f --- /dev/null +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.cpp @@ -0,0 +1,138 @@ +/* + * 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/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.h" + +#include "fboss/cli/fboss2/CmdHandler.cpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" +#include "fmt/format.h" + +namespace facebook::fboss { + +namespace { +// CLI keyword selecting the nested community member, matching the config +// command's grammar. +constexpr std::string_view kObjectName = "community-list"; +constexpr std::string_view kCommunityKeyword = "community"; +} // namespace + +// Parse + validate at construction so queryClient stays a thin dispatch. +BgpCommunityListRef::BgpCommunityListRef(std::vector v) + : utils::BaseObjectArgType(v) { + auto selector = bgpcli::parseListMemberSelector( + v, + kObjectName, + kCommunityKeyword, + "Error: delete protocol bgp policy community-list requires , " + "optionally followed by `community `"); + // Unlike the config grammar, nothing may follow the parsed prefix: there + // are no attributes to delete through this command. + if (selector.restStart < v.size()) { + throw std::invalid_argument( + selector.memberName + ? fmt::format( + "Error: unexpected token '{}' after community ", + v[selector.restStart]) + : fmt::format( + "Error: unexpected token '{}'. Usage: delete protocol bgp " + "policy community-list [community ]", + v[selector.restStart])); + } + listName_ = std::move(selector.listName); + if (selector.memberName) { + hasCommunity_ = true; + communityName_ = std::move(*selector.memberName); + } +} + +CmdDeleteProtocolBgpPolicyCommunityListTraits::RetType +CmdDeleteProtocolBgpPolicyCommunityList::queryClient( + const HostInfo& /* hostInfo */, + const ObjectArgType& args) { + auto& session = ConfigSession::getInstance(); + auto& cfg = session.getBgpConfig(); + if (!cfg.policies().has_value()) { + // Nothing is persisted for an unknown list, so a typo'd delete can't stage + // an unrelated session change. + return fmt::format( + "Error: BGP community-list {} not found", args.listName()); + } + auto& lists = *cfg.policies()->community_lists(); + auto it = std::find_if(lists.begin(), lists.end(), [&](const auto& list) { + return *list.name() == args.listName(); + }); + if (it == lists.end()) { + return fmt::format( + "Error: BGP community-list {} not found", args.listName()); + } + if (args.hasCommunity()) { + // Delete a single inline member; the list itself stays. + auto& list = *it; + if (list.members().has_value()) { + auto& members = *list.members(); + auto memberIt = + std::find_if(members.begin(), members.end(), [&](const auto& member) { + return member.community_ref().has_value() && + *member.community_ref()->name() == args.communityName(); + }); + if (memberIt != members.end()) { + members.erase(memberIt); + if (members.empty()) { + // Leave the list looking like one that never had members, so a + // later `community ` add starts from the same shape. + list.members().reset(); + } + session.saveBgpConfig(); + return fmt::format( + "Successfully deleted BGP community-list {} community {}\n" + "Config saved to: {}", + args.listName(), + args.communityName(), + session.getBgpSessionConfigPath()); + } + } + return fmt::format( + "Error: BGP community-list {} community {} not found", + args.listName(), + args.communityName()); + } + lists.erase(it); + session.saveBgpConfig(); + return fmt::format( + "Successfully deleted BGP community-list {}\nConfig saved to: {}", + args.listName(), + session.getBgpSessionConfigPath()); +} + +void CmdDeleteProtocolBgpPolicyCommunityList::printOutput( + const RetType& output) { + std::cout << output << std::endl; +} + +// Explicit template instantiation +template void CmdHandler< + CmdDeleteProtocolBgpPolicyCommunityList, + CmdDeleteProtocolBgpPolicyCommunityListTraits>::run(); + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.h b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.h new file mode 100644 index 0000000000000..127ea9d6e8e74 --- /dev/null +++ b/fboss/cli/fboss2/commands/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.h @@ -0,0 +1,74 @@ +/* + * 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 +#include +#include "CLI/App.hpp" +#include "fboss/cli/fboss2/CmdHandler.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/policy/CmdDeleteProtocolBgpPolicy.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +namespace facebook::fboss { + +// Parsed `delete protocol bgp policy community-list +// [community ]`, validated at construction. The names are the list's +// and member's identity, matched exactly the same way the config command +// stores them. Without the `community` selector the whole list is deleted; +// with it, only that inline member. Mirrors BgpAsPathListRef. +class BgpCommunityListRef : public utils::BaseObjectArgType { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + /* implicit */ BgpCommunityListRef(std::vector v); + const std::string& listName() const { + return listName_; + } + bool hasCommunity() const { + return hasCommunity_; + } + const std::string& communityName() const { + return communityName_; + } + const static utils::ObjectArgTypeId id = + utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_MESSAGE; + + private: + std::string listName_; + bool hasCommunity_{false}; + std::string communityName_; +}; + +struct CmdDeleteProtocolBgpPolicyCommunityListTraits + : public WriteCommandTraits { + using ParentCmd = CmdDeleteProtocolBgpPolicy; + static void addCliArg(CLI::App& cmd, std::vector& args) { + cmd.add_option("args", args, " [community ]"); + } + using ObjectArgType = BgpCommunityListRef; + using RetType = std::string; +}; + +class CmdDeleteProtocolBgpPolicyCommunityList + : public CmdHandler< + CmdDeleteProtocolBgpPolicyCommunityList, + CmdDeleteProtocolBgpPolicyCommunityListTraits> { + public: + using ObjectArgType = + CmdDeleteProtocolBgpPolicyCommunityListTraits::ObjectArgType; + using RetType = CmdDeleteProtocolBgpPolicyCommunityListTraits::RetType; + + RetType queryClient(const HostInfo& hostInfo, const ObjectArgType& args); + + void printOutput(const RetType& output); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/config/BUCK b/fboss/cli/fboss2/test/config/BUCK index d92a096554cb4..2e95b7ad72064 100644 --- a/fboss/cli/fboss2/test/config/BUCK +++ b/fboss/cli/fboss2/test/config/BUCK @@ -20,6 +20,7 @@ cpp_unittest( "CmdConfigBgpPeerGroupTest.cpp", "CmdConfigBgpPolicyAsPathListEntryTest.cpp", "CmdConfigBgpPolicyAsPathListTest.cpp", + "CmdConfigBgpPolicyCommunityListTest.cpp", "CmdConfigCoppTest.cpp", "CmdConfigDhcpTest.cpp", "CmdConfigHistoryTest.cpp", @@ -69,6 +70,7 @@ cpp_unittest( "ConfigSessionSystemdTest.cpp", "CmdDeleteBgpPeerGroupTest.cpp", "CmdDeleteBgpPolicyAsPathListTest.cpp", + "CmdDeleteBgpPolicyCommunityListTest.cpp", "ProfileValidationTest.cpp", ], headers = [ diff --git a/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyCommunityListTest.cpp b/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyCommunityListTest.cpp new file mode 100644 index 0000000000000..186bce550c7ec --- /dev/null +++ b/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyCommunityListTest.cpp @@ -0,0 +1,181 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include +#include +#include + +#include +#include +#include +#include + +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/routing_policy_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/test/config/CmdConfigTestBase.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +using namespace ::testing; +using facebook::bgp::routing_policy::BooleanOperator; + +namespace facebook::fboss { + +// The community-list dispatcher only touches the BGP side of ConfigSession, +// which seeds from thrift schema defaults when neither a staged session nor a +// system bgpcpp.conf exists — so no seed agent config is needed (mirrors +// CmdConfigBgpPolicyAsPathListTest). +class CmdConfigBgpPolicyCommunityListTestFixture : public CmdConfigTestBase { + public: + CmdConfigBgpPolicyCommunityListTestFixture() + : CmdConfigTestBase("bgp_community_list_test_%%%%-%%%%-%%%%", "") {} + + void SetUp() override { + CmdConfigTestBase::SetUp(); + setupTestableConfigSession(); + } + + std::string run(const std::vector& tokens) { + CmdConfigProtocolBgpPolicyCommunityList cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpCommunityListConfig(tokens)); + } + + const std::vector& lists() { + return *ConfigSession::getInstance() + .getBgpConfig() + .policies() + .ensure() + .community_lists(); + } + + bool sessionFileExists() { + return std::filesystem::exists( + ConfigSession::getInstance().getBgpSessionConfigPath()); + } +}; + +// ============================================================================== +// BgpCommunityListConfig (arg) validation +// ============================================================================== + +TEST_F(CmdConfigBgpPolicyCommunityListTestFixture, argValidation) { + // Bare create list. + auto bareList = BgpCommunityListConfig({"CL100"}); + EXPECT_EQ(bareList.listName(), "CL100"); + EXPECT_TRUE(bareList.attr().empty()); + + // List-level attribute. + auto listAttr = + BgpCommunityListConfig({"CL100", "description", "my", "list"}); + EXPECT_EQ(listAttr.attr(), "description"); + EXPECT_EQ(listAttr.values(), std::vector({"my", "list"})); + + // Invalid: empty, empty name, unknown attribute. + EXPECT_THROW(BgpCommunityListConfig({}), std::invalid_argument); + EXPECT_THROW(BgpCommunityListConfig({""}), std::invalid_argument); + EXPECT_THROW( + BgpCommunityListConfig({"CL100", "no-such-attr", "1"}), + std::invalid_argument); + // value is a community attribute, not a list attribute. + EXPECT_THROW( + BgpCommunityListConfig({"CL100", "value", "65000:100"}), + std::invalid_argument); +} + +// ============================================================================== +// List-level handlers +// ============================================================================== + +TEST_F(CmdConfigBgpPolicyCommunityListTestFixture, bareCreateList) { + auto result = run({"CL100"}); + EXPECT_THAT( + result, HasSubstr("Successfully created BGP community-list CL100")); + ASSERT_EQ(lists().size(), 1); + EXPECT_EQ(*lists()[0].name(), "CL100"); + EXPECT_FALSE(lists()[0].members().has_value()); + EXPECT_TRUE(sessionFileExists()); + + run({"CL200"}); + ASSERT_EQ(lists().size(), 2); + EXPECT_EQ(*lists()[1].name(), "CL200"); +} + +TEST_F(CmdConfigBgpPolicyCommunityListTestFixture, setListDescription) { + auto result = run({"CL100", "description", "spine", "communities"}); + EXPECT_THAT(result, HasSubstr("Successfully set description")); + ASSERT_EQ(lists().size(), 1); + // Multi-token description is re-joined. + EXPECT_EQ(*lists()[0].description(), "spine communities"); +} + +TEST_F(CmdConfigBgpPolicyCommunityListTestFixture, setBooleanOperator) { + auto result = run({"CL100", "boolean-operator", "AND"}); + EXPECT_THAT(result, HasSubstr("Successfully set boolean-operator")); + EXPECT_EQ(*lists()[0].boolean_operator(), BooleanOperator::AND); + + run({"CL100", "boolean-operator", "NOT"}); + EXPECT_EQ(*lists()[0].boolean_operator(), BooleanOperator::NOT); +} + +TEST_F(CmdConfigBgpPolicyCommunityListTestFixture, booleanOperatorDefaultIsOr) { + run({"CL100"}); + EXPECT_EQ(*lists()[0].boolean_operator(), BooleanOperator::OR); +} + +TEST_F(CmdConfigBgpPolicyCommunityListTestFixture, setExactMatch) { + auto result = run({"CL100", "exact-match", "true"}); + EXPECT_THAT(result, HasSubstr("Successfully enabled exact-match")); + ASSERT_TRUE(lists()[0].exact_match().has_value()); + EXPECT_TRUE(*lists()[0].exact_match()); + + auto disabled = run({"CL100", "exact-match", "false"}); + EXPECT_THAT(disabled, HasSubstr("Successfully disabled exact-match")); + EXPECT_FALSE(*lists()[0].exact_match()); +} + +TEST_F(CmdConfigBgpPolicyCommunityListTestFixture, namedListsAreDistinct) { + run({"CL100", "description", "one"}); + run({"CL200", "description", "two"}); + ASSERT_EQ(lists().size(), 2); + // Re-referencing an existing list by name updates it, not appends. + run({"CL100", "description", "one-updated"}); + ASSERT_EQ(lists().size(), 2); + EXPECT_EQ(*lists()[0].description(), "one-updated"); +} + +TEST_F(CmdConfigBgpPolicyCommunityListTestFixture, reReferenceReportsExisting) { + run({"CL100"}); + // A second bare reference must not claim to have created it again. + EXPECT_THAT(run({"CL100"}), HasSubstr("already exists")); + ASSERT_EQ(lists().size(), 1); +} + +// ============================================================================== +// Reject paths — the error is surfaced and nothing is persisted +// ============================================================================== + +TEST_F( + CmdConfigBgpPolicyCommunityListTestFixture, + invalidBooleanOperatorRejected) { + auto result = run({"CL100", "boolean-operator", "XOR"}); + // The full message matters: the accepted-values suffix once printed garbage + // (a string_view capture dangling over a fmt::format temporary). + EXPECT_THAT( + result, + HasSubstr("Invalid boolean-operator value 'XOR'; expected AND|OR|NOT")); + // The rejected value must not leave a phantom list behind. + EXPECT_TRUE(lists().empty()); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +TEST_F(CmdConfigBgpPolicyCommunityListTestFixture, invalidExactMatchRejected) { + auto result = run({"CL100", "exact-match", "maybe"}); + EXPECT_THAT(result, HasSubstr("Invalid")); + EXPECT_TRUE(lists().empty()); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyCommunityListTest.cpp b/fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyCommunityListTest.cpp new file mode 100644 index 0000000000000..23daff00d388f --- /dev/null +++ b/fboss/cli/fboss2/test/config/CmdDeleteBgpPolicyCommunityListTest.cpp @@ -0,0 +1,223 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include +#include +#include + +#include +#include +#include +#include + +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/BgpCommunityListCliUtils.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h" +#include "fboss/cli/fboss2/commands/delete/protocol/bgp/policy/community-list/CmdDeleteProtocolBgpPolicyCommunityList.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/test/config/CmdConfigTestBase.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +using namespace ::testing; + +namespace facebook::fboss { + +// Deleting only touches the BGP side of ConfigSession, which seeds from thrift +// schema defaults when neither a staged session nor a system bgpcpp.conf +// exists — so no seed agent config is needed (mirrors +// CmdDeleteBgpPolicyAsPathListTest). +class CmdDeleteBgpPolicyCommunityListTestFixture : public CmdConfigTestBase { + public: + CmdDeleteBgpPolicyCommunityListTestFixture() + : CmdConfigTestBase("bgp_community_list_delete_test_%%%%-%%%%-%%%%", "") { + } + + void SetUp() override { + CmdConfigTestBase::SetUp(); + setupTestableConfigSession(); + } + + std::string configure(const std::vector& tokens) { + CmdConfigProtocolBgpPolicyCommunityList cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpCommunityListConfig(tokens)); + } + + // Seed an inline Community member. The member level is its own subcommand + // (CmdConfigProtocolBgpPolicyCommunityListCommunity), which lands above this + // command in the stack — so stage it through the shared helpers rather than + // depending on that handler. + void configureCommunity( + const std::string& listName, + const std::string& communityName, + const std::string& value) { + auto& session = ConfigSession::getInstance(); + auto& list = + bgpcli::findOrCreateCommunityList(session.getBgpConfig(), listName); + bgpcli::findOrCreateCommunityMember(list, communityName).value() = value; + session.saveBgpConfig(); + } + + std::string del(const std::vector& tokens) { + CmdDeleteProtocolBgpPolicyCommunityList cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpCommunityListRef(tokens)); + } + + const std::vector& lists() { + return *ConfigSession::getInstance() + .getBgpConfig() + .policies() + .ensure() + .community_lists(); + } + + bool sessionFileExists() { + return std::filesystem::exists( + ConfigSession::getInstance().getBgpSessionConfigPath()); + } +}; + +// ============================================================================== +// BgpCommunityListRef (arg) validation +// ============================================================================== + +TEST_F(CmdDeleteBgpPolicyCommunityListTestFixture, argValidation) { + auto listOnly = BgpCommunityListRef({"CL100"}); + EXPECT_EQ(listOnly.listName(), "CL100"); + EXPECT_FALSE(listOnly.hasCommunity()); + + auto withMember = BgpCommunityListRef({"CL100", "community", "CM1"}); + EXPECT_EQ(withMember.listName(), "CL100"); + EXPECT_TRUE(withMember.hasCommunity()); + EXPECT_EQ(withMember.communityName(), "CM1"); + + // Invalid: empty, empty names, non-`community` second token, missing + // community name, extra tokens. + EXPECT_THROW(BgpCommunityListRef({}), std::invalid_argument); + EXPECT_THROW(BgpCommunityListRef({""}), std::invalid_argument); + EXPECT_THROW(BgpCommunityListRef({"CL100", "CL200"}), std::invalid_argument); + EXPECT_THROW( + BgpCommunityListRef({"CL100", "community"}), std::invalid_argument); + EXPECT_THROW( + BgpCommunityListRef({"CL100", "community", ""}), std::invalid_argument); + EXPECT_THROW( + BgpCommunityListRef({"CL100", "community", "CM1", "extra"}), + std::invalid_argument); +} + +// ============================================================================== +// queryClient +// ============================================================================== + +TEST_F(CmdDeleteBgpPolicyCommunityListTestFixture, deleteExistingList) { + configureCommunity("CL100", "CM1", "65000:100"); + configure({"CL200", "description", "keep"}); + ASSERT_EQ(lists().size(), 2); + + auto result = del({"CL100"}); + EXPECT_THAT( + result, HasSubstr("Successfully deleted BGP community-list CL100")); + // The other list survives untouched. + ASSERT_EQ(lists().size(), 1); + EXPECT_EQ(*lists()[0].name(), "CL200"); + EXPECT_EQ(*lists()[0].description(), "keep"); + EXPECT_TRUE(sessionFileExists()); +} + +TEST_F(CmdDeleteBgpPolicyCommunityListTestFixture, deleteUnknownListRejected) { + auto result = del({"NO-SUCH-LIST"}); + EXPECT_THAT( + result, HasSubstr("Error: BGP community-list NO-SUCH-LIST not found")); + // Nothing was persisted for the failed delete. + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected delete"; +} + +TEST_F( + CmdDeleteBgpPolicyCommunityListTestFixture, + deleteUnknownLeavesOthersIntact) { + configure({"CL100", "description", "one"}); + auto result = del({"CL200"}); + EXPECT_THAT(result, HasSubstr("not found")); + ASSERT_EQ(lists().size(), 1); + EXPECT_EQ(*lists()[0].name(), "CL100"); +} + +// ============================================================================== +// queryClient: single community member deletion +// ============================================================================== + +TEST_F(CmdDeleteBgpPolicyCommunityListTestFixture, deleteExistingCommunity) { + configureCommunity("CL100", "CM1", "65000:100"); + configureCommunity("CL100", "CM2", "65000:200"); + + auto result = del({"CL100", "community", "CM1"}); + EXPECT_THAT( + result, + HasSubstr("Successfully deleted BGP community-list CL100 community CM1")); + // The list and its other member survive. + ASSERT_EQ(lists().size(), 1); + ASSERT_TRUE(lists()[0].members().has_value()); + ASSERT_EQ(lists()[0].members()->size(), 1); + EXPECT_EQ(*(*lists()[0].members())[0].community_ref()->name(), "CM2"); + EXPECT_TRUE(sessionFileExists()); +} + +TEST_F( + CmdDeleteBgpPolicyCommunityListTestFixture, + deleteLastCommunityUnsetsMembers) { + configureCommunity("CL100", "CM1", "65000:100"); + + auto result = del({"CL100", "community", "CM1"}); + EXPECT_THAT(result, HasSubstr("Successfully deleted")); + // The list stays, shaped like one that never had members. + ASSERT_EQ(lists().size(), 1); + EXPECT_EQ(*lists()[0].name(), "CL100"); + EXPECT_FALSE(lists()[0].members().has_value()); +} + +TEST_F( + CmdDeleteBgpPolicyCommunityListTestFixture, + deleteUnknownCommunityRejected) { + configureCommunity("CL100", "CM1", "65000:100"); + // Consume the session file created by configure so we can assert the + // rejected delete does not persist anything new. + ASSERT_TRUE(sessionFileExists()); + + auto result = del({"CL100", "community", "NO-SUCH-COMMUNITY"}); + EXPECT_THAT( + result, + HasSubstr( + "Error: BGP community-list CL100 community NO-SUCH-COMMUNITY not " + "found")); + // The existing member is untouched. + ASSERT_EQ(lists().size(), 1); + ASSERT_TRUE(lists()[0].members().has_value()); + ASSERT_EQ(lists()[0].members()->size(), 1); + EXPECT_EQ(*(*lists()[0].members())[0].community_ref()->name(), "CM1"); +} + +TEST_F( + CmdDeleteBgpPolicyCommunityListTestFixture, + deleteCommunityFromMemberlessListRejected) { + configure({"CL100", "description", "no", "members"}); + + auto result = del({"CL100", "community", "CM1"}); + EXPECT_THAT( + result, + HasSubstr("Error: BGP community-list CL100 community CM1 not found")); + ASSERT_EQ(lists().size(), 1); + EXPECT_FALSE(lists()[0].members().has_value()); +} + +TEST_F( + CmdDeleteBgpPolicyCommunityListTestFixture, + deleteCommunityFromUnknownListRejected) { + auto result = del({"NO-SUCH-LIST", "community", "CM1"}); + EXPECT_THAT( + result, HasSubstr("Error: BGP community-list NO-SUCH-LIST not found")); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected delete"; +} + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/integration_test/BUCK b/fboss/cli/fboss2/test/integration_test/BUCK index 9551b63fe776b..84739a2a54c79 100644 --- a/fboss/cli/fboss2/test/integration_test/BUCK +++ b/fboss/cli/fboss2/test/integration_test/BUCK @@ -25,6 +25,7 @@ cpp_binary( "ConfigBgpPeerGroupTest.cpp", "ConfigBgpPolicyAsPathListEntryTest.cpp", "ConfigBgpPolicyAsPathListTest.cpp", + "ConfigBgpPolicyCommunityListTest.cpp", "ConfigBgpSessionTest.cpp", "ConfigConcurrentSessionsTest.cpp", "ConfigCoppTest.cpp", diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyCommunityListTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyCommunityListTest.cpp new file mode 100644 index 0000000000000..7a30d7425dcc1 --- /dev/null +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyCommunityListTest.cpp @@ -0,0 +1,126 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +/** + * End-to-end tests for `fboss2-dev config protocol bgp policy community-list + * [ ...]`. + * + * Scope: the community-list level (its inline community members are covered by + * ConfigBgpPolicyCommunityListCommunityTest). Every test + * stages the change AND commits it, then asserts the value landed at the + * correct thrift field path inside the matching .policies.community_lists[] + * entry of bgpd's running config (via getRunningConfig RPC) — which also + * confirms bgpd accepts and adopts a `.policies` blob at all. Stage-only + * behavior (attribute parsing, validation, rejection) is covered by the unit + * tests; an integration test that never commits exercises no daemon. + * + * Requirements: + * - The fboss2-dev binary under test (config subcommand tree). + * - HOME is set (the session file lives under $HOME/.fboss2). + * - bgpd is installed/active (commit restarts it). + */ + +#include +#include +#include + +#include "fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h" +#include "folly/json/dynamic.h" +#include "gmock/gmock.h" + +using namespace facebook::fboss; +using ::testing::HasSubstr; +using ::testing::Not; + +namespace { +// Test-only community-list name, unlikely to collide with a real list in the +// device's running BGP config. +const std::string kList = "FBOSS2-TEST-COMMUNITY"; +} // namespace + +class ConfigBgpPolicyCommunityListTest : public ConfigBgpTestBase { + protected: + // Stage `config protocol bgp policy community-list ` WITHOUT + // clearing the staged session, so attributes can accumulate across + // invocations. Returns the staged session JSON. + folly::dynamic stageCommunityList(const std::vector& tokens) { + std::vector args = { + "config", "protocol", "bgp", "policy", "community-list"}; + args.insert(args.end(), tokens.begin(), tokens.end()); + auto result = runCli(args); + EXPECT_EQ(result.exitCode, 0) + << "stdout=" << result.stdout << " stderr=" << result.stderr; + EXPECT_THAT(result.stdout, Not(HasSubstr("Error:"))); + return readBgpSessionConfig(); + } + + // The .policies.community_lists[] entry named `name`, or nullptr. + static const folly::dynamic* findList( + const folly::dynamic& config, + const std::string& name) { + if (config.count("policies") == 0 || + config["policies"].count("community_lists") == 0) { + return nullptr; + } + for (const auto& list : config["policies"]["community_lists"]) { + if (list.count("name") && list["name"].asString() == name) { + return &list; + } + } + return nullptr; + } +}; + +TEST_F(ConfigBgpPolicyCommunityListTest, SetListAttributesAndCommit) { + discardSession(); + clearBgpSession(); + stageCommunityList({kList, "description", "test", "spine", "communities"}); + stageCommunityList({kList, "boolean-operator", "AND"}); + stageCommunityList({kList, "exact-match", "true"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + // Two layers: the promoted file proves what the commit wrote; the daemon's + // getRunningConfig RPC proves bgpd parsed and adopted the .policies blob. + auto config = readSystemBgpConfig(); + const auto* list = findList(config, kList); + ASSERT_NE(list, nullptr) << "committed config has no community-list " + << kList; + EXPECT_EQ((*list)["description"].asString(), "test spine communities"); + + auto running = readRunningBgpConfigViaRpc(); + const auto* runningList = findList(running, kList); + ASSERT_NE(runningList, nullptr) + << "bgpd's running config has no community-list " << kList; + EXPECT_EQ((*runningList)["description"].asString(), "test spine communities"); + // Enums ride the SimpleJSON wire format as integers: + // routing_policy.BooleanOperator.AND = 1. + EXPECT_EQ((*runningList)["boolean_operator"].asInt(), 1); + EXPECT_TRUE((*runningList)["exact_match"].asBool()); +} + +TEST_F(ConfigBgpPolicyCommunityListTest, DeleteListAndCommit) { + // Land a community-list in the system config, then delete it through a + // second commit and verify it is gone from bgpd's running config. + discardSession(); + clearBgpSession(); + stageCommunityList({kList, "description", "delete-me"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + ASSERT_NE(findList(readRunningBgpConfigViaRpc(), kList), nullptr) + << "setup commit did not land the community-list in bgpd's running " + "config"; + + clearBgpSession(); + auto result = + runCli({"delete", "protocol", "bgp", "policy", "community-list", kList}); + EXPECT_THAT(result.stdout, HasSubstr("Successfully deleted")); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after delete commit; state=" + << bgpDaemonActiveState(); + EXPECT_EQ(findList(readRunningBgpConfigViaRpc(), kList), nullptr) + << "deleted community-list still present in bgpd's running config"; +} From 8caee5d39fb7a6974beb5f5b8ba331b84ba93520 Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Tue, 15 Sep 2026 12:18:05 +0000 Subject: [PATCH 20/22] Emit community-list commands from bgp_json_to_cli.py Adds generate_community_list_commands (description, boolean-operator when not the OR default, exact-match) and hooks it into the policy block after the as-path-lists. The deprecated inline `communities` and `community_list_names` have no CLI spelling and are emitted as `# WARNING:` lines. Members follow in the community subcommand change. Co-Authored-By: Claude Fable 5.1 --- fboss/cli/fboss2/tools/bgp_json_to_cli.py | 46 ++++++++++++ .../tools/tests/test_bgp_json_to_cli.py | 73 +++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/fboss/cli/fboss2/tools/bgp_json_to_cli.py b/fboss/cli/fboss2/tools/bgp_json_to_cli.py index 36ccf7909d360..60b200f11629d 100644 --- a/fboss/cli/fboss2/tools/bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/bgp_json_to_cli.py @@ -481,6 +481,50 @@ def generate_as_path_list_commands(as_path_list: dict[str, Any]) -> list[str]: return commands +def generate_community_list_commands(community_list: dict[str, Any]) -> list[str]: + """Generate `config protocol bgp policy community-list` commands for one list. + + The deprecated inline `communities` and the `community_list_names` + references have no CLI spelling; they surface as warnings. + """ + name = community_list.get("name", "") + if not name: + return [] + + prefix = f"config protocol bgp policy community-list {escape_shell_arg(name)}" + commands = [] + if community_list.get("description"): + commands.append( + f"{prefix} description {escape_shell_arg(community_list['description'])}" + ) + if "boolean_operator" in community_list: + operator = _boolean_operator_name(community_list["boolean_operator"]) + if operator != "OR": + commands.append(f"{prefix} boolean-operator {escape_shell_arg(operator)}") + if "exact_match" in community_list: + commands.append( + f"{prefix} exact-match {_shell_bool(community_list['exact_match'])}" + ) + if community_list.get("communities"): + commands.append( + _warning( + f"community-list {name}: inline `communities` has no CLI " + "equivalent (use members); not emitted" + ) + ) + if community_list.get("community_list_names"): + commands.append( + _warning( + f"community-list {name}: community_list_names has no CLI " + "equivalent; not emitted" + ) + ) + if not commands: + # Nothing to set: still recreate the (empty) community-list by name. + commands.append(prefix) + return commands + + def generate_policy_commands(config: dict[str, Any]) -> list[str]: """Generate the `config protocol bgp policy ...` commands. @@ -492,6 +536,8 @@ def generate_policy_commands(config: dict[str, Any]) -> list[str]: commands = [] for as_path_list in policies.get("aspath_lists", []): commands.extend(generate_as_path_list_commands(as_path_list)) + for community_list in policies.get("community_lists", []): + commands.extend(generate_community_list_commands(community_list)) return commands diff --git a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py index 4262d4a523cfe..ca3e6889bece7 100644 --- a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py @@ -22,6 +22,7 @@ format_bandwidth, generate_as_path_list_commands, generate_commands, + generate_community_list_commands, generate_exec_commands, generate_global_commands, generate_peer_commands, @@ -1039,6 +1040,78 @@ def test_no_policies_key(self) -> None: self.assertEqual(generate_policy_commands({}), []) +class GenerateCommunityListCommandsTest(unittest.TestCase): + """Tests for generate_community_list_commands (community-list grammar).""" + + PREFIX = "config protocol bgp policy community-list CL" + + def test_empty_name_returns_empty(self) -> None: + self.assertEqual(generate_community_list_commands({"exact_match": True}), []) + + def test_bare_list_is_recreated(self) -> None: + self.assertEqual( + generate_community_list_commands({"name": "CL"}), [self.PREFIX] + ) + + def test_scalar_attributes(self) -> None: + commands = generate_community_list_commands( + { + "name": "CL", + "description": "transit communities", + "boolean_operator": 1, + "exact_match": True, + } + ) + self.assertEqual( + commands, + [ + f"{self.PREFIX} description 'transit communities'", + f"{self.PREFIX} boolean-operator AND", + f"{self.PREFIX} exact-match true", + ], + ) + + def test_boolean_operator_or_omitted_not_kept(self) -> None: + self.assertEqual( + generate_community_list_commands({"name": "CL", "boolean_operator": "OR"}), + [self.PREFIX], + ) + self.assertEqual( + generate_community_list_commands({"name": "CL", "boolean_operator": 3}), + [f"{self.PREFIX} boolean-operator NOT"], + ) + + def test_exact_match_false_preserved(self) -> None: + self.assertEqual( + generate_community_list_commands({"name": "CL", "exact_match": False}), + [f"{self.PREFIX} exact-match false"], + ) + + def test_dead_fields_warn(self) -> None: + commands = generate_community_list_commands( + {"name": "CL", "communities": ["65000:1"], "community_list_names": ["X"]} + ) + self.assertEqual(len(commands), 2) + for c in commands: + self.assertTrue(c.startswith("# WARNING:"), c) + + def test_policy_block_lists_after_as_path_lists(self) -> None: + config = { + "policies": { + "aspath_lists": [{"name": "A"}], + "community_lists": [{"name": "CL"}], + } + } + commands = generate_policy_commands(config) + self.assertEqual( + commands, + [ + "config protocol bgp policy as-path-list A", + "config protocol bgp policy community-list CL", + ], + ) + + class GenerateScriptCommandsTest(unittest.TestCase): """Tests for generate_exec_commands.""" From a2e0cd05bc86f057ec98e6fbd5fcf17a61088ae7 Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Fri, 7 Aug 2026 19:02:08 +0000 Subject: [PATCH 21/22] [Nexthop] NOS-6649..NOS-6656: fboss2 bgp policy community-list community subcommand Splits the `community` (inline member) level of `config protocol bgp policy community-list` into its own CLI11 subcommand, mirroring `as-path-list entry `. The previous commit ships the list level and the whole delete command; this one ships the member level of the config command: community-list community -> members[] inline Community, keyed by name community-list community value -> Community.value community-list community type -> Community.type community-list community description -> Community.description - The member is selected through the CommunityRefType union's inline arm. The list name arrives through the ancestor-args tuple, and lookup/create is shared with the parent through BgpCommunityListCliUtils.h. - A list attribute typed alongside `community` is rejected rather than silently dropped, because only the leaf command runs. - `delete ... community-list community ` already ships in the previous commit and is untouched here: delete is a single dispatcher that handles both levels through parseListMemberSelector(), so it did not need splitting. - A rejected value leaves nothing staged: a list or member implicitly created for the failed command is rolled back. - unit tests (9): CmdConfigBgpPolicyCommunityListCommunityTest covering arg validation, bare member create, the three member attributes, named members staying distinct, and the rejection paths. - integration tests (2): ConfigBgpPolicyCommunityListCommunityTest stages and commits, then verifies through bgpd's getRunningConfig RPC, including that the list and a sibling member survive a single-member delete. Co-Authored-By: Claude Fable 5 --- cmake/CliFboss2.cmake | 2 + cmake/CliFboss2TestConfig.cmake | 1 + cmake/CliFboss2TestIntegrationTest.cmake | 1 + fboss/cli/fboss2/BUCK | 2 + fboss/cli/fboss2/CmdListConfig.cpp | 11 + ...rotocolBgpPolicyCommunityListCommunity.cpp | 234 ++++++++++++++++++ ...gProtocolBgpPolicyCommunityListCommunity.h | 91 +++++++ fboss/cli/fboss2/test/config/BUCK | 1 + ...figBgpPolicyCommunityListCommunityTest.cpp | 219 ++++++++++++++++ fboss/cli/fboss2/test/integration_test/BUCK | 1 + ...figBgpPolicyCommunityListCommunityTest.cpp | 174 +++++++++++++ 11 files changed, 737 insertions(+) create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.cpp create mode 100644 fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.h create mode 100644 fboss/cli/fboss2/test/config/CmdConfigBgpPolicyCommunityListCommunityTest.cpp create mode 100644 fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyCommunityListCommunityTest.cpp diff --git a/cmake/CliFboss2.cmake b/cmake/CliFboss2.cmake index fa59063e3c870..c98292ff25603 100644 --- a/cmake/CliFboss2.cmake +++ b/cmake/CliFboss2.cmake @@ -961,6 +961,8 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/BgpCommunityListCliUtils.h fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.cpp fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h + fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.cpp + fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.h fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.cpp fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.h fboss/cli/fboss2/commands/config/ptp/transparent_clock/CmdConfigPtpTransparentClock.cpp diff --git a/cmake/CliFboss2TestConfig.cmake b/cmake/CliFboss2TestConfig.cmake index 166910e746936..fcb126ab40057 100644 --- a/cmake/CliFboss2TestConfig.cmake +++ b/cmake/CliFboss2TestConfig.cmake @@ -16,6 +16,7 @@ add_executable(fboss2_cmd_config_test fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListTest.cpp fboss/cli/fboss2/test/config/CmdConfigBgpPolicyAsPathListEntryTest.cpp fboss/cli/fboss2/test/config/CmdConfigBgpPolicyCommunityListTest.cpp + fboss/cli/fboss2/test/config/CmdConfigBgpPolicyCommunityListCommunityTest.cpp fboss/cli/fboss2/test/config/CmdConfigCoppTest.cpp fboss/cli/fboss2/test/config/CmdConfigDhcpTest.cpp fboss/cli/fboss2/test/config/CmdConfigHostnameTest.cpp diff --git a/cmake/CliFboss2TestIntegrationTest.cmake b/cmake/CliFboss2TestIntegrationTest.cmake index ced95dadbfa32..8c52d5ac01ac0 100644 --- a/cmake/CliFboss2TestIntegrationTest.cmake +++ b/cmake/CliFboss2TestIntegrationTest.cmake @@ -19,6 +19,7 @@ add_executable(fboss2_integration_test fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyAsPathListEntryTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyCommunityListTest.cpp + fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyCommunityListCommunityTest.cpp fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp fboss/cli/fboss2/test/integration_test/ConfigConcurrentSessionsTest.cpp fboss/cli/fboss2/test/integration_test/ConfigHostnameTest.cpp diff --git a/fboss/cli/fboss2/BUCK b/fboss/cli/fboss2/BUCK index 8667039122d4c..1e6be07a6ae39 100644 --- a/fboss/cli/fboss2/BUCK +++ b/fboss/cli/fboss2/BUCK @@ -1170,6 +1170,7 @@ cpp_library( "commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.cpp", "commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.cpp", "commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.cpp", + "commands/config/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.cpp", "commands/config/protocol/static/CmdConfigProtocolStatic.cpp", "commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.cpp", "commands/config/ptp/CmdConfigPtp.cpp", @@ -1304,6 +1305,7 @@ cpp_library( "commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.h", "commands/config/protocol/bgp/policy/community-list/BgpCommunityListCliUtils.h", "commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h", + "commands/config/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.h", "commands/config/protocol/static/CmdConfigProtocolStatic.h", "commands/config/protocol/static/route/StaticRouteUtils.h", "commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.h", diff --git a/fboss/cli/fboss2/CmdListConfig.cpp b/fboss/cli/fboss2/CmdListConfig.cpp index 6e25133b69800..c7481dd1d94fa 100644 --- a/fboss/cli/fboss2/CmdListConfig.cpp +++ b/fboss/cli/fboss2/CmdListConfig.cpp @@ -45,6 +45,7 @@ #include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/CmdConfigProtocolBgpPolicyAsPathList.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/as-path-list/entry/CmdConfigProtocolBgpPolicyAsPathListEntry.h" #include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.h" #include "fboss/cli/fboss2/commands/config/protocol/static/CmdConfigProtocolStatic.h" #include "fboss/cli/fboss2/commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.h" #include "fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.h" @@ -428,6 +429,16 @@ const CommandTree& kConfigCommandTree() { CmdConfigProtocolBgpPolicyCommunityList>, argRegistrar< CmdConfigProtocolBgpPolicyCommunityListTraits>, + {{ + "community", + "Configure a community-list community: " + " [ ...] " + "(description, type, value)", + commandHandler< + CmdConfigProtocolBgpPolicyCommunityListCommunity>, + argRegistrar< + CmdConfigProtocolBgpPolicyCommunityListCommunityTraits>, + }}, }}, }, }, diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.cpp b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.cpp new file mode 100644 index 0000000000000..0c1df51f1dfd5 --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.cpp @@ -0,0 +1,234 @@ +/* + * 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/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.h" + +#include "fboss/cli/fboss2/CmdHandler.cpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliAttrHandlers.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/BgpCommunityListCliUtils.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" +#include "fmt/format.h" + +namespace facebook::fboss { + +namespace { + +// The attribute names, exactly as documented. Kept here so the +// valid-attribute set and the handler table stay in sync. +constexpr std::string_view kDescription = "description"; +constexpr std::string_view kType = "type"; +constexpr std::string_view kValue = "value"; + +// type values (bgp_policy.CommunityType names). +constexpr std::string_view kCommunityTypeNormal = "NORMAL"; +constexpr std::string_view kCommunityTypeExtended = "EXTENDED"; +constexpr std::string_view kCommunityTypeLarge = "LARGE"; + +using Community = bgp::bgp_policy::Community; +using CommunityType = bgp::bgp_policy::CommunityType; +using bgpcli::AttrHandler; +using bgpcli::enumAttr; +using bgpcli::joinedStringAttr; +using bgpcli::ok; +using bgpcli::Result; +using bgpcli::stringAttr; + +// ---- value lookups ---------------------------------------------------------- + +std::optional lookupCommunityType(const std::string& s) { + if (s == kCommunityTypeNormal) { + return CommunityType::NORMAL; + } + if (s == kCommunityTypeExtended) { + return CommunityType::EXTENDED; + } + if (s == kCommunityTypeLarge) { + return CommunityType::LARGE; + } + return std::nullopt; +} + +// ---- community-level setters ------------------------------------------------ +// Each writes one already-parsed, already-validated value. Parsing and message +// text belong to the shared factories in BgpCliAttrHandlers.h. + +void setDescription(Community& community, const std::string& description) { + community.description() = description; +} + +void setType(Community& community, CommunityType type) { + community.type() = type; +} + +void setValue(Community& community, const std::string& value) { + community.value() = value; +} + +// ---- community-level attribute registry ------------------------------------- +// One line per documented attribute: its dispatch key, its value shape, and +// the setter that stores it. +const std::map, std::less<>>& +communityAttrHandlers() { + static const std::string kCommunityTypeValues = fmt::format( + "{}|{}|{}", + kCommunityTypeNormal, + kCommunityTypeExtended, + kCommunityTypeLarge); + static const std::map, std::less<>> + kHandlers = { + {std::string(kDescription), + joinedStringAttr(kDescription, setDescription)}, + {std::string(kType), + enumAttr( + kType, kCommunityTypeValues, lookupCommunityType, setType)}, + {std::string(kValue), + stringAttr(kValue, "string", setValue)}, + }; + return kHandlers; +} + +std::string validAttrList() { + std::string out; + for (const auto& [name, _] : communityAttrHandlers()) { + if (!out.empty()) { + out += ", "; + } + out += name; + } + return out; +} + +} // namespace + +// Parse + validate at construction so queryClient stays a thin dispatch. +// Throwing std::invalid_argument is how the framework surfaces arg parse +// errors (same mechanism as BgpCommunityListConfig). +BgpCommunityListCommunityConfig::BgpCommunityListCommunityConfig( + std::vector v) + : utils::BaseObjectArgType(v) { + if (v.empty() || v[0].empty()) { + throw std::invalid_argument( + "Error: community is required, optionally followed by an " + " "); + } + communityName_ = v[0]; + if (v.size() == 1) { + return; // bare `community `: create it + } + + attr_ = v[1]; + values_.assign(v.begin() + 2, v.end()); + + if (communityAttrHandlers().find(attr_) == communityAttrHandlers().end()) { + throw std::invalid_argument( + fmt::format( + "Error: unknown community-list community attribute '{}'. Valid " + "attributes: {}", + attr_, + validAttrList())); + } +} + +CmdConfigProtocolBgpPolicyCommunityListCommunityTraits::RetType +CmdConfigProtocolBgpPolicyCommunityListCommunity::queryClient( + const HostInfo& /* hostInfo */, + const BgpCommunityListConfig& listArgs, + const ObjectArgType& args) { + // The parent parse accepts `community-list ... + // community ...`, but only the leaf (this command) runs — silently dropping + // the list-level attribute would look like it was staged. Reject the mix. + if (!listArgs.attr().empty()) { + return fmt::format( + "Error: configure community-list attributes and community in separate " + "commands (got community-list attribute '{}' alongside community {})", + listArgs.attr(), + args.communityName()); + } + + auto& session = ConfigSession::getInstance(); + auto& cfg = session.getBgpConfig(); + const bool listCreated = + !bgpcli::communityListExists(cfg, listArgs.listName()); + auto& list = bgpcli::findOrCreateCommunityList(cfg, listArgs.listName()); + const bool hadMembers = list.members().has_value(); + const bool memberCreated = + !bgpcli::communityMemberExists(list, args.communityName()); + auto& community = + bgpcli::findOrCreateCommunityMember(list, args.communityName()); + + Result result = args.attr().empty() + ? ok(memberCreated + ? fmt::format( + "Successfully created BGP community-list {} community {}", + listArgs.listName(), + args.communityName()) + : fmt::format( + "BGP community-list {} community {} already exists", + listArgs.listName(), + args.communityName())) + // The attribute is guaranteed valid: BgpCommunityListCommunityConfig's + // constructor rejects an unknown attribute before we get here. + : communityAttrHandlers() + .find(args.attr()) + ->second(community, args.values()); + + if (result.ok) { + if (!args.attr().empty()) { + result.message += fmt::format( + " for community-list {} community {}", + listArgs.listName(), + args.communityName()); + } + session.saveBgpConfig(); + result.message += + fmt::format("\nConfig saved to: {}", session.getBgpSessionConfigPath()); + } else { + // A rejected value must not leave a phantom member (or a phantom list + // implicitly created for it) visible to later lookups in this process — + // restoring an unset members field if this invocation ensured it. + if (memberCreated) { + if (hadMembers) { + list.members()->pop_back(); + } else { + list.members().reset(); + } + } + if (listCreated) { + cfg.policies()->community_lists()->pop_back(); + } + } + return result.message; +} + +void CmdConfigProtocolBgpPolicyCommunityListCommunity::printOutput( + const RetType& output) { + std::cout << output << std::endl; +} + +// Explicit template instantiation +template void CmdHandler< + CmdConfigProtocolBgpPolicyCommunityListCommunity, + CmdConfigProtocolBgpPolicyCommunityListCommunityTraits>::run(); + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.h b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.h new file mode 100644 index 0000000000000..857d344622ae4 --- /dev/null +++ b/fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.h @@ -0,0 +1,91 @@ +/* + * 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 +#include +#include "CLI/App.hpp" +#include "fboss/cli/fboss2/CmdHandler.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +namespace facebook::fboss { + +// Parsed `community [ ...]`, validated at +// construction. A member (bgp_policy.Community, held inline through the +// CommunityRefType union's `community` arm in CommunityList.members[]) is +// keyed by ; the list it belongs to is supplied by the parent command's +// args. +// +// Grammar (from the FBOSS proposed syntax): +// ... community-list community (create/select) +// ... community-list community description +// ... community-list community type +// ... community-list community value +class BgpCommunityListCommunityConfig + : public utils::BaseObjectArgType { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + /* implicit */ BgpCommunityListCommunityConfig(std::vector v); + const std::string& communityName() const { + return communityName_; + } + const std::string& attr() const { + return attr_; + } + const std::vector& values() const { + return values_; + } + const static utils::ObjectArgTypeId id = + utils::ObjectArgTypeId::OBJECT_ARG_TYPE_ID_MESSAGE; + + private: + std::string communityName_; + std::string attr_; // matched dispatch key ("" = bare create) + std::vector values_; +}; + +// The community (member) level of the community-list family as its own CLI11 +// subcommand; the parent's parsed args arrive through the ancestor-args tuple, +// mirroring CmdConfigProtocolBgpPolicyAsPathListEntry. +struct CmdConfigProtocolBgpPolicyCommunityListCommunityTraits + : public WriteCommandTraits { + using ParentCmd = CmdConfigProtocolBgpPolicyCommunityList; + static void addCliArg(CLI::App& cmd, std::vector& args) { + // Community has no nested subcommands; stop CLI11's parent-chain + // fallthrough from stealing a value token that spells `community`. + cmd.positionals_at_end(); + cmd.add_option("args", args, " [ ...]"); + } + using ObjectArgType = BgpCommunityListCommunityConfig; + using RetType = std::string; +}; + +class CmdConfigProtocolBgpPolicyCommunityListCommunity + : public CmdHandler< + CmdConfigProtocolBgpPolicyCommunityListCommunity, + CmdConfigProtocolBgpPolicyCommunityListCommunityTraits> { + public: + using ObjectArgType = + CmdConfigProtocolBgpPolicyCommunityListCommunityTraits::ObjectArgType; + using RetType = + CmdConfigProtocolBgpPolicyCommunityListCommunityTraits::RetType; + + RetType queryClient( + const HostInfo& hostInfo, + const BgpCommunityListConfig& listArgs, + const ObjectArgType& args); + + void printOutput(const RetType& output); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/config/BUCK b/fboss/cli/fboss2/test/config/BUCK index 2e95b7ad72064..e4c8903a4dd50 100644 --- a/fboss/cli/fboss2/test/config/BUCK +++ b/fboss/cli/fboss2/test/config/BUCK @@ -20,6 +20,7 @@ cpp_unittest( "CmdConfigBgpPeerGroupTest.cpp", "CmdConfigBgpPolicyAsPathListEntryTest.cpp", "CmdConfigBgpPolicyAsPathListTest.cpp", + "CmdConfigBgpPolicyCommunityListCommunityTest.cpp", "CmdConfigBgpPolicyCommunityListTest.cpp", "CmdConfigCoppTest.cpp", "CmdConfigDhcpTest.cpp", diff --git a/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyCommunityListCommunityTest.cpp b/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyCommunityListCommunityTest.cpp new file mode 100644 index 0000000000000..805eeae8b49f3 --- /dev/null +++ b/fboss/cli/fboss2/test/config/CmdConfigBgpPolicyCommunityListCommunityTest.cpp @@ -0,0 +1,219 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include +#include +#include + +#include +#include +#include +#include + +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/CmdConfigProtocolBgpPolicyCommunityList.h" +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/community-list/community/CmdConfigProtocolBgpPolicyCommunityListCommunity.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/test/config/CmdConfigTestBase.h" +#include "fboss/cli/fboss2/utils/HostInfo.h" + +using namespace ::testing; + +namespace facebook::fboss { + +// The community dispatcher only touches the BGP side of ConfigSession, which +// seeds from thrift schema defaults when neither a staged session nor a system +// bgpcpp.conf exists — so no seed agent config is needed (mirrors +// CmdConfigBgpPolicyCommunityListTest). +class CmdConfigBgpPolicyCommunityListCommunityTestFixture + : public CmdConfigTestBase { + public: + CmdConfigBgpPolicyCommunityListCommunityTestFixture() + : CmdConfigTestBase("bgp_community_list_community_test_%%%%-%%%%", "") {} + + void SetUp() override { + CmdConfigTestBase::SetUp(); + setupTestableConfigSession(); + } + + // Invoke the community handler the way the framework does: the parent's + // parsed args plus the community's own tokens. + std::string runCommunity( + const std::vector& listTokens, + const std::vector& communityTokens) { + CmdConfigProtocolBgpPolicyCommunityListCommunity cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient( + hostInfo, + BgpCommunityListConfig(listTokens), + BgpCommunityListCommunityConfig(communityTokens)); + } + + std::string runList(const std::vector& tokens) { + CmdConfigProtocolBgpPolicyCommunityList cmd; + HostInfo hostInfo("testhost"); + return cmd.queryClient(hostInfo, BgpCommunityListConfig(tokens)); + } + + const std::vector& lists() { + return *ConfigSession::getInstance() + .getBgpConfig() + .policies() + .ensure() + .community_lists(); + } + + // The inline Community held by members[idx] of list[listIdx]. + const bgp::bgp_policy::Community& member(size_t listIdx, size_t idx) { + return *lists()[listIdx].members()->at(idx).community_ref(); + } + + bool sessionFileExists() { + return std::filesystem::exists( + ConfigSession::getInstance().getBgpSessionConfigPath()); + } +}; + +// ============================================================================== +// BgpCommunityListCommunityConfig (arg) validation +// ============================================================================== + +TEST_F(CmdConfigBgpPolicyCommunityListCommunityTestFixture, argValidation) { + // Bare create. + auto bare = BgpCommunityListCommunityConfig({"CM1"}); + EXPECT_EQ(bare.communityName(), "CM1"); + EXPECT_TRUE(bare.attr().empty()); + + // Attribute with values. + auto attr = BgpCommunityListCommunityConfig({"CM1", "value", "65000:100"}); + EXPECT_EQ(attr.communityName(), "CM1"); + EXPECT_EQ(attr.attr(), "value"); + EXPECT_EQ(attr.values(), std::vector({"65000:100"})); + + // Invalid: empty, empty name, unknown attribute. + EXPECT_THROW(BgpCommunityListCommunityConfig({}), std::invalid_argument); + EXPECT_THROW(BgpCommunityListCommunityConfig({""}), std::invalid_argument); + EXPECT_THROW( + BgpCommunityListCommunityConfig({"CM1", "no-such-attr", "1"}), + std::invalid_argument); + // exact-match is a list attribute, not a community attribute. + EXPECT_THROW( + BgpCommunityListCommunityConfig({"CM1", "exact-match", "true"}), + std::invalid_argument); +} + +// ============================================================================== +// Community-level handlers +// ============================================================================== + +TEST_F( + CmdConfigBgpPolicyCommunityListCommunityTestFixture, + bareCreateCommunity) { + auto result = runCommunity({"CL100"}, {"CM1"}); + EXPECT_THAT( + result, + HasSubstr("Successfully created BGP community-list CL100 community CM1")); + ASSERT_EQ(lists().size(), 1); + ASSERT_EQ(lists()[0].members()->size(), 1); + EXPECT_EQ(*member(0, 0).name(), "CM1"); +} + +TEST_F( + CmdConfigBgpPolicyCommunityListCommunityTestFixture, + setCommunityAttributes) { + runCommunity({"CL100"}, {"CM1", "value", "65000:100"}); + runCommunity({"CL100"}, {"CM1", "type", "LARGE"}); + runCommunity({"CL100"}, {"CM1", "description", "spine", "tag"}); + + ASSERT_EQ(lists().size(), 1); + ASSERT_EQ(lists()[0].members()->size(), 1); + const auto& community = member(0, 0); + EXPECT_EQ(*community.name(), "CM1"); + EXPECT_EQ(*community.value(), "65000:100"); + EXPECT_EQ(*community.type(), bgp::bgp_policy::CommunityType::LARGE); + EXPECT_EQ(*community.description(), "spine tag"); +} + +TEST_F( + CmdConfigBgpPolicyCommunityListCommunityTestFixture, + communitiesAccumulateAndAreKeyed) { + runCommunity({"CL100"}, {"CM1", "value", "65000:100"}); + runCommunity({"CL100"}, {"CM2", "value", "65000:200"}); + ASSERT_EQ(lists().size(), 1); + ASSERT_EQ(lists()[0].members()->size(), 2); + + // Re-referencing an existing member by name updates it, not appends. + runCommunity({"CL100"}, {"CM1", "type", "NORMAL"}); + EXPECT_EQ(lists()[0].members()->size(), 2); + EXPECT_EQ(*member(0, 0).value(), "65000:100"); + EXPECT_EQ(*member(0, 0).type(), bgp::bgp_policy::CommunityType::NORMAL); +} + +TEST_F( + CmdConfigBgpPolicyCommunityListCommunityTestFixture, + reReferenceReportsExisting) { + runCommunity({"CL100"}, {"CM1"}); + EXPECT_THAT( + runCommunity({"CL100"}, {"CM1"}), + HasSubstr("community CM1 already exists")); + EXPECT_EQ(lists()[0].members()->size(), 1); +} + +// ============================================================================== +// Reject paths — the error is surfaced and nothing is persisted +// ============================================================================== + +TEST_F( + CmdConfigBgpPolicyCommunityListCommunityTestFixture, + invalidCommunityTypeRejected) { + auto result = runCommunity({"CL100"}, {"CM1", "type", "MEDIUM"}); + // The full message matters: the accepted-values suffix once printed garbage + // (a string_view capture dangling over a fmt::format temporary). + EXPECT_THAT( + result, + HasSubstr("Invalid type value 'MEDIUM'; expected NORMAL|EXTENDED|LARGE")); + EXPECT_TRUE(lists().empty()); + EXPECT_FALSE(sessionFileExists()) + << "session file should not exist after rejected input"; +} + +TEST_F( + CmdConfigBgpPolicyCommunityListCommunityTestFixture, + rejectedCommunityOnExistingListKeepsList) { + // Create the list first, then reject a member value on it. + runList({"CL100", "description", "keep-me"}); + ASSERT_EQ(lists().size(), 1); + + auto result = runCommunity({"CL100"}, {"CM1", "type", "MEDIUM"}); + EXPECT_THAT(result, HasSubstr("Invalid")); + // The pre-existing list survives; only the phantom member is rolled back — + // including the members field itself, which was unset before. + ASSERT_EQ(lists().size(), 1); + EXPECT_FALSE(lists()[0].members().has_value()); + EXPECT_EQ(*lists()[0].description(), "keep-me"); +} + +TEST_F( + CmdConfigBgpPolicyCommunityListCommunityTestFixture, + rejectedCommunityKeepsExistingMembers) { + runCommunity({"CL100"}, {"CM1", "value", "65000:100"}); + ASSERT_EQ(lists()[0].members()->size(), 1); + + auto result = runCommunity({"CL100"}, {"CM2", "type", "MEDIUM"}); + EXPECT_THAT(result, HasSubstr("Invalid")); + // Only the phantom member is rolled back; the existing one survives. + ASSERT_EQ(lists()[0].members()->size(), 1); + EXPECT_EQ(*member(0, 0).name(), "CM1"); +} + +// A list-level attribute alongside a community must be rejected: only the leaf +// command runs, so the list attribute would be silently dropped. +TEST_F( + CmdConfigBgpPolicyCommunityListCommunityTestFixture, + listAttributeMixedWithCommunityRejected) { + auto result = runCommunity({"CL100", "description", "mixed"}, {"CM1"}); + EXPECT_THAT(result, HasSubstr("separate commands")); + EXPECT_TRUE(lists().empty()); + EXPECT_FALSE(sessionFileExists()); +} + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/integration_test/BUCK b/fboss/cli/fboss2/test/integration_test/BUCK index 84739a2a54c79..f000b53f2f8a9 100644 --- a/fboss/cli/fboss2/test/integration_test/BUCK +++ b/fboss/cli/fboss2/test/integration_test/BUCK @@ -25,6 +25,7 @@ cpp_binary( "ConfigBgpPeerGroupTest.cpp", "ConfigBgpPolicyAsPathListEntryTest.cpp", "ConfigBgpPolicyAsPathListTest.cpp", + "ConfigBgpPolicyCommunityListCommunityTest.cpp", "ConfigBgpPolicyCommunityListTest.cpp", "ConfigBgpSessionTest.cpp", "ConfigConcurrentSessionsTest.cpp", diff --git a/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyCommunityListCommunityTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyCommunityListCommunityTest.cpp new file mode 100644 index 0000000000000..8b06618521fc9 --- /dev/null +++ b/fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyCommunityListCommunityTest.cpp @@ -0,0 +1,174 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +/** + * End-to-end tests for `fboss2-dev config protocol bgp policy community-list + * community [ ...]`. + * + * Scope: the inline community-member level. Every test stages the change AND + * commits it, then asserts the value landed at the correct thrift field path + * inside the matching .policies.community_lists[].members[] entry of bgpd's + * running config (via getRunningConfig RPC). Stage-only behavior (attribute + * parsing, validation, rejection) is covered by the unit tests; an integration + * test that never commits exercises no daemon. + * + * Requirements: + * - The fboss2-dev binary under test (config subcommand tree). + * - HOME is set (the session file lives under $HOME/.fboss2). + * - bgpd is installed/active (commit restarts it). + */ + +#include +#include +#include +#include + +#include "fboss/cli/fboss2/test/integration_test/ConfigBgpTestBase.h" +#include "folly/json/dynamic.h" +#include "gmock/gmock.h" + +using namespace facebook::fboss; +using ::testing::HasSubstr; +using ::testing::Not; + +namespace { +// Test-only community-list / member names, unlikely to collide with a real +// list in the device's running BGP config. +const std::string kList = "FBOSS2-TEST-COMMUNITY-MEMBER"; +const std::string kMember = "FBOSS2-TEST-CM1"; +const std::string kMember2 = "FBOSS2-TEST-CM2"; +} // namespace + +class ConfigBgpPolicyCommunityListCommunityTest : public ConfigBgpTestBase { + protected: + // Stage `config protocol bgp policy community-list community + // ` WITHOUT clearing the staged session, so attributes can + // accumulate across invocations. Returns the staged session JSON. + folly::dynamic stageCommunity(const std::vector& tokens) { + std::vector args = { + "config", + "protocol", + "bgp", + "policy", + "community-list", + kList, + "community"}; + args.insert(args.end(), tokens.begin(), tokens.end()); + auto result = runCli(args); + EXPECT_EQ(result.exitCode, 0) + << "stdout=" << result.stdout << " stderr=" << result.stderr; + EXPECT_THAT(result.stdout, Not(HasSubstr("Error:"))); + return readBgpSessionConfig(); + } + + // The .policies.community_lists[] entry named `name`, or nullptr. + static const folly::dynamic* findList( + const folly::dynamic& config, + const std::string& name) { + if (config.count("policies") == 0 || + config["policies"].count("community_lists") == 0) { + return nullptr; + } + for (const auto& list : config["policies"]["community_lists"]) { + if (list.count("name") && list["name"].asString() == name) { + return &list; + } + } + return nullptr; + } + + // The inline Community held by a .members[] entry named `name`, or nullptr. + static const folly::dynamic* findMember( + const folly::dynamic& list, + const std::string& name) { + if (list.count("members") == 0) { + return nullptr; + } + for (const auto& member : list["members"]) { + if (member.count("community") && member["community"].count("name") && + member["community"]["name"].asString() == name) { + return &member["community"]; + } + } + return nullptr; + } +}; + +TEST_F(ConfigBgpPolicyCommunityListCommunityTest, SetAttributesAndCommit) { + discardSession(); + clearBgpSession(); + stageCommunity({kMember, "value", "65000:100"}); + stageCommunity({kMember, "type", "NORMAL"}); + stageCommunity({kMember, "description", "test"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + // Verify the nested member path .members[].community.{value,type, + // description} through the daemon's own view of its config — which also + // confirms the community subcommand's implicit creation of its parent list + // reaches bgpd. + auto running = readRunningBgpConfigViaRpc(); + const auto* list = findList(running, kList); + ASSERT_NE(list, nullptr) << "bgpd's running config has no community-list " + << kList; + const auto* community = findMember(*list, kMember); + ASSERT_NE(community, nullptr) + << "running config has no member community " << kMember; + EXPECT_EQ((*community)["value"].asString(), "65000:100"); + // bgp_policy.CommunityType.NORMAL = 1 (integer on the SimpleJSON wire). + EXPECT_EQ((*community)["type"].asInt(), 1); + EXPECT_EQ((*community)["description"].asString(), "test"); +} + +TEST_F(ConfigBgpPolicyCommunityListCommunityTest, DeleteCommunityAndCommit) { + // Land a community-list with two inline members, delete one member through + // a second commit, and verify the list and the other member survive in + // bgpd's running config. + discardSession(); + clearBgpSession(); + stageCommunity({kMember, "value", "65000:100"}); + stageCommunity({kMember2, "value", "65000:200"}); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after commit; state=" + << bgpDaemonActiveState(); + { + auto running = readRunningBgpConfigViaRpc(); + const auto* list = findList(running, kList); + ASSERT_NE(list, nullptr) + << "setup commit did not land the community-list in bgpd's running " + "config"; + ASSERT_NE(findMember(*list, kMember), nullptr); + ASSERT_NE(findMember(*list, kMember2), nullptr); + } + + clearBgpSession(); + auto result = runCli( + {"delete", + "protocol", + "bgp", + "policy", + "community-list", + kList, + "community", + kMember}); + EXPECT_THAT( + result.stdout, + HasSubstr( + fmt::format( + "Successfully deleted BGP community-list {} community {}", + kList, + kMember))); + commitAndGetSha(); + ASSERT_TRUE(waitForBgpDaemonActive()) + << "bgpd did not return active after delete commit; state=" + << bgpDaemonActiveState(); + auto running = readRunningBgpConfigViaRpc(); + const auto* list = findList(running, kList); + ASSERT_NE(list, nullptr) + << "deleting one member must not delete the community-list"; + EXPECT_EQ(findMember(*list, kMember), nullptr) + << "deleted member still present in bgpd's running config"; + EXPECT_NE(findMember(*list, kMember2), nullptr) + << "surviving member missing from bgpd's running config"; +} From 85446b426f9379f321e6251ae2cad9e5ef58aba5 Mon Sep 17 00:00:00 2001 From: Hillol Chakraborty Date: Tue, 15 Sep 2026 12:18:08 +0000 Subject: [PATCH 22/22] Emit community-list community commands from bgp_json_to_cli.py Each inline `community` member of a community-list becomes `... community-list community {description|type|value}` lines, emitted inside the list generator right after the list attributes. A `community_name` reference member has no CLI spelling and is emitted as a `# WARNING:` line. Co-Authored-By: Claude Fable 5.1 --- fboss/cli/fboss2/tools/bgp_json_to_cli.py | 53 +++++++++++++++ .../tools/tests/test_bgp_json_to_cli.py | 65 +++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/fboss/cli/fboss2/tools/bgp_json_to_cli.py b/fboss/cli/fboss2/tools/bgp_json_to_cli.py index 60b200f11629d..f3c3fbb236a95 100644 --- a/fboss/cli/fboss2/tools/bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/bgp_json_to_cli.py @@ -519,12 +519,65 @@ def generate_community_list_commands(community_list: dict[str, Any]) -> list[str "equivalent; not emitted" ) ) + for member in community_list.get("members") or []: + commands.extend(generate_community_list_community_commands(name, member)) if not commands: # Nothing to set: still recreate the (empty) community-list by name. commands.append(prefix) return commands +_COMMUNITY_TYPE_NAMES = {1: "NORMAL", 2: "EXTENDED", 3: "LARGE"} + + +def generate_community_list_community_commands( + list_name: str, member: dict[str, Any] +) -> list[str]: + """Generate `... community-list community ` commands for one + member. Only inline `community` members have a CLI spelling; a + `community_name` reference surfaces as a warning.""" + if "community" not in member: + return [ + _warning( + f"community-list {list_name}: member references community " + f"'{member.get('community_name', '')}' by name, which has no " + "CLI equivalent; not emitted" + ) + ] + community = member["community"] + name = community.get("name", "") + if not name: + return [ + _warning( + f"community-list {list_name}: unnamed community member cannot " + "be addressed by the CLI; not emitted" + ) + ] + + prefix = ( + f"config protocol bgp policy community-list {escape_shell_arg(list_name)} " + f"community {escape_shell_arg(name)}" + ) + commands = [] + if community.get("description"): + commands.append( + f"{prefix} description {escape_shell_arg(community['description'])}" + ) + if "type" in community: + raw = community["type"] + type_name = ( + raw + if isinstance(raw, str) + else _COMMUNITY_TYPE_NAMES.get(int(raw), str(raw)) + ) + commands.append(f"{prefix} type {escape_shell_arg(type_name)}") + if community.get("value"): + commands.append(f"{prefix} value {escape_shell_arg(community['value'])}") + if not commands: + commands.append(prefix) + return commands + + def generate_policy_commands(config: dict[str, Any]) -> list[str]: """Generate the `config protocol bgp policy ...` commands. diff --git a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py index ca3e6889bece7..3b27b2b9ae5b8 100644 --- a/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py +++ b/fboss/cli/fboss2/tools/tests/test_bgp_json_to_cli.py @@ -23,6 +23,7 @@ generate_as_path_list_commands, generate_commands, generate_community_list_commands, + generate_community_list_community_commands, generate_exec_commands, generate_global_commands, generate_peer_commands, @@ -1112,6 +1113,70 @@ def test_policy_block_lists_after_as_path_lists(self) -> None: ) +class GenerateCommunityListCommunityCommandsTest(unittest.TestCase): + """Tests for generate_community_list_community_commands (member grammar).""" + + PREFIX = "config protocol bgp policy community-list CL community C1" + + def test_inline_member(self) -> None: + commands = generate_community_list_community_commands( + "CL", + { + "community": { + "name": "C1", + "type": 1, + "value": "65000:100", + "description": "peer tag", + } + }, + ) + self.assertEqual( + commands, + [ + f"{self.PREFIX} description 'peer tag'", + f"{self.PREFIX} type NORMAL", + f"{self.PREFIX} value 65000:100", + ], + ) + + def test_type_by_name(self) -> None: + commands = generate_community_list_community_commands( + "CL", {"community": {"name": "C1", "type": "LARGE"}} + ) + self.assertEqual(commands, [f"{self.PREFIX} type LARGE"]) + + def test_bare_member_is_recreated(self) -> None: + commands = generate_community_list_community_commands( + "CL", {"community": {"name": "C1"}} + ) + self.assertEqual(commands, [self.PREFIX]) + + def test_reference_member_warns(self) -> None: + commands = generate_community_list_community_commands( + "CL", {"community_name": "OTHER"} + ) + self.assertEqual(len(commands), 1) + self.assertTrue(commands[0].startswith("# WARNING:")) + self.assertIn("OTHER", commands[0]) + + def test_members_emitted_inside_list(self) -> None: + """Members follow their list's own attributes in the list generator.""" + commands = generate_community_list_commands( + { + "name": "CL", + "exact_match": True, + "members": [{"community": {"name": "C1", "value": "65000:1"}}], + } + ) + self.assertEqual( + commands, + [ + "config protocol bgp policy community-list CL exact-match true", + f"{self.PREFIX} value 65000:1", + ], + ) + + class GenerateScriptCommandsTest(unittest.TestCase): """Tests for generate_exec_commands."""