diff --git a/cmake/CliFboss2.cmake b/cmake/CliFboss2.cmake index f081e383cbbda..b69c32eead0e5 100644 --- a/cmake/CliFboss2.cmake +++ b/cmake/CliFboss2.cmake @@ -844,6 +844,8 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/config/interface/ipv6/ndp/CmdConfigInterfaceIpv6Ndp.h fboss/cli/fboss2/commands/config/interface/pfc_config/CmdConfigInterfacePfcConfig.cpp fboss/cli/fboss2/commands/config/interface/pfc_config/CmdConfigInterfacePfcConfig.h + fboss/cli/fboss2/commands/config/interface/sflow/CmdConfigInterfaceSflow.cpp + fboss/cli/fboss2/commands/config/interface/sflow/CmdConfigInterfaceSflow.h fboss/cli/fboss2/commands/config/interface/pfc_config/PfcConfigUtils.h fboss/cli/fboss2/commands/config/interface/switchport/CmdConfigInterfaceSwitchport.cpp fboss/cli/fboss2/commands/config/interface/switchport/CmdConfigInterfaceSwitchport.h @@ -1084,6 +1086,8 @@ add_library(fboss2_config_lib fboss/cli/fboss2/commands/delete/interface/ipv6/CmdDeleteInterfaceIpv6.h fboss/cli/fboss2/commands/delete/interface/ipv6/ndp/CmdDeleteInterfaceIpv6Ndp.cpp fboss/cli/fboss2/commands/delete/interface/ipv6/ndp/CmdDeleteInterfaceIpv6Ndp.h + 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/CmdDeleteProtocol.h fboss/cli/fboss2/commands/delete/protocol/static/CmdDeleteProtocolStatic.cpp diff --git a/cmake/CliFboss2TestConfig.cmake b/cmake/CliFboss2TestConfig.cmake index d0e15d2497718..7adc14d972962 100644 --- a/cmake/CliFboss2TestConfig.cmake +++ b/cmake/CliFboss2TestConfig.cmake @@ -15,6 +15,7 @@ add_executable(fboss2_cmd_config_test fboss/cli/fboss2/test/config/CmdConfigHostnameTest.cpp fboss/cli/fboss2/test/config/CmdConfigHistoryTest.cpp fboss/cli/fboss2/test/config/CmdConfigInterfaceIpv6NdpTest.cpp + fboss/cli/fboss2/test/config/CmdConfigInterfaceSflowSampleDestTest.cpp fboss/cli/fboss2/test/config/CmdConfigInterfaceSwitchportAccessVlanTest.cpp fboss/cli/fboss2/test/config/CmdConfigInterfaceSwitchportTrunkAllowedVlanTest.cpp fboss/cli/fboss2/test/config/CmdConfigInterfaceTest.cpp diff --git a/cmake/CliFboss2TestIntegrationTest.cmake b/cmake/CliFboss2TestIntegrationTest.cmake index 1873293ac8710..a7fef710e319f 100644 --- a/cmake/CliFboss2TestIntegrationTest.cmake +++ b/cmake/CliFboss2TestIntegrationTest.cmake @@ -11,6 +11,8 @@ add_executable(fboss2_integration_test fboss/cli/fboss2/test/integration_test/ConfigAdminDistanceTest.cpp fboss/cli/fboss2/test/integration_test/ConfigAclRuleTest.cpp 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/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 7d0a8d82a186a..6e622190b8d73 100644 --- a/fboss/cli/fboss2/BUCK +++ b/fboss/cli/fboss2/BUCK @@ -1091,6 +1091,7 @@ cpp_library( "commands/config/interface/ipv6/CmdConfigInterfaceIpv6.cpp", "commands/config/interface/ipv6/ndp/CmdConfigInterfaceIpv6Ndp.cpp", "commands/config/interface/pfc_config/CmdConfigInterfacePfcConfig.cpp", + "commands/config/interface/sflow/CmdConfigInterfaceSflow.cpp", "commands/config/interface/switchport/CmdConfigInterfaceSwitchport.cpp", "commands/config/interface/switchport/access/CmdConfigInterfaceSwitchportAccess.cpp", "commands/config/interface/switchport/access/vlan/CmdConfigInterfaceSwitchportAccessVlan.cpp", @@ -1210,6 +1211,7 @@ cpp_library( "commands/delete/interface/CmdDeleteInterface.cpp", "commands/delete/interface/ipv6/CmdDeleteInterfaceIpv6.cpp", "commands/delete/interface/ipv6/ndp/CmdDeleteInterfaceIpv6Ndp.cpp", + "commands/delete/interface/sflow/CmdDeleteInterfaceSflow.cpp", "commands/delete/protocol/CmdDeleteProtocol.cpp", "commands/delete/protocol/static/CmdDeleteProtocolStatic.cpp", "commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.cpp", @@ -1248,6 +1250,7 @@ cpp_library( "commands/config/interface/ipv6/CmdConfigInterfaceIpv6.h", "commands/config/interface/ipv6/ndp/CmdConfigInterfaceIpv6Ndp.h", "commands/config/interface/pfc_config/CmdConfigInterfacePfcConfig.h", + "commands/config/interface/sflow/CmdConfigInterfaceSflow.h", "commands/config/interface/pfc_config/PfcConfigUtils.h", "commands/config/interface/switchport/CmdConfigInterfaceSwitchport.h", "commands/config/interface/switchport/access/CmdConfigInterfaceSwitchportAccess.h", @@ -1352,6 +1355,7 @@ cpp_library( "commands/delete/interface/CmdDeleteInterface.h", "commands/delete/interface/ipv6/CmdDeleteInterfaceIpv6.h", "commands/delete/interface/ipv6/ndp/CmdDeleteInterfaceIpv6Ndp.h", + "commands/delete/interface/sflow/CmdDeleteInterfaceSflow.h", "commands/delete/protocol/CmdDeleteProtocol.h", "commands/delete/protocol/static/CmdDeleteProtocolStatic.h", "commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.h", diff --git a/fboss/cli/fboss2/CmdListConfig.cpp b/fboss/cli/fboss2/CmdListConfig.cpp index 164a2fdca30c9..3740cf7a5139e 100644 --- a/fboss/cli/fboss2/CmdListConfig.cpp +++ b/fboss/cli/fboss2/CmdListConfig.cpp @@ -24,6 +24,7 @@ #include "fboss/cli/fboss2/commands/config/interface/ipv6/CmdConfigInterfaceIpv6.h" #include "fboss/cli/fboss2/commands/config/interface/ipv6/ndp/CmdConfigInterfaceIpv6Ndp.h" #include "fboss/cli/fboss2/commands/config/interface/pfc_config/CmdConfigInterfacePfcConfig.h" +#include "fboss/cli/fboss2/commands/config/interface/sflow/CmdConfigInterfaceSflow.h" #include "fboss/cli/fboss2/commands/config/interface/switchport/CmdConfigInterfaceSwitchport.h" #include "fboss/cli/fboss2/commands/config/interface/switchport/access/CmdConfigInterfaceSwitchportAccess.h" #include "fboss/cli/fboss2/commands/config/interface/switchport/access/vlan/CmdConfigInterfaceSwitchportAccessVlan.h" @@ -139,6 +140,7 @@ #include "fboss/cli/fboss2/commands/delete/interface/CmdDeleteInterface.h" #include "fboss/cli/fboss2/commands/delete/interface/ipv6/CmdDeleteInterfaceIpv6.h" #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/static/CmdDeleteProtocolStatic.h" #include "fboss/cli/fboss2/commands/delete/protocol/static/route/CmdDeleteProtocolStaticRoute.h" @@ -278,6 +280,13 @@ const CommandTree& kConfigCommandTree() { argRegistrar, }}, }, + { + "sflow", + "Configure sFlow settings: sample-dest , " + "ingress-rate , egress-rate ", + commandHandler, + argRegistrar, + }, { "switchport", "Configure switchport settings", @@ -1086,17 +1095,24 @@ const CommandTree& kConfigCommandTree() { commandHandler, argRegistrar, {{ - "ipv6", - "Delete (reset to default) IPv6 settings for interface", - commandHandler, - argTypeHandler, - {{ - "ndp", - "Reset IPv6 Neighbor Discovery (NDP/RA) settings to defaults", - commandHandler, - argRegistrar, - }}, - }}, + "ipv6", + "Delete (reset to default) IPv6 settings for interface", + commandHandler, + argTypeHandler, + {{ + "ndp", + "Reset IPv6 Neighbor Discovery (NDP/RA) settings to defaults", + commandHandler, + argRegistrar, + }}, + }, + { + "sflow", + "Delete (reset to default) sFlow settings for interface: " + "sample-dest, ingress-rate, egress-rate", + commandHandler, + argRegistrar, + }}, }, { diff --git a/fboss/cli/fboss2/commands/config/interface/sflow/CmdConfigInterfaceSflow.cpp b/fboss/cli/fboss2/commands/config/interface/sflow/CmdConfigInterfaceSflow.cpp new file mode 100644 index 0000000000000..f025e5a01b758 --- /dev/null +++ b/fboss/cli/fboss2/commands/config/interface/sflow/CmdConfigInterfaceSflow.cpp @@ -0,0 +1,198 @@ +/* + * 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/interface/sflow/CmdConfigInterfaceSflow.h" + +#include "fboss/cli/fboss2/CmdHandler.cpp" + +#include +#include +#include +#include +#include +#include +#include "fboss/agent/gen-cpp2/switch_config_types.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" + +namespace facebook::fboss { + +namespace { +constexpr std::string_view kAttrSampleDest = "sample-dest"; +constexpr std::string_view kAttrIngressRate = "ingress-rate"; +constexpr std::string_view kAttrEgressRate = "egress-rate"; +constexpr std::string_view kSampleDestCpu = "cpu"; +constexpr std::string_view kSampleDestMirror = "mirror"; +constexpr auto kValidSflowAttrs = "sample-dest, ingress-rate, egress-rate"; + +std::string toLower(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { + return std::tolower(c); + }); + return s; +} + +cfg::SampleDestination parseSampleDest(const std::string& token) { + if (token == kSampleDestCpu) { + return cfg::SampleDestination::CPU; + } + if (token == kSampleDestMirror) { + return cfg::SampleDestination::MIRROR; + } + throw std::invalid_argument( + fmt::format( + "Invalid sample destination '{}': must be {} or {}", + token, + kSampleDestCpu, + kSampleDestMirror)); +} + +// sFlowIngressRate/sFlowEgressRate are "every 1/rate packets sampled"; 0 +// disables sampling. Negative values are meaningless and would wrap when +// handed to the SAI SDK's unsigned rate attribute. +int64_t parseSampleRate(const std::string& attr, const std::string& value) { + int64_t rate = 0; + try { + rate = folly::to(value); + } catch (const std::exception&) { + throw std::invalid_argument( + fmt::format("Invalid {} value '{}': must be an integer", attr, value)); + } + if (rate < 0) { + throw std::invalid_argument( + fmt::format( + "Invalid {} value '{}': must be a non-negative integer", + attr, + value)); + } + return rate; +} +} // namespace + +SflowAttrArgs::SflowAttrArgs(std::vector v) { + if (v.empty()) { + throw std::invalid_argument( + fmt::format( + "No sflow attribute provided. Valid attributes are: {}", + kValidSflowAttrs)); + } + attr_ = toLower(v[0]); + if (v.size() != 2) { + throw std::invalid_argument( + fmt::format("Expected exactly one value for '{}'", attr_)); + } + value_ = v[1]; + data_ = std::move(v); +} + +CmdConfigInterfaceSflowTraits::RetType CmdConfigInterfaceSflow::queryClient( + const HostInfo& /* hostInfo */, + const utils::InterfaceList& interfaces, + const ObjectArgType& sflowAttr) { + if (interfaces.empty()) { + throw std::invalid_argument("No interface name provided"); + } + + const std::string& attr = sflowAttr.attr(); + if (attr != kAttrSampleDest && attr != kAttrIngressRate && + attr != kAttrEgressRate) { + throw std::invalid_argument( + fmt::format( + "Unknown sflow attribute '{}'. Valid attributes are: {}", + attr, + kValidSflowAttrs)); + } + + cfg::SampleDestination dest = cfg::SampleDestination::CPU; + int64_t rate = 0; + std::string displayValue; + if (attr == kAttrSampleDest) { + displayValue = toLower(sflowAttr.value()); + dest = parseSampleDest(displayValue); + } else { + rate = parseSampleRate(attr, sflowAttr.value()); + displayValue = folly::to(rate); + } + + auto& session = ConfigSession::getInstance(); + + std::vector updatedNames; + std::vector skippedNames; + for (const utils::Intf& intf : interfaces) { + cfg::Port* port = intf.getPort(); + if (!port) { + // Resolved as an L3 interface only (e.g. an SVI): these sflow + // attributes are all Port attributes, so there is nothing to set -- + // report it rather than silently succeeding. + skippedNames.push_back(intf.name()); + continue; + } + if (attr == kAttrSampleDest) { + // The agent rejects egress sampling to a mirror destination + // (ApplyThriftConfig throws for MIRROR + sFlowEgressRate > 0); fail + // here with a targeted message before touching the config. + if (dest == cfg::SampleDestination::MIRROR && + *port->sFlowEgressRate() > 0) { + throw std::invalid_argument( + fmt::format( + "Port {}: sample-dest {} requires sFlowEgressRate 0 — egress " + "sampling to a mirror destination is unsupported", + *port->name(), + kSampleDestMirror)); + } + port->sampleDest() = dest; + } else if (attr == kAttrIngressRate) { + port->sFlowIngressRate() = rate; + } else { + // Same MIRROR/egress-rate constraint as above, checked from the other + // side: refuse a non-zero egress-rate on a port whose sampleDest is + // already MIRROR. + if (rate > 0 && port->sampleDest().has_value() && + *port->sampleDest() == cfg::SampleDestination::MIRROR) { + throw std::invalid_argument( + fmt::format( + "Port {}: egress-rate must be 0 while sample-dest is {} — " + "egress sampling to a mirror destination is unsupported", + *port->name(), + kSampleDestMirror)); + } + port->sFlowEgressRate() = rate; + } + updatedNames.push_back(intf.name()); + } + if (updatedNames.empty()) { + throw std::invalid_argument("No port found for the specified interface(s)"); + } + + session.saveConfig(cli::ServiceType::AGENT, cli::ConfigActionLevel::HITLESS); + + std::string attrLabel = attr == kAttrSampleDest + ? "sample destination" + : (attr == kAttrIngressRate ? "ingress-rate" : "egress-rate"); + std::string message = fmt::format( + "Successfully set sFlow {} for interface(s) {} to {}", + attrLabel, + folly::join(", ", updatedNames), + displayValue); + if (!skippedNames.empty()) { + message += + fmt::format("; skipped (no port): {}", folly::join(", ", skippedNames)); + } + return message; +} + +void CmdConfigInterfaceSflow::printOutput(const RetType& logMsg) { + std::cout << logMsg << std::endl; +} + +// Explicit template instantiation +template void +CmdHandler::run(); + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/config/interface/sflow/CmdConfigInterfaceSflow.h b/fboss/cli/fboss2/commands/config/interface/sflow/CmdConfigInterfaceSflow.h new file mode 100644 index 0000000000000..ecbe89f144d73 --- /dev/null +++ b/fboss/cli/fboss2/commands/config/interface/sflow/CmdConfigInterfaceSflow.h @@ -0,0 +1,70 @@ +/* + * 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/config/interface/CmdConfigInterface.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/InterfaceList.h" + +namespace facebook::fboss { + +// Parses the two positional tokens of +// config interface sflow +// where is one of: sample-dest, ingress-rate, egress-rate. +class SflowAttrArgs : public utils::BaseObjectArgType { + public: + /* implicit */ SflowAttrArgs( // NOLINT(google-explicit-constructor) + std::vector v); + + const std::string& attr() const { + return attr_; + } + const std::string& value() const { + return value_; + } + + private: + std::string attr_; + std::string value_; +}; + +struct CmdConfigInterfaceSflowTraits : public WriteCommandTraits { + using ParentCmd = CmdConfigInterface; + static void addCliArg(CLI::App& cmd, std::vector& args) { + cmd.add_option( + "sflow_attr", + args, + " where is one of: sample-dest, ingress-rate, " + "egress-rate"); + } + using ObjectArgType = SflowAttrArgs; + using RetType = std::string; +}; + +class CmdConfigInterfaceSflow : public CmdHandler< + CmdConfigInterfaceSflow, + CmdConfigInterfaceSflowTraits> { + public: + using ObjectArgType = CmdConfigInterfaceSflowTraits::ObjectArgType; + using RetType = CmdConfigInterfaceSflowTraits::RetType; + + RetType queryClient( + const HostInfo& hostInfo, + const utils::InterfaceList& interfaces, + const ObjectArgType& sflowAttr); + + void printOutput(const RetType& logMsg); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/delete/interface/sflow/CmdDeleteInterfaceSflow.cpp b/fboss/cli/fboss2/commands/delete/interface/sflow/CmdDeleteInterfaceSflow.cpp new file mode 100644 index 0000000000000..d930de1f35d7b --- /dev/null +++ b/fboss/cli/fboss2/commands/delete/interface/sflow/CmdDeleteInterfaceSflow.cpp @@ -0,0 +1,116 @@ +/* + * 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/interface/sflow/CmdDeleteInterfaceSflow.h" + +#include "fboss/cli/fboss2/CmdHandler.cpp" + +#include +#include +#include +#include +#include +#include "fboss/agent/gen-cpp2/switch_config_types.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" + +namespace facebook::fboss { + +namespace { +constexpr std::string_view kAttrSampleDest = "sample-dest"; +constexpr std::string_view kAttrIngressRate = "ingress-rate"; +constexpr std::string_view kAttrEgressRate = "egress-rate"; +constexpr auto kValidSflowAttrs = "sample-dest, ingress-rate, egress-rate"; +} // namespace + +SflowDeleteAttrArg::SflowDeleteAttrArg(std::vector v) { + if (v.size() != 1) { + throw std::invalid_argument( + fmt::format( + "Expected exactly one sflow attribute to reset. Valid " + "attributes are: {}", + kValidSflowAttrs)); + } + attr_ = v[0]; + std::transform( + attr_.begin(), attr_.end(), attr_.begin(), [](unsigned char c) { + return std::tolower(c); + }); + data_ = std::move(v); +} + +CmdDeleteInterfaceSflowTraits::RetType CmdDeleteInterfaceSflow::queryClient( + const HostInfo& /* hostInfo */, + const utils::InterfaceList& interfaces, + const ObjectArgType& sflowAttr) { + if (interfaces.empty()) { + throw std::invalid_argument("No interface name provided"); + } + + const std::string& attr = sflowAttr.attr(); + if (attr != kAttrSampleDest && attr != kAttrIngressRate && + attr != kAttrEgressRate) { + throw std::invalid_argument( + fmt::format( + "Unknown sflow attribute '{}'. Valid attributes are: {}", + attr, + kValidSflowAttrs)); + } + + auto& session = ConfigSession::getInstance(); + + std::vector updatedNames; + std::vector skippedNames; + for (const utils::Intf& intf : interfaces) { + cfg::Port* port = intf.getPort(); + if (!port) { + // Resolved as an L3 interface only (e.g. an SVI): these sflow + // attributes are all Port attributes, so there is nothing to clear -- + // report it rather than silently succeeding. + skippedNames.push_back(intf.name()); + continue; + } + if (attr == kAttrSampleDest) { + port->sampleDest().reset(); + } else if (attr == kAttrIngressRate) { + port->sFlowIngressRate() = 0; + } else { + port->sFlowEgressRate() = 0; + } + updatedNames.push_back(intf.name()); + } + if (updatedNames.empty()) { + throw std::invalid_argument("No port found for the specified interface(s)"); + } + + session.saveConfig(cli::ServiceType::AGENT, cli::ConfigActionLevel::HITLESS); + + std::string attrLabel = attr == kAttrSampleDest + ? "sample destination" + : (attr == kAttrIngressRate ? "ingress-rate" : "egress-rate"); + std::string message = fmt::format( + "Reset sFlow {} for interface(s) {}", + attrLabel, + folly::join(", ", updatedNames)); + if (!skippedNames.empty()) { + message += + fmt::format("; skipped (no port): {}", folly::join(", ", skippedNames)); + } + return message; +} + +void CmdDeleteInterfaceSflow::printOutput(const RetType& logMsg) { + std::cout << logMsg << std::endl; +} + +// Explicit template instantiation +template void +CmdHandler::run(); + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/commands/delete/interface/sflow/CmdDeleteInterfaceSflow.h b/fboss/cli/fboss2/commands/delete/interface/sflow/CmdDeleteInterfaceSflow.h new file mode 100644 index 0000000000000..1e9c600f9defe --- /dev/null +++ b/fboss/cli/fboss2/commands/delete/interface/sflow/CmdDeleteInterfaceSflow.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 "fboss/cli/fboss2/CmdHandler.h" +#include "fboss/cli/fboss2/commands/delete/interface/CmdDeleteInterface.h" +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" +#include "fboss/cli/fboss2/utils/InterfaceList.h" + +namespace facebook::fboss { + +// Parses the single positional token of +// delete interface sflow +// where is one of: sample-dest, ingress-rate, egress-rate. +class SflowDeleteAttrArg : public utils::BaseObjectArgType { + public: + /* implicit */ SflowDeleteAttrArg( // NOLINT(google-explicit-constructor) + std::vector v); + + const std::string& attr() const { + return attr_; + } + + private: + std::string attr_; +}; + +struct CmdDeleteInterfaceSflowTraits : public WriteCommandTraits { + using ParentCmd = CmdDeleteInterface; + static void addCliArg(CLI::App& cmd, std::vector& args) { + cmd.add_option( + "sflow_attr", + args, + " - which sflow attribute to reset: sample-dest, " + "ingress-rate, egress-rate"); + } + using ObjectArgType = SflowDeleteAttrArg; + using RetType = std::string; +}; + +class CmdDeleteInterfaceSflow : public CmdHandler< + CmdDeleteInterfaceSflow, + CmdDeleteInterfaceSflowTraits> { + public: + using ObjectArgType = CmdDeleteInterfaceSflowTraits::ObjectArgType; + using RetType = CmdDeleteInterfaceSflowTraits::RetType; + + RetType queryClient( + const HostInfo& hostInfo, + const utils::InterfaceList& interfaces, + const ObjectArgType& sflowAttr); + + void printOutput(const RetType& logMsg); +}; + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/config/BUCK b/fboss/cli/fboss2/test/config/BUCK index ed105d2d8998b..0ad15fc526ab4 100644 --- a/fboss/cli/fboss2/test/config/BUCK +++ b/fboss/cli/fboss2/test/config/BUCK @@ -17,6 +17,7 @@ cpp_unittest( "CmdConfigHostnameTest.cpp", "CmdConfigIcmpV4UnavailableSrcAddrTest.cpp", "CmdConfigInterfaceIpv6NdpTest.cpp", + "CmdConfigInterfaceSflowSampleDestTest.cpp", "CmdConfigInterfaceSwitchportAccessVlanTest.cpp", "CmdConfigInterfaceSwitchportTrunkAllowedVlanTest.cpp", "CmdConfigInterfaceTest.cpp", diff --git a/fboss/cli/fboss2/test/config/CmdConfigInterfaceSflowSampleDestTest.cpp b/fboss/cli/fboss2/test/config/CmdConfigInterfaceSflowSampleDestTest.cpp new file mode 100644 index 0000000000000..bfe7a94df0db9 --- /dev/null +++ b/fboss/cli/fboss2/test/config/CmdConfigInterfaceSflowSampleDestTest.cpp @@ -0,0 +1,349 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include "fboss/cli/fboss2/test/config/CmdConfigTestBase.h" + +#include +#include +#include +#include +#include + +#include "fboss/cli/fboss2/commands/config/interface/sflow/CmdConfigInterfaceSflow.h" +#include "fboss/cli/fboss2/commands/delete/interface/sflow/CmdDeleteInterfaceSflow.h" +#include "fboss/cli/fboss2/session/ConfigSession.h" +#include "fboss/cli/fboss2/utils/InterfaceList.h" + +using namespace ::testing; + +namespace facebook::fboss { + +class CmdConfigInterfaceSflowSampleDestTestFixture : public CmdConfigTestBase { + public: + // Production ports carry no sampleDest (unset optional); eth1/2/1 has a + // non-zero sFlowEgressRate to exercise the mirror-destination restriction. + CmdConfigInterfaceSflowSampleDestTestFixture() + : CmdConfigTestBase( + "fboss_sflow_sample_dest_test_%%%%-%%%%-%%%%-%%%%", + R"({ + "sw": { + "ports": [ + { + "logicalID": 1, + "name": "eth1/1/1", + "state": 2, + "speed": 100000, + "ingressVlan": 1, + "sFlowIngressRate": 0, + "sFlowEgressRate": 0 + }, + { + "logicalID": 2, + "name": "eth1/2/1", + "state": 2, + "speed": 100000, + "ingressVlan": 1, + "sFlowIngressRate": 512, + "sFlowEgressRate": 512 + } + ], + "vlans": [ + {"id": 1, "name": "vlan1", "routable": true, "intfID": 1} + ], + "interfaces": [ + {"intfID": 1, "vlanID": 1, "routerID": 0, "type": 1, "mtu": 9412, "name": "vlan1"} + ] + } +})") {} + + void SetUp() override { + CmdConfigTestBase::SetUp(); + setupTestableConfigSession( + "config interface sflow sample-dest eth1/1/1", "cpu"); + } + + static std::optional sampleDestOf( + const std::string& portName) { + auto& swConfig = *ConfigSession::getInstance().getAgentConfig().sw(); + for (const auto& port : *swConfig.ports()) { + if (*port.name() == portName) { + return port.sampleDest().to_optional(); + } + } + throw std::runtime_error("port not found: " + portName); + } + + static int64_t ingressRateOf(const std::string& portName) { + auto& swConfig = *ConfigSession::getInstance().getAgentConfig().sw(); + for (const auto& port : *swConfig.ports()) { + if (*port.name() == portName) { + return *port.sFlowIngressRate(); + } + } + throw std::runtime_error("port not found: " + portName); + } + + static int64_t egressRateOf(const std::string& portName) { + auto& swConfig = *ConfigSession::getInstance().getAgentConfig().sw(); + for (const auto& port : *swConfig.ports()) { + if (*port.name() == portName) { + return *port.sFlowEgressRate(); + } + } + throw std::runtime_error("port not found: " + portName); + } +}; + +// SflowAttrArgs / SflowDeleteAttrArg arity validation + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, configArgsWrongArity) { + EXPECT_THROW(SflowAttrArgs({}), std::invalid_argument); + EXPECT_THROW(SflowAttrArgs({"sample-dest"}), std::invalid_argument); + EXPECT_THROW( + SflowAttrArgs({"sample-dest", "cpu", "extra"}), std::invalid_argument); +} + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, deleteArgsWrongArity) { + EXPECT_THROW(SflowDeleteAttrArg({}), std::invalid_argument); + EXPECT_THROW( + SflowDeleteAttrArg({"sample-dest", "extra"}), std::invalid_argument); +} + +// config queryClient + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, setCpu) { + ASSERT_EQ(sampleDestOf("eth1/1/1"), std::nullopt); + + auto cmd = CmdConfigInterfaceSflow(); + utils::InterfaceList interfaces({"eth1/1/1"}); + auto result = cmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"sample-dest", "cpu"})); + + EXPECT_THAT(result, HasSubstr("eth1/1/1")); + EXPECT_THAT(result, HasSubstr("cpu")); + EXPECT_EQ(sampleDestOf("eth1/1/1"), cfg::SampleDestination::CPU); + // The other port is untouched. + EXPECT_EQ(sampleDestOf("eth1/2/1"), std::nullopt); +} + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, setMirror) { + auto cmd = CmdConfigInterfaceSflow(); + utils::InterfaceList interfaces({"eth1/1/1"}); + cmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"sample-dest", "mirror"})); + + EXPECT_EQ(sampleDestOf("eth1/1/1"), cfg::SampleDestination::MIRROR); +} + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, valueCaseInsensitive) { + auto cmd = CmdConfigInterfaceSflow(); + utils::InterfaceList interfaces({"eth1/1/1"}); + cmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"SAMPLE-DEST", "CPU"})); + + EXPECT_EQ(sampleDestOf("eth1/1/1"), cfg::SampleDestination::CPU); +} + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, valueInvalid) { + auto cmd = CmdConfigInterfaceSflow(); + utils::InterfaceList interfaces({"eth1/1/1"}); + EXPECT_THROW( + cmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"sample-dest", "collector"})), + std::invalid_argument); +} + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, unknownAttr) { + auto cmd = CmdConfigInterfaceSflow(); + utils::InterfaceList interfaces({"eth1/1/1"}); + EXPECT_THROW( + cmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"bogus-attr", "100"})), + std::invalid_argument); +} + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, setIngressRate) { + ASSERT_EQ(ingressRateOf("eth1/1/1"), 0); + + auto cmd = CmdConfigInterfaceSflow(); + utils::InterfaceList interfaces({"eth1/1/1"}); + auto result = cmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"ingress-rate", "256"})); + + EXPECT_THAT(result, HasSubstr("eth1/1/1")); + EXPECT_THAT(result, HasSubstr("256")); + EXPECT_EQ(ingressRateOf("eth1/1/1"), 256); + // The other port is untouched. + EXPECT_EQ(ingressRateOf("eth1/2/1"), 512); +} + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, setEgressRate) { + auto cmd = CmdConfigInterfaceSflow(); + utils::InterfaceList interfaces({"eth1/1/1"}); + auto result = cmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"egress-rate", "256"})); + + EXPECT_THAT(result, HasSubstr("256")); + EXPECT_EQ(egressRateOf("eth1/1/1"), 256); +} + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, rateValueInvalid) { + auto cmd = CmdConfigInterfaceSflow(); + utils::InterfaceList interfaces({"eth1/1/1"}); + EXPECT_THROW( + cmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"ingress-rate", "abc"})), + std::invalid_argument); + EXPECT_THROW( + cmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"egress-rate", "-5"})), + std::invalid_argument); +} + +TEST_F( + CmdConfigInterfaceSflowSampleDestTestFixture, + egressRateRefusedWhenMirror) { + // eth1/1/1 starts with sFlowEgressRate 0, so mirror is accepted first. + auto cmd = CmdConfigInterfaceSflow(); + utils::InterfaceList interfaces({"eth1/1/1"}); + cmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"sample-dest", "mirror"})); + ASSERT_EQ(sampleDestOf("eth1/1/1"), cfg::SampleDestination::MIRROR); + + // A non-zero egress-rate now conflicts with the existing mirror + // destination, same constraint as setting mirror onto a non-zero rate. + EXPECT_THROW( + cmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"egress-rate", "100"})), + std::invalid_argument); + EXPECT_EQ(egressRateOf("eth1/1/1"), 0); + + // Zero is always fine. + cmd.queryClient(localhost(), interfaces, SflowAttrArgs({"egress-rate", "0"})); + EXPECT_EQ(egressRateOf("eth1/1/1"), 0); +} + +TEST_F( + CmdConfigInterfaceSflowSampleDestTestFixture, + mirrorRefusedWithEgressSampling) { + // eth1/2/1 has sFlowEgressRate 512; the agent rejects MIRROR with egress + // sampling, so the CLI must refuse before touching the config. + auto cmd = CmdConfigInterfaceSflow(); + utils::InterfaceList interfaces({"eth1/2/1"}); + EXPECT_THROW( + cmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"sample-dest", "mirror"})), + std::invalid_argument); + EXPECT_EQ(sampleDestOf("eth1/2/1"), std::nullopt); + + // CPU is fine on the same port. + cmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"sample-dest", "cpu"})); + EXPECT_EQ(sampleDestOf("eth1/2/1"), cfg::SampleDestination::CPU); +} + +TEST_F( + CmdConfigInterfaceSflowSampleDestTestFixture, + mixedListReportsSkippedNonPortNames) { + // vlan1 resolves only as an L3 interface — sampleDest is a Port attribute, + // so the command must name it as skipped instead of silently succeeding. + auto cmd = CmdConfigInterfaceSflow(); + utils::InterfaceList interfaces({"eth1/1/1", "vlan1"}); + auto result = cmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"sample-dest", "cpu"})); + + EXPECT_THAT(result, HasSubstr("skipped (no port): vlan1")); + EXPECT_EQ(sampleDestOf("eth1/1/1"), cfg::SampleDestination::CPU); + + auto deleteCmd = CmdDeleteInterfaceSflow(); + auto deleteResult = deleteCmd.queryClient( + localhost(), interfaces, SflowDeleteAttrArg({"sample-dest"})); + EXPECT_THAT(deleteResult, HasSubstr("skipped (no port): vlan1")); + EXPECT_EQ(sampleDestOf("eth1/1/1"), std::nullopt); +} + +TEST_F( + CmdConfigInterfaceSflowSampleDestTestFixture, + setThrowsOnEmptyInterfaceList) { + auto cmd = CmdConfigInterfaceSflow(); + utils::InterfaceList emptyInterfaces({}); + EXPECT_THROW( + cmd.queryClient( + localhost(), emptyInterfaces, SflowAttrArgs({"sample-dest", "cpu"})), + std::invalid_argument); +} + +// delete queryClient + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, deleteClearsSampleDest) { + auto configCmd = CmdConfigInterfaceSflow(); + utils::InterfaceList interfaces({"eth1/1/1"}); + configCmd.queryClient( + localhost(), interfaces, SflowAttrArgs({"sample-dest", "cpu"})); + ASSERT_EQ(sampleDestOf("eth1/1/1"), cfg::SampleDestination::CPU); + + auto deleteCmd = CmdDeleteInterfaceSflow(); + auto result = deleteCmd.queryClient( + localhost(), interfaces, SflowDeleteAttrArg({"sample-dest"})); + + EXPECT_THAT(result, HasSubstr("Reset sFlow sample destination")); + EXPECT_THAT(result, HasSubstr("eth1/1/1")); + EXPECT_EQ(sampleDestOf("eth1/1/1"), std::nullopt); +} + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, deleteIsIdempotent) { + auto cmd = CmdDeleteInterfaceSflow(); + utils::InterfaceList interfaces({"eth1/1/1"}); + + cmd.queryClient(localhost(), interfaces, SflowDeleteAttrArg({"sample-dest"})); + cmd.queryClient(localhost(), interfaces, SflowDeleteAttrArg({"sample-dest"})); + + EXPECT_EQ(sampleDestOf("eth1/1/1"), std::nullopt); +} + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, deleteUnknownAttrThrows) { + auto cmd = CmdDeleteInterfaceSflow(); + utils::InterfaceList interfaces({"eth1/1/1"}); + EXPECT_THROW( + cmd.queryClient( + localhost(), interfaces, SflowDeleteAttrArg({"bogus-attr"})), + std::invalid_argument); +} + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, deleteClearsIngressRate) { + ASSERT_EQ(ingressRateOf("eth1/2/1"), 512); + utils::InterfaceList interfaces({"eth1/2/1"}); + + auto deleteCmd = CmdDeleteInterfaceSflow(); + auto result = deleteCmd.queryClient( + localhost(), interfaces, SflowDeleteAttrArg({"ingress-rate"})); + + EXPECT_THAT(result, HasSubstr("Reset sFlow ingress-rate")); + EXPECT_EQ(ingressRateOf("eth1/2/1"), 0); + // egress-rate is untouched. + EXPECT_EQ(egressRateOf("eth1/2/1"), 512); +} + +TEST_F(CmdConfigInterfaceSflowSampleDestTestFixture, deleteClearsEgressRate) { + ASSERT_EQ(egressRateOf("eth1/2/1"), 512); + utils::InterfaceList interfaces({"eth1/2/1"}); + + auto deleteCmd = CmdDeleteInterfaceSflow(); + auto result = deleteCmd.queryClient( + localhost(), interfaces, SflowDeleteAttrArg({"egress-rate"})); + + EXPECT_THAT(result, HasSubstr("Reset sFlow egress-rate")); + EXPECT_EQ(egressRateOf("eth1/2/1"), 0); +} + +TEST_F( + CmdConfigInterfaceSflowSampleDestTestFixture, + deleteThrowsOnEmptyInterfaceList) { + auto cmd = CmdDeleteInterfaceSflow(); + utils::InterfaceList emptyInterfaces({}); + EXPECT_THROW( + cmd.queryClient( + localhost(), emptyInterfaces, SflowDeleteAttrArg({"sample-dest"})), + std::invalid_argument); +} + +} // namespace facebook::fboss diff --git a/fboss/cli/fboss2/test/integration_test/BUCK b/fboss/cli/fboss2/test/integration_test/BUCK index b6ec5b952c37b..b394f1b41d2a8 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", + "ConfigSflowRateTest.cpp", + "ConfigSflowSampleDestTest.cpp", "ConfigConcurrentSessionsTest.cpp", "ConfigCoppTest.cpp", "ConfigDhcpSourceOverrideTest.cpp", diff --git a/fboss/cli/fboss2/test/integration_test/ConfigSflowRateTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigSflowRateTest.cpp new file mode 100644 index 0000000000000..fb2ce1416a1e3 --- /dev/null +++ b/fboss/cli/fboss2/test/integration_test/ConfigSflowRateTest.cpp @@ -0,0 +1,161 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +/** + * End-to-end test for: + * fboss2-dev config interface sflow ingress-rate|egress-rate + * fboss2-dev delete interface sflow ingress-rate|egress-rate + * + * Picks a port, sets its ingress-rate and egress-rate, verifies both + * round-trip through the agent's running config, deletes them, and verifies + * the port returns to rate 0 on both. + * + * Requirements: + * - FBOSS agent is running with a valid configuration + * - Test is run as root (or with sudo) on a DUT + */ + +#include +#include +#include +#include "fboss/cli/fboss2/test/integration_test/Fboss2IntegrationTest.h" + +using namespace facebook::fboss; + +namespace { +constexpr int64_t kTestRate = 256; +} // namespace + +class ConfigSflowRateTest : public Fboss2IntegrationTest { + protected: + void TearDown() override { + // If the test failed after committing a non-zero rate but before the + // delete commit, best-effort clear it so a shared DUT is left clean. + if (!committedPort_.empty()) { + try { + auto config = getRunningConfig(); + if (ingressRateOf(config, committedPort_) != 0) { + runCli( + {"delete", "interface", committedPort_, "sflow", "ingress-rate"}); + commitConfig(); + } + config = getRunningConfig(); + if (egressRateOf(config, committedPort_) != 0) { + runCli( + {"delete", "interface", committedPort_, "sflow", "egress-rate"}); + commitConfig(); + } + } catch (const std::exception&) { + // Best-effort; do not mask the test result. + } + } + Fboss2IntegrationTest::TearDown(); + } + + std::string committedPort_; + + static int64_t ingressRateOf( + const folly::dynamic& config, + const std::string& portName) { + return rateOf(config, portName, "sFlowIngressRate"); + } + + static int64_t egressRateOf( + const folly::dynamic& config, + const std::string& portName) { + return rateOf(config, portName, "sFlowEgressRate"); + } + + static int64_t rateOf( + const folly::dynamic& config, + const std::string& portName, + const std::string& field) { + for (const auto& p : config["sw"]["ports"]) { + if (p.count("name") && p["name"].asString() == portName) { + return p.count(field) ? p[field].asInt() : 0; + } + } + return 0; + } + + static std::string pickAnyPort(const folly::dynamic& config) { + for (const auto& p : config["sw"]["ports"]) { + if (p.count("name")) { + return p["name"].asString(); + } + } + return ""; + } + + void runCliOkOrDiscard(const std::vector& args) { + auto result = runCli(args); + if (result.exitCode != 0) { + discardSession(); + } + ASSERT_EQ(result.exitCode, 0) + << "stdout=" << result.stdout << " stderr=" << result.stderr; + } +}; + +TEST_F(ConfigSflowRateTest, SetThenDeleteIngressAndEgressRate) { + auto initial = getRunningConfig(); + ASSERT_TRUE(initial.isObject() && initial.count("sw")); + + const std::string portName = pickAnyPort(initial); + ASSERT_FALSE(portName.empty()) << "no ports in running config"; + committedPort_ = portName; + + XLOG(INFO) << "[Step 1] config interface " << portName + << " sflow ingress-rate " << kTestRate; + runCliOkOrDiscard( + {"config", + "interface", + portName, + "sflow", + "ingress-rate", + std::to_string(kTestRate)}); + commitConfig(); + waitForAgentReady(); + { + auto config = waitForRunningConfig([&](const folly::dynamic& c) { + return ingressRateOf(c, portName) == kTestRate; + }); + ASSERT_EQ(ingressRateOf(config, portName), kTestRate) + << "ingress-rate not in running config for " << portName; + } + + XLOG(INFO) << "[Step 2] config interface " << portName + << " sflow egress-rate " << kTestRate; + runCliOkOrDiscard( + {"config", + "interface", + portName, + "sflow", + "egress-rate", + std::to_string(kTestRate)}); + commitConfig(); + waitForAgentReady(); + { + auto config = waitForRunningConfig([&](const folly::dynamic& c) { + return egressRateOf(c, portName) == kTestRate; + }); + ASSERT_EQ(egressRateOf(config, portName), kTestRate) + << "egress-rate not in running config for " << portName; + } + + XLOG(INFO) << "[Step 3] delete interface " << portName + << " sflow ingress-rate, egress-rate"; + runCliOkOrDiscard({"delete", "interface", portName, "sflow", "ingress-rate"}); + commitConfig(); + runCliOkOrDiscard({"delete", "interface", portName, "sflow", "egress-rate"}); + commitConfig(); + waitForAgentReady(); + + XLOG(INFO) << "[Step 4] Verify " << portName << " has rate 0 on both again"; + auto config = waitForRunningConfig([&](const folly::dynamic& c) { + return ingressRateOf(c, portName) == 0 && egressRateOf(c, portName) == 0; + }); + EXPECT_EQ(ingressRateOf(config, portName), 0) + << "ingress-rate still non-zero on " << portName << " after delete"; + EXPECT_EQ(egressRateOf(config, portName), 0) + << "egress-rate still non-zero on " << portName << " after delete"; +} diff --git a/fboss/cli/fboss2/test/integration_test/ConfigSflowSampleDestTest.cpp b/fboss/cli/fboss2/test/integration_test/ConfigSflowSampleDestTest.cpp new file mode 100644 index 0000000000000..9f4c2f2250653 --- /dev/null +++ b/fboss/cli/fboss2/test/integration_test/ConfigSflowSampleDestTest.cpp @@ -0,0 +1,110 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +/** + * End-to-end test for: + * fboss2-dev config interface sflow sample-dest + * fboss2-dev delete interface sflow sample-dest + * + * Picks a port without a sampleDest, sets it to cpu, verifies the value + * round-trips through the agent's running config, deletes it, and verifies + * the port returns exactly to its original (unset) state. + * + * Requirements: + * - FBOSS agent is running with a valid configuration + * - Test is run as root (or with sudo) on a DUT + */ + +#include +#include +#include +#include "fboss/cli/fboss2/test/integration_test/Fboss2IntegrationTest.h" + +using namespace facebook::fboss; + +class ConfigSflowSampleDestTest : public Fboss2IntegrationTest { + protected: + void TearDown() override { + // If the test failed after committing the cpu sample-dest but before the + // delete commit, best-effort clear it so a shared DUT is left clean. + if (!committedPort_.empty()) { + try { + if (sampleDestOf(getRunningConfig(), committedPort_) != -1) { + runCli( + {"delete", "interface", committedPort_, "sflow", "sample-dest"}); + commitConfig(); + } + } catch (const std::exception&) { + // Best-effort; do not mask the test result. + } + } + Fboss2IntegrationTest::TearDown(); + } + + std::string committedPort_; + + // sampleDest of the named port in the given running config, or -1 if unset. + static int sampleDestOf( + const folly::dynamic& config, + const std::string& portName) { + for (const auto& p : config["sw"]["ports"]) { + if (p.count("name") && p["name"].asString() == portName) { + return p.count("sampleDest") ? p["sampleDest"].asInt() : -1; + } + } + return -1; + } + + // Name of the first port with no sampleDest configured. + static std::string pickPortWithoutSampleDest(const folly::dynamic& config) { + for (const auto& p : config["sw"]["ports"]) { + if (p.count("name") && !p.count("sampleDest")) { + return p["name"].asString(); + } + } + return ""; + } + + void runCliOkOrDiscard(const std::vector& args) { + auto result = runCli(args); + if (result.exitCode != 0) { + discardSession(); + } + ASSERT_EQ(result.exitCode, 0) + << "stdout=" << result.stdout << " stderr=" << result.stderr; + } +}; + +TEST_F(ConfigSflowSampleDestTest, SetThenDeleteSampleDest) { + auto initial = getRunningConfig(); + ASSERT_TRUE(initial.isObject() && initial.count("sw")); + + const std::string portName = pickPortWithoutSampleDest(initial); + ASSERT_FALSE(portName.empty()) << "every port already has a sampleDest"; + + XLOG(INFO) << "[Step 1] config interface " << portName + << " sflow sample-dest cpu"; + runCliOkOrDiscard( + {"config", "interface", portName, "sflow", "sample-dest", "cpu"}); + commitConfig(); + committedPort_ = portName; + waitForAgentReady(); + { + auto config = waitForRunningConfig([&](const folly::dynamic& c) { + return sampleDestOf(c, portName) == 0; + }); + ASSERT_EQ(sampleDestOf(config, portName), 0) + << "sampleDest cpu (0) not in running config for " << portName; + } + + XLOG(INFO) << "[Step 2] delete interface " << portName + << " sflow sample-dest"; + runCliOkOrDiscard({"delete", "interface", portName, "sflow", "sample-dest"}); + commitConfig(); + waitForAgentReady(); + + XLOG(INFO) << "[Step 3] Verify " << portName << " has no sampleDest again"; + auto config = waitForRunningConfig( + [&](const folly::dynamic& c) { return sampleDestOf(c, portName) == -1; }); + EXPECT_EQ(sampleDestOf(config, portName), -1) + << "sampleDest still present on " << portName << " after delete"; +}