diff --git a/cmake/CliFboss2.cmake b/cmake/CliFboss2.cmake index b75dc5a2e60d4..ce64af07dd670 100644 --- a/cmake/CliFboss2.cmake +++ b/cmake/CliFboss2.cmake @@ -476,6 +476,7 @@ add_library(fboss2_lib fboss/cli/fboss2/utils/CLIParserUtils.cpp fboss/cli/fboss2/utils/CmdClientUtils.cpp fboss/cli/fboss2/utils/CmdUtilsCommon.cpp + fboss/cli/fboss2/utils/PortMap.cpp fboss/cli/fboss2/utils/Table.cpp fboss/cli/fboss2/utils/HostInfo.h fboss/cli/fboss2/utils/FilterOp.h diff --git a/cmake/CliFboss2Test.cmake b/cmake/CliFboss2Test.cmake index 81327d875fdc1..bdeb5d5c9ee2e 100644 --- a/cmake/CliFboss2Test.cmake +++ b/cmake/CliFboss2Test.cmake @@ -37,6 +37,7 @@ add_executable(fboss2_cmd_test # fboss/cli/fboss2/test/CmdShowTransceiverTest.cpp - excluded (depends on configerator bgp namespace) fboss/cli/fboss2/test/CmdStartPcapTest.cpp fboss/cli/fboss2/test/CmdStopPcapTest.cpp + fboss/cli/fboss2/test/PortMapTest.cpp ) target_link_libraries(fboss2_cmd_test diff --git a/fboss/cli/fboss2/BUCK b/fboss/cli/fboss2/BUCK index 05e6ade9a131e..c59953336628f 100644 --- a/fboss/cli/fboss2/BUCK +++ b/fboss/cli/fboss2/BUCK @@ -313,6 +313,7 @@ cpp_library( "utils/CmdClientUtils.cpp", "utils/CmdUtils.cpp", "utils/NetwhoamiUtils.cpp", + "utils/PortMap.cpp", "utils/PrbsUtils.cpp", "utils/TeFlowUtils.cpp", "utils/clients/BmcClient.cpp", @@ -774,8 +775,15 @@ cpp_library( exported_deps = [ ":cmd-handler", ":fboss2-lib", + "//fboss/agent:agent_config-cpp2-types", + "//fboss/agent:agent_dir_util", + "//fboss/agent:fboss-types", + "//fboss/agent/if:ctrl-cpp2-types", "//folly:conv", ], + exported_external_deps = [ + "glog", + ], ) cpp_binary( diff --git a/fboss/cli/fboss2/test/BUCK b/fboss/cli/fboss2/test/BUCK index 9d97fc24c6b66..b28420b07d244 100644 --- a/fboss/cli/fboss2/test/BUCK +++ b/fboss/cli/fboss2/test/BUCK @@ -87,7 +87,15 @@ cpp_unittest( "CmdShowTransceiverTest.cpp", "CmdStartPcapTest.cpp", "CmdStopPcapTest.cpp", + "PortMapTest.cpp", ], + # Config files for PortMapTest parameterized tests + resources = [ + "//fboss/oss/link_test_configs:link_test_configs", + ], + # Required for parameterized tests (TEST_P) - static listing doesn't support them + # See: https://fburl.com/parameterized-test-not-supported + supports_static_listing = False, deps = [ "fbsource//third-party/googletest:gmock", ":cmd_test_utils", @@ -121,9 +129,12 @@ cpp_unittest( "//fboss/cli/fboss2/commands/show/route:model-cpp2-types", "//fboss/cli/fboss2/commands/show/teflow:model-cpp2-types", "//fboss/cli/fboss2/commands/show/transceiver:model-cpp2-types", + "//folly:file_util", "//folly:network_address", "//folly/json:dynamic", + "//folly/testing:test_util", "//neteng/fboss/bgp/if:bgp_thrift-cpp2-services", + "//thrift/lib/cpp2/protocol:protocol", "//thrift/lib/cpp2/reflection:testing", ], external_deps = [("boost", None, "boost_algorithm")], diff --git a/fboss/cli/fboss2/test/PortMapTest.cpp b/fboss/cli/fboss2/test/PortMapTest.cpp new file mode 100644 index 0000000000000..963221cfbcacc --- /dev/null +++ b/fboss/cli/fboss2/test/PortMapTest.cpp @@ -0,0 +1,392 @@ +/* + * 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/utils/PortMap.h" +#include +#include +#include +#include + +namespace facebook::fboss::utils { + +namespace fs = std::filesystem; + +namespace { + +/** + * Get the full path to a config file given a relative path. + * This abstracts the path resolution between OSS and FB internal environments. + * + * In Buck builds, resources declared in the BUCK file are accessible via + * relative paths from the repo root (the working directory for tests). + * + * @param relativePath Path relative to the fboss repo root, + * e.g. "fboss/oss/link_test_configs/minipack3n.materialized_JSON" + * @return Full path to the config file + */ +std::string getConfigPath(const std::string& relativePath) { + // First, try the relative path directly (works in Buck builds where + // resources are available and the working directory is the repo root) + if (fs::exists(relativePath)) { + return relativePath; + } + +#if IS_OSS + // Fall back to the hardcoded OSS path for CMake builds + std::string ossPath = "/var/FBOSS/fboss/" + relativePath; + if (fs::exists(ossPath)) { + return ossPath; + } +#endif + + // Return the relative path and let the caller handle the error + return relativePath; +} + +/** + * Load a config file using a relative path. + * The path is relative to the fboss repo root. + */ +cfg::AgentConfig loadConfig(const std::string& relativePath) { + std::string configJson; + std::string fullPath = getConfigPath(relativePath); + if (!folly::readFile(fullPath.c_str(), configJson)) { + throw std::runtime_error("Failed to read config file: " + fullPath); + } + + cfg::AgentConfig agentConfig; + apache::thrift::SimpleJSONSerializer::deserialize( + configJson, agentConfig); + return agentConfig; +} + +} // namespace + +class PortMapTest : public ::testing::Test {}; + +// Test with minipack3n config which uses portID attribute +TEST_F(PortMapTest, Minipack3nPortIdMapping) { + auto config = + loadConfig("fboss/oss/link_test_configs/minipack3n.materialized_JSON"); + PortMap portMap(config); + + // Test port name to interface ID mapping using portID attribute + // Interface 2001 has portID 241, which is port eth1/1/1 + auto intfId = portMap.getInterfaceIdForPort("eth1/1/1"); + ASSERT_TRUE(intfId.has_value()); + EXPECT_EQ(*intfId, InterfaceID(2001)); + + // Interface 2002 has portID 243, which is port eth1/1/5 + intfId = portMap.getInterfaceIdForPort("eth1/1/5"); + ASSERT_TRUE(intfId.has_value()); + EXPECT_EQ(*intfId, InterfaceID(2002)); + + // Interface 2003 has portID 245, which is port eth1/2/1 + intfId = portMap.getInterfaceIdForPort("eth1/2/1"); + ASSERT_TRUE(intfId.has_value()); + EXPECT_EQ(*intfId, InterfaceID(2003)); + + // Test reverse mapping + auto portName = portMap.getPortNameForInterface(InterfaceID(2001)); + ASSERT_TRUE(portName.has_value()); + EXPECT_EQ(*portName, "eth1/1/1"); + + portName = portMap.getPortNameForInterface(InterfaceID(2002)); + ASSERT_TRUE(portName.has_value()); + EXPECT_EQ(*portName, "eth1/1/5"); +} + +// Test with montblanc config which uses VLAN-based mapping +TEST_F(PortMapTest, MontblancVlanMapping) { + auto config = + loadConfig("fboss/oss/link_test_configs/montblanc.materialized_JSON"); + PortMap portMap(config); + + // Test VLAN-based mapping + // Port eth1/1/1 has logicalID 9, vlanPorts maps 9 -> vlanID 2001, + // interface 2001 has vlanID 2001 + auto intfId = portMap.getInterfaceIdForPort("eth1/1/1"); + ASSERT_TRUE(intfId.has_value()); + EXPECT_EQ(*intfId, InterfaceID(2001)); + + // Port eth1/2/1 has logicalID 1, vlanPorts maps 1 -> vlanID 2003, + // interface 2003 has vlanID 2003 + intfId = portMap.getInterfaceIdForPort("eth1/2/1"); + ASSERT_TRUE(intfId.has_value()); + EXPECT_EQ(*intfId, InterfaceID(2003)); + + // Port eth1/2/5 has logicalID 5, vlanPorts maps 5 -> vlanID 2004, + // interface 2004 has vlanID 2004 + intfId = portMap.getInterfaceIdForPort("eth1/2/5"); + ASSERT_TRUE(intfId.has_value()); + EXPECT_EQ(*intfId, InterfaceID(2004)); + + // Test reverse mapping + auto portName = portMap.getPortNameForInterface(InterfaceID(2001)); + ASSERT_TRUE(portName.has_value()); + EXPECT_EQ(*portName, "eth1/1/1"); + + portName = portMap.getPortNameForInterface(InterfaceID(2003)); + ASSERT_TRUE(portName.has_value()); + EXPECT_EQ(*portName, "eth1/2/1"); +} + +// Test port existence checks +TEST_F(PortMapTest, PortExistenceChecks) { + auto config = + loadConfig("fboss/oss/link_test_configs/minipack3n.materialized_JSON"); + PortMap portMap(config); + + // Test hasPort + EXPECT_TRUE(portMap.hasPort("eth1/1/1")); + EXPECT_TRUE(portMap.hasPort("eth1/1/5")); + EXPECT_FALSE(portMap.hasPort("eth99/99/99")); + EXPECT_FALSE(portMap.hasPort("nonexistent")); + + // Test hasInterface + EXPECT_TRUE(portMap.hasInterface(InterfaceID(2001))); + EXPECT_TRUE(portMap.hasInterface(InterfaceID(2002))); + EXPECT_FALSE(portMap.hasInterface(InterfaceID(9999))); +} + +// Test port logical ID lookup +TEST_F(PortMapTest, PortLogicalIdLookup) { + auto config = + loadConfig("fboss/oss/link_test_configs/minipack3n.materialized_JSON"); + PortMap portMap(config); + + // Test getPortLogicalId + auto logicalId = portMap.getPortLogicalId("eth1/1/1"); + ASSERT_TRUE(logicalId.has_value()); + EXPECT_EQ(*logicalId, PortID(241)); + + logicalId = portMap.getPortLogicalId("eth1/1/5"); + ASSERT_TRUE(logicalId.has_value()); + EXPECT_EQ(*logicalId, PortID(243)); + + logicalId = portMap.getPortLogicalId("eth1/2/1"); + ASSERT_TRUE(logicalId.has_value()); + EXPECT_EQ(*logicalId, PortID(245)); + + // Test non-existent port + logicalId = portMap.getPortLogicalId("nonexistent"); + EXPECT_FALSE(logicalId.has_value()); +} + +// Test getAllPortNames +TEST_F(PortMapTest, GetAllPortNames) { + auto config = + loadConfig("fboss/oss/link_test_configs/minipack3n.materialized_JSON"); + PortMap portMap(config); + + auto portNames = portMap.getAllPortNames(); + EXPECT_GT(portNames.size(), 0); + + // Check that some expected ports are in the list + bool foundEth1_1_1 = false; + bool foundEth1_1_5 = false; + for (const auto& name : portNames) { + if (name == "eth1/1/1") { + foundEth1_1_1 = true; + } + if (name == "eth1/1/5") { + foundEth1_1_5 = true; + } + } + EXPECT_TRUE(foundEth1_1_1); + EXPECT_TRUE(foundEth1_1_5); +} + +// Test getAllInterfaceIds +TEST_F(PortMapTest, GetAllInterfaceIds) { + auto config = + loadConfig("fboss/oss/link_test_configs/minipack3n.materialized_JSON"); + PortMap portMap(config); + + auto interfaceIds = portMap.getAllInterfaceIds(); + EXPECT_GT(interfaceIds.size(), 0); + + // Check that some expected interfaces are in the list + bool found2001 = false; + bool found2002 = false; + for (const auto& id : interfaceIds) { + if (id == InterfaceID(2001)) { + found2001 = true; + } + if (id == InterfaceID(2002)) { + found2002 = true; + } + } + EXPECT_TRUE(found2001); + EXPECT_TRUE(found2002); +} + +// Test non-existent port/interface lookups +TEST_F(PortMapTest, NonExistentLookups) { + auto config = + loadConfig("fboss/oss/link_test_configs/minipack3n.materialized_JSON"); + PortMap portMap(config); + + // Test non-existent port name + auto intfId = portMap.getInterfaceIdForPort("eth99/99/99"); + EXPECT_FALSE(intfId.has_value()); + + // Test non-existent interface ID + auto portName = portMap.getPortNameForInterface(InterfaceID(9999)); + EXPECT_FALSE(portName.has_value()); +} + +// Test with multiple mapping strategies in montblanc config +TEST_F(PortMapTest, MontblancMultiplePorts) { + auto config = + loadConfig("fboss/oss/link_test_configs/montblanc.materialized_JSON"); + PortMap portMap(config); + + // Test multiple ports to ensure consistency + std::vector> expectedMappings = { + {"eth1/1/1", InterfaceID(2001)}, + {"eth1/2/1", InterfaceID(2003)}, + {"eth1/2/5", InterfaceID(2004)}, + }; + + for (const auto& [portName, expectedIntfId] : expectedMappings) { + auto intfId = portMap.getInterfaceIdForPort(portName); + ASSERT_TRUE(intfId.has_value()) + << "Port " << portName << " should map to an interface"; + EXPECT_EQ(*intfId, expectedIntfId) + << "Port " << portName << " should map to interface " << expectedIntfId; + + // Test reverse mapping + auto reversedPortName = portMap.getPortNameForInterface(expectedIntfId); + ASSERT_TRUE(reversedPortName.has_value()) + << "Interface " << expectedIntfId << " should map to a port"; + EXPECT_EQ(*reversedPortName, portName) + << "Interface " << expectedIntfId << " should map to port " << portName; + } +} + +// Test that interfaces without port mappings are handled correctly +TEST_F(PortMapTest, InterfacesWithoutPortMapping) { + auto config = + loadConfig("fboss/oss/link_test_configs/minipack3n.materialized_JSON"); + PortMap portMap(config); + + // Interface 10 is a virtual interface (vlanID 1) and may not have a port + // mapping + auto portName = portMap.getPortNameForInterface(InterfaceID(10)); + // This may or may not have a mapping depending on the config + // Just verify it doesn't crash + if (portName.has_value()) { + EXPECT_FALSE(portName->empty()); + } +} + +// Test that duplicate port mappings are rejected +TEST_F(PortMapTest, DuplicatePortMappingRejected) { + // Load a valid config + auto config = + loadConfig("fboss/oss/link_test_configs/minipack3n.materialized_JSON"); + + // Modify the config to create a duplicate port mapping + // Find two interfaces and make them both point to the same port + auto& interfaces = *config.sw()->interfaces(); + if (interfaces.size() >= 2) { + // Get the portID from the first interface + auto firstInterfacePortId = interfaces[0].portID(); + if (firstInterfacePortId.has_value()) { + // Make the second interface point to the same port + interfaces[1].portID() = *firstInterfacePortId; + + // Building the PortMap should throw an exception + EXPECT_THROW({ PortMap portMap(config); }, std::runtime_error); + } + } +} + +// Parameterized test to load all config files from link_test_configs directory +class PortMapAllConfigsTest : public ::testing::TestWithParam {}; + +// Test that we can successfully build a PortMap for all config files +TEST_P(PortMapAllConfigsTest, CanLoadConfig) { + std::string relativePath = GetParam(); + + // Load the config + auto config = loadConfig(relativePath); + + // Build the PortMap - this should not throw + PortMap portMap(config); + + // Verify that the PortMap has some ports + auto portNames = portMap.getAllPortNames(); + EXPECT_GT(portNames.size(), 0) + << "Config " << relativePath << " should have at least one port"; + + // Verify that we can get Port objects for all port names + for (const auto& portName : portNames) { + auto* port = portMap.getPort(portName); + EXPECT_NE(port, nullptr) << "Port " << portName << " in config " + << relativePath << " should be retrievable"; + } + + // Verify that we can get Interface objects for all interface IDs + // Note: Some configs may not have interfaces defined, so we only check + // if there are any + auto interfaceIds = portMap.getAllInterfaceIds(); + for (const auto& interfaceId : interfaceIds) { + auto* interface = portMap.getInterface(interfaceId); + EXPECT_NE(interface, nullptr) + << "Interface " << interfaceId << " in config " << relativePath + << " should be retrievable"; + } +} + +// Helper function to get all config files from the link_test_configs directory +std::vector getAllConfigFiles() { + std::vector configFiles; + std::string configDir = getConfigPath("fboss/oss/link_test_configs/"); + + try { + for (const auto& entry : fs::directory_iterator(configDir)) { + if (entry.is_regular_file()) { + std::string filename = entry.path().filename().string(); + // Only include .materialized_JSON files + if (filename.find(".materialized_JSON") != std::string::npos) { + // Store relative path for portability + configFiles.push_back("fboss/oss/link_test_configs/" + filename); + } + } + } + } catch (const std::exception& e) { + // If directory doesn't exist or can't be read, return empty vector + std::cerr << "Warning: Could not read config directory: " << e.what() + << std::endl; + } + + // Sort for consistent test ordering + std::sort(configFiles.begin(), configFiles.end()); + return configFiles; +} + +INSTANTIATE_TEST_SUITE_P( + AllConfigs, + PortMapAllConfigsTest, + ::testing::ValuesIn(getAllConfigFiles()), + [](const ::testing::TestParamInfo& info) { + // Extract just the filename (without path and extension) for test name + std::string filename = fs::path(info.param).filename().string(); + // Remove .materialized_JSON extension + size_t pos = filename.find(".materialized_JSON"); + if (pos != std::string::npos) { + filename = filename.substr(0, pos); + } + return filename; + }); + +} // namespace facebook::fboss::utils diff --git a/fboss/cli/fboss2/utils/PortMap.cpp b/fboss/cli/fboss2/utils/PortMap.cpp new file mode 100644 index 0000000000000..7fd723dd8cac9 --- /dev/null +++ b/fboss/cli/fboss2/utils/PortMap.cpp @@ -0,0 +1,249 @@ +/* + * 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/utils/PortMap.h" + +#include +#include +#include + +namespace facebook::fboss::utils { + +PortMap::PortMap(const cfg::AgentConfig& config) { + const auto& switchConfig = *config.sw(); + + // Build the mappings in order: + // 1. Port name <-> logical ID + buildPortMaps(switchConfig); + + // 2. VLAN-based mappings (logicalPort <-> vlanID) + buildVlanMaps(switchConfig); + + // 3. Interface mappings (using portID, name, or VLAN) + buildInterfaceMaps(switchConfig); +} + +void PortMap::buildPortMaps(const cfg::SwitchConfig& switchConfig) { + for (auto& port : *switchConfig.ports()) { + PortID logicalId(*port.logicalID()); + + // Port name is technically optional in the thrift schema for historical + // reasons, but we require it to be set for the PortMap to work correctly + if (!port.name().has_value()) { + throw std::runtime_error( + "Port with logicalID " + folly::to(logicalId) + + " is missing required 'name' field"); + } + const std::string& portName = *port.name(); + + portNameToLogicalId_[portName] = logicalId; + portLogicalIdToName_[logicalId] = portName; + portNameToPort_[portName] = const_cast(&port); + } + + VLOG(2) << "Built port maps with " << portNameToLogicalId_.size() << " ports"; +} + +void PortMap::buildVlanMaps(const cfg::SwitchConfig& switchConfig) { + if (switchConfig.vlanPorts()->empty()) { + return; + } + + for (const auto& vlanPort : *switchConfig.vlanPorts()) { + // vlanID and logicalPort are required fields + VlanID vlanId(*vlanPort.vlanID()); + PortID logicalPort(*vlanPort.logicalPort()); + + portLogicalIdToVlanId_[logicalPort] = vlanId; + vlanIdToPortLogicalId_[vlanId] = logicalPort; + } + + VLOG(2) << "Built VLAN maps with " << vlanIdToPortLogicalId_.size() + << " VLAN-port mappings"; +} + +void PortMap::buildInterfaceMaps(const cfg::SwitchConfig& switchConfig) { + if (switchConfig.interfaces()->empty()) { + return; + } + + // First pass: build interface ID to VLAN ID mapping and store interface + // pointers + for (auto& interface : *switchConfig.interfaces()) { + // intfID is required + InterfaceID intfId(*interface.intfID()); + + // Store pointer to interface object + interfaceIdToInterface_[intfId] = const_cast(&interface); + + // vlanID is a non-optional field but 0 indicates "not set" + if (*interface.vlanID() != 0) { + VlanID vlanId(*interface.vlanID()); + interfaceIdToVlanId_[intfId] = vlanId; + vlanIdToInterfaceId_[vlanId] = intfId; + } + } + + // Second pass: map each interface to a port and build name mapping + for (const auto& interface : *switchConfig.interfaces()) { + mapInterfaceToPort(interface); + + // Build interface name mapping + InterfaceID intfId(*interface.intfID()); + if (interface.name().has_value()) { + const std::string& intfName = *interface.name(); + interfaceNameToInterface_[intfName] = + const_cast(&interface); + VLOG(3) << "Mapped interface name " << intfName << " to interface " + << intfId; + } + } + + VLOG(2) << "Built interface maps with " << portNameToInterfaceId_.size() + << " port-to-interface mappings and " + << interfaceNameToInterface_.size() << " interface name mappings"; +} + +void PortMap::mapInterfaceToPort(const cfg::Interface& interface) { + // intfID is required + InterfaceID intfId(*interface.intfID()); + std::optional portLogicalId; + + // Use portID attribute if set (optional field) + if (interface.portID().has_value()) { + portLogicalId = PortID(*interface.portID()); + VLOG(3) << "Interface " << intfId + << " mapped to port via portID: " << *portLogicalId; + } else if (*interface.vlanID() != 0) { + // Fall back to using VLAN-based mapping (non-optional field, 0 = not set) + // The assumption here is that there is a one-to-one mapping between VLAN + // and port, for physical interfaces. This is true for most platforms. + VlanID vlanId(*interface.vlanID()); + auto it = vlanIdToPortLogicalId_.find(vlanId); + if (it != vlanIdToPortLogicalId_.end()) { + portLogicalId = it->second; + VLOG(3) << "Interface " << intfId + << " mapped to port via VLAN: " << vlanId << " -> " + << *portLogicalId; + } + } + + // If we found a port logical ID, create the final mapping + if (portLogicalId) { + auto portNameIt = portLogicalIdToName_.find(*portLogicalId); + if (portNameIt != portLogicalIdToName_.end()) { + const std::string& portName = portNameIt->second; + + // Validate that this port is not already mapped to a different interface + auto existingMapping = portNameToInterfaceId_.find(portName); + if (existingMapping != portNameToInterfaceId_.end()) { + throw std::runtime_error( + "Port " + portName + " (logical ID " + + folly::to(*portLogicalId) + + ") is already mapped to interface " + + folly::to(existingMapping->second) + + ". Cannot map it to interface " + folly::to(intfId) + + " as well."); + } + + portNameToInterfaceId_[portName] = intfId; + interfaceIdToPortName_[intfId] = portName; + VLOG(3) << "Final mapping: port " << portName << " <-> interface " + << intfId; + } else { + VLOG(2) << "Could not find port name for interface " << intfId + << " with logical ID " << *portLogicalId; + } + } else { + VLOG(3) << "Could not map interface " << intfId << " to any port"; + } +} + +std::optional PortMap::getInterfaceIdForPort( + const std::string& portName) const { + auto it = portNameToInterfaceId_.find(portName); + if (it != portNameToInterfaceId_.end()) { + return it->second; + } + return std::nullopt; +} + +std::optional PortMap::getPortNameForInterface( + const InterfaceID& interfaceId) const { + auto it = interfaceIdToPortName_.find(interfaceId); + if (it != interfaceIdToPortName_.end()) { + return it->second; + } + return std::nullopt; +} + +std::optional PortMap::getPortLogicalId( + const std::string& portName) const { + auto it = portNameToLogicalId_.find(portName); + if (it != portNameToLogicalId_.end()) { + return it->second; + } + return std::nullopt; +} + +bool PortMap::hasPort(const std::string& portName) const { + return portNameToLogicalId_.find(portName) != portNameToLogicalId_.end(); +} + +bool PortMap::hasInterface(const InterfaceID& interfaceId) const { + return interfaceIdToPortName_.find(interfaceId) != + interfaceIdToPortName_.end(); +} + +std::vector PortMap::getAllPortNames() const { + std::vector portNames; + portNames.reserve(portNameToLogicalId_.size()); + for (const auto& [portName, _] : portNameToLogicalId_) { + portNames.push_back(portName); + } + return portNames; +} + +std::vector PortMap::getAllInterfaceIds() const { + std::vector interfaceIds; + interfaceIds.reserve(interfaceIdToPortName_.size()); + for (const auto& [interfaceId, _] : interfaceIdToPortName_) { + interfaceIds.push_back(interfaceId); + } + return interfaceIds; +} + +cfg::Port* FOLLY_NULLABLE PortMap::getPort(const std::string& portName) const { + auto it = portNameToPort_.find(portName); + if (it != portNameToPort_.end()) { + return it->second; + } + return nullptr; +} + +cfg::Interface* FOLLY_NULLABLE +PortMap::getInterfaceByName(const std::string& interfaceName) const { + auto it = interfaceNameToInterface_.find(interfaceName); + if (it != interfaceNameToInterface_.end()) { + return it->second; + } + return nullptr; +} + +cfg::Interface* FOLLY_NULLABLE +PortMap::getInterface(const InterfaceID& interfaceId) const { + auto it = interfaceIdToInterface_.find(interfaceId); + if (it != interfaceIdToInterface_.end()) { + return it->second; + } + return nullptr; +} + +} // namespace facebook::fboss::utils diff --git a/fboss/cli/fboss2/utils/PortMap.h b/fboss/cli/fboss2/utils/PortMap.h new file mode 100644 index 0000000000000..9183322ae5179 --- /dev/null +++ b/fboss/cli/fboss2/utils/PortMap.h @@ -0,0 +1,186 @@ +/* + * 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 "fboss/agent/gen-cpp2/agent_config_types.h" +#include "fboss/agent/types.h" + +namespace facebook::fboss::utils { + +/** + * PortMap provides a mapping between port names and interface IDs. + * + * This class builds a comprehensive mapping by analyzing the agent + * configuration: + * 1. Maps port names to port logical IDs + * 2. Maps interfaces to ports using: + * - Direct portID attribute (if set) + * - VLAN-based mapping (port logicalID -> vlanID -> interface with same + * vlanID) + * + * This allows users to reference interfaces by their port names (e.g., + * "eth1/20/1") instead of interface IDs (e.g., "interface-2039"). + */ +class PortMap { + public: + /** + * Build the port-to-interface mapping from an agent configuration. + * + * @param config The agent configuration containing ports, interfaces, and + * vlanPorts + */ + explicit PortMap(const cfg::AgentConfig& config); + + /** + * Get the interface ID for a given port name. + * + * @param portName The name of the port (e.g., "eth1/20/1") + * @return The interface ID if found, std::nullopt otherwise + */ + std::optional getInterfaceIdForPort( + const std::string& portName) const; + + /** + * Get the port name for a given interface ID. + * + * @param interfaceId The interface ID + * @return The port name if found, std::nullopt otherwise + */ + std::optional getPortNameForInterface( + const InterfaceID& interfaceId) const; + + /** + * Get the port logical ID for a given port name. + * + * @param portName The name of the port + * @return The port logical ID if found, std::nullopt otherwise + */ + std::optional getPortLogicalId(const std::string& portName) const; + + /** + * Check if a port name exists in the configuration. + * + * @param portName The name of the port + * @return true if the port exists, false otherwise + */ + bool hasPort(const std::string& portName) const; + + /** + * Check if an interface ID exists in the configuration. + * + * @param interfaceId The interface ID + * @return true if the interface exists, false otherwise + */ + bool hasInterface(const InterfaceID& interfaceId) const; + + /** + * Get all port names in the configuration. + * + * @return Vector of all port names + */ + std::vector getAllPortNames() const; + + /** + * Get all interface IDs in the configuration. + * + * @return Vector of all interface IDs + */ + std::vector getAllInterfaceIds() const; + + /** + * Get a pointer to the Port object for a given port name. + * + * @param portName The name of the port + * @return Pointer to the Port object if found, nullptr otherwise + */ + cfg::Port* FOLLY_NULLABLE getPort(const std::string& portName) const; + + /** + * Get a pointer to the Interface object for a given interface ID. + * + * @param interfaceId The interface ID + * @return Pointer to the Interface object if found, nullptr otherwise + */ + cfg::Interface* FOLLY_NULLABLE + getInterface(const InterfaceID& interfaceId) const; + + /** + * Get a pointer to the Interface object for a given interface name. + * + * @param interfaceName The interface name + * @return Pointer to the Interface object if found, nullptr otherwise + */ + cfg::Interface* FOLLY_NULLABLE + getInterfaceByName(const std::string& interfaceName) const; + + private: + // Map from port name to port logical ID + std::unordered_map portNameToLogicalId_; + + // Map from port logical ID to port name + std::unordered_map portLogicalIdToName_; + + // Map from port logical ID to VLAN ID (from vlanPorts) + std::unordered_map portLogicalIdToVlanId_; + + // Map from VLAN ID to port logical ID (from vlanPorts) + std::unordered_map vlanIdToPortLogicalId_; + + // Map from interface ID to VLAN ID + std::unordered_map interfaceIdToVlanId_; + + // Map from VLAN ID to interface ID + std::unordered_map vlanIdToInterfaceId_; + + // Map from port name to interface ID (the final mapping) + std::unordered_map portNameToInterfaceId_; + + // Map from interface ID to port name (reverse mapping) + std::unordered_map interfaceIdToPortName_; + + // Map from port name to Port object pointer + std::unordered_map portNameToPort_; + + // Map from interface ID to Interface object pointer + std::unordered_map interfaceIdToInterface_; + + // Map from interface name to Interface object pointer + std::unordered_map interfaceNameToInterface_; + + /** + * Build the port name to logical ID mapping. + */ + void buildPortMaps(const cfg::SwitchConfig& switchConfig); + + /** + * Build the VLAN-based mappings. + */ + void buildVlanMaps(const cfg::SwitchConfig& switchConfig); + + /** + * Build the interface mappings. + */ + void buildInterfaceMaps(const cfg::SwitchConfig& switchConfig); + + /** + * Map an interface to a port using various strategies. + */ + void mapInterfaceToPort(const cfg::Interface& interface); +}; + +} // namespace facebook::fboss::utils diff --git a/fboss/oss/link_test_configs/BUCK b/fboss/oss/link_test_configs/BUCK new file mode 100644 index 0000000000000..a62fb0d213ea4 --- /dev/null +++ b/fboss/oss/link_test_configs/BUCK @@ -0,0 +1,12 @@ +load("@fbcode_macros//build_defs:native_rules.bzl", "filegroup") + +oncall("fboss_agent_push") + +# Filegroup containing all materialized JSON config files for link tests. +# These configs are used by PortMapTest and other tests that need to validate +# against real platform configurations. +filegroup( + name = "link_test_configs", + srcs = glob(["*.materialized_JSON"]), + visibility = ["PUBLIC"], +)