Implement Fabric PortCollection and Port schemas

This implements 2 schemas for FabricAdapters [1][2].

The implementation uses `GetAssociatedSubTreePathsById` &
`GetAssociatedSubTreeById`.
- https://gerrit.openbmc.org/c/openbmc/phosphor-dbus-interfaces/+/69999

The association is defined via
- https://gerrit.openbmc.org/c/openbmc/phosphor-dbus-interfaces/+/62881.

The backend port examples are also committed via
- https://gerrit.openbmc.org/c/openbmc/openpower-vpd-parser/+/66540
- https://gerrit.openbmc.org/c/openbmc/openpower-vpd-parser/+/70888
- https://gerrit.openbmc.org/c/openbmc/openbmc/+/66541

The current submission only implements the basic properties of Port
(e.g. Id, Name etc) as a foundation of the future additional
properties.
- Location
- LocationIndicatorActive
- Status

One example of Ports is this cable card for the i/o expansion drawers
and modeling the 2 ports on the cable card [3]. These ports have an
identify led, a location code, and a status.

Tested:
- Redfish Validator passes
- perform GET methods like these:

```
curl -k -X GET https://${bmc}/redfish/v1/Systems/system/FabricAdapters/disk_backplane0
{
  "@odata.id": "/redfish/v1/Systems/system/FabricAdapters/disk_backplane0",
  "@odata.type": "#FabricAdapter.v1_4_0.FabricAdapter",
  "Id": "disk_backplane0",
  ...
  "Ports": {
    "@odata.id": "/redfish/v1/Systems/system/FabricAdapters/disk_backplane0/Ports"
  },
  ...
}
```

```
curl -k -X GET https://${bmc}/redfish/v1/Systems/system/FabricAdapters/disk_backplane0/Ports
{
  "@odata.id": "/redfish/v1/Systems/system/FabricAdapters/disk_backplane0/Ports",
  "@odata.type": "#PortCollection.PortCollection",
  "Members": [
    {
      "@odata.id": "/redfish/v1/Systems/system/FabricAdapters/disk_backplane0/Ports/dp0_connector4"
    },
    {
      "@odata.id": "/redfish/v1/Systems/system/FabricAdapters/disk_backplane0/Ports/dp0_connector5"
    }
  ],
  "Members@odata.count": 2,
  "Name": "Port Collection"
}
```

```
curl -k -X GET https://${bmc}:18080/redfish/v1/Systems/system/FabricAdapters/disk_backplane0/Ports/dp0_connector4
{
  "@odata.id": "/redfish/v1/Systems/system/FabricAdapters/disk_backplane0/Ports/dp0_connector4",
  "@odata.type": "#Port.v1_7_0.Port",
  "Id": "dp0_connector4",
  "Name": "dp0_connector4"
}%
```

Also try the invalid port like

```
curl -k -X GET https://${bmc}:18080/redfish/v1/Systems/system/FabricAdapters/io_module1/Ports/INVALID
{
  "error": {
    "@Message.ExtendedInfo": [
      {
        "@odata.type": "#Message.v1_1_1.Message",
        "Message": "The requested resource of type Port named 'INVALID' was not found.",
        "MessageArgs": [
          "Port",
          "INVALID"
        ],
        "MessageId": "Base.1.16.0.ResourceNotFound",
        "MessageSeverity": "Critical",
        "Resolution": "Provide a valid resource identifier and resubmit the request."
      }
    ],
    "code": "Base.1.16.0.ResourceNotFound",
    "message": "The requested resource of type Port named 'INVALID' was not found."
  }
}%
```

[1] https://redfish.dmtf.org/schemas/v1/PortCollection_v1.xml
[2] https://redfish.dmtf.org/schemas/v1/Port_v1.xml
[3] https://www.ibm.com/docs/en/power10?topic=details-pcie4-cable-adapter-fc-ej24-ccin-6b92

Signed-off-by: George Liu <liuxiwei@inspur.com>
Change-Id: I8c64c16764e85c0716e264263708b18f897a2c0c
Signed-off-by: Myung Bae <myungbae@us.ibm.com>
diff --git a/docs/Redfish.md b/docs/Redfish.md
index 20ed111..0575962 100644
--- a/docs/Redfish.md
+++ b/docs/Redfish.md
@@ -849,10 +849,24 @@
 - LocationIndicatorActive
 - Model
 - PartNumber
+- Ports
 - SerialNumber
 - SparePartNumber
 - Status
 
+### /redfish/v1/Systems/system/FabricAdapters/{FabricAdapterId}/Ports/
+
+#### PortCollection
+
+- Members
+- `Members@odata.count`
+
+### /redfish/v1/Systems/system/FabricAdapters/{FabricAdapterId}/Ports/{PortId}/
+
+#### Port
+
+- no properties
+
 ### /redfish/v1/Systems/system/LogServices/
 
 #### LogServiceCollection
diff --git a/redfish-core/lib/fabric_adapters.hpp b/redfish-core/lib/fabric_adapters.hpp
index 52213c7..532da5f 100644
--- a/redfish-core/lib/fabric_adapters.hpp
+++ b/redfish-core/lib/fabric_adapters.hpp
@@ -191,6 +191,10 @@
     asyncResp->res.jsonValue["Status"]["State"] = resource::State::Enabled;
     asyncResp->res.jsonValue["Status"]["Health"] = resource::Health::OK;
 
+    asyncResp->res.jsonValue["Ports"]["@odata.id"] =
+        boost::urls::format("/redfish/v1/Systems/{}/FabricAdapters/{}/Ports",
+                            systemName, adapterId);
+
     getFabricAdapterLocation(asyncResp, serviceName, fabricAdapterPath);
     getFabricAdapterAsset(asyncResp, serviceName, fabricAdapterPath);
     getFabricAdapterState(asyncResp, serviceName, fabricAdapterPath);
diff --git a/redfish-core/lib/fabric_ports.hpp b/redfish-core/lib/fabric_ports.hpp
new file mode 100644
index 0000000..24d5e51
--- /dev/null
+++ b/redfish-core/lib/fabric_ports.hpp
@@ -0,0 +1,350 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#pragma once
+
+#include "bmcweb_config.h"
+
+#include "app.hpp"
+#include "async_resp.hpp"
+#include "dbus_utility.hpp"
+#include "error_messages.hpp"
+#include "http_request.hpp"
+#include "human_sort.hpp"
+#include "logging.hpp"
+#include "query.hpp"
+#include "registries/privilege_registry.hpp"
+
+#include <boost/beast/http/field.hpp>
+#include <boost/beast/http/verb.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/url/format.hpp>
+
+#include <algorithm>
+#include <array>
+#include <functional>
+#include <memory>
+#include <ranges>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace redfish
+{
+static constexpr std::array<std::string_view, 1> fabricInterfaces{
+    "xyz.openbmc_project.Inventory.Item.FabricAdapter"};
+static constexpr std::array<std::string_view, 1> portInterfaces{
+    "xyz.openbmc_project.Inventory.Connector.Port"};
+
+inline void getFabricPortProperties(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& systemName, const std::string& adapterId,
+    const std::string& portId, const std::string& portPath)
+{
+    if (portPath.empty())
+    {
+        BMCWEB_LOG_WARNING("Port not found");
+        messages::resourceNotFound(asyncResp->res, "Port", portId);
+        return;
+    }
+
+    asyncResp->res.addHeader(
+        boost::beast::http::field::link,
+        "</redfish/v1/JsonSchemas/Port/Port.json>; rel=describedby");
+
+    asyncResp->res.jsonValue["@odata.type"] = "#Port.v1_11_0.Port";
+    asyncResp->res.jsonValue["@odata.id"] =
+        boost::urls::format("/redfish/v1/Systems/{}/FabricAdapters/{}/Ports/{}",
+                            systemName, adapterId, portId);
+    asyncResp->res.jsonValue["Id"] = portId;
+    asyncResp->res.jsonValue["Name"] = "Fabric Port";
+}
+
+inline void afterGetValidFabricPortPath(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& portId,
+    std::function<void(const std::string&)>& callback,
+    const boost::system::error_code& ec,
+    const dbus::utility::MapperGetSubTreePathsResponse& portSubTreePaths)
+{
+    if (ec)
+    {
+        if (ec.value() != boost::system::errc::io_error)
+        {
+            BMCWEB_LOG_ERROR("DBUS response error {}", ec.value());
+            messages::internalError(asyncResp->res);
+            return;
+        }
+        // Port not found
+        callback(std::string());
+        return;
+    }
+    const auto& it =
+        std::ranges::find_if(portSubTreePaths, [portId](const auto& portPath) {
+            return portId ==
+                   sdbusplus::message::object_path(portPath).filename();
+        });
+    if (it == portSubTreePaths.end())
+    {
+        // Port not found
+        callback(std::string());
+        return;
+    }
+
+    const std::string& portPath = *it;
+    dbus::utility::getDbusObject(
+        portPath, portInterfaces,
+        [asyncResp, portPath, callback{std::move(callback)}](
+            const boost::system::error_code& ec1,
+            const dbus::utility::MapperGetObject& object) {
+            if (ec1 || object.empty())
+            {
+                BMCWEB_LOG_ERROR("DBUS response error on getDbusObject {}",
+                                 ec1.value());
+                messages::internalError(asyncResp->res);
+                return;
+            }
+            callback(portPath);
+        });
+}
+
+inline void getValidFabricPortPath(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& adapterId, const std::string& portId,
+    std::function<void(const std::string&)>&& callback)
+{
+    dbus::utility::getAssociatedSubTreePathsById(
+        adapterId, "/xyz/openbmc_project/inventory", fabricInterfaces,
+        "connecting", portInterfaces,
+        std::bind_front(afterGetValidFabricPortPath, asyncResp, portId,
+                        std::move(callback)));
+}
+
+inline void handleFabricPortHead(
+    crow::App& app, const crow::Request& req,
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& systemName, const std::string& adapterId,
+    const std::string& portId)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+    if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM)
+    {
+        // Option currently returns no systems.  TBD
+        messages::resourceNotFound(asyncResp->res, "ComputerSystem",
+                                   systemName);
+        return;
+    }
+    if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME)
+    {
+        messages::resourceNotFound(asyncResp->res, "ComputerSystem",
+                                   systemName);
+        return;
+    }
+
+    getValidFabricPortPath(
+        asyncResp, adapterId, portId,
+        [asyncResp, portId](const std::string& portPath) {
+            if (portPath.empty())
+            {
+                BMCWEB_LOG_WARNING("Port not found");
+                messages::resourceNotFound(asyncResp->res, "Port", portId);
+                return;
+            }
+            asyncResp->res.addHeader(
+                boost::beast::http::field::link,
+                "</redfish/v1/JsonSchemas/Port/Port.json>; rel=describedby");
+        });
+}
+
+inline void handleFabricPortGet(
+    App& app, const crow::Request& req,
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& systemName, const std::string& adapterId,
+    const std::string& portId)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+    if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM)
+    {
+        // Option currently returns no systems.  TBD
+        messages::resourceNotFound(asyncResp->res, "ComputerSystem",
+                                   systemName);
+        return;
+    }
+    if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME)
+    {
+        messages::resourceNotFound(asyncResp->res, "ComputerSystem",
+                                   systemName);
+        return;
+    }
+    getValidFabricPortPath(asyncResp, adapterId, portId,
+                           std::bind_front(getFabricPortProperties, asyncResp,
+                                           systemName, adapterId, portId));
+}
+
+inline void afterHandleFabricPortCollectionHead(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& adapterId, const boost::system::error_code& ec,
+    const dbus::utility::MapperGetSubTreePathsResponse& /* portSubTreePaths */)
+{
+    if (ec)
+    {
+        if (ec.value() != boost::system::errc::io_error)
+        {
+            BMCWEB_LOG_ERROR("DBUS response error {}", ec.value());
+            messages::internalError(asyncResp->res);
+            return;
+        }
+        BMCWEB_LOG_WARNING("Adapter not found");
+        messages::resourceNotFound(asyncResp->res, "Adapter", adapterId);
+        return;
+    }
+    asyncResp->res.addHeader(
+        boost::beast::http::field::link,
+        "</redfish/v1/JsonSchemas/PortCollection/PortCollection.json>; rel=describedby");
+}
+
+inline void handleFabricPortCollectionHead(
+    crow::App& app, const crow::Request& req,
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& systemName, const std::string& adapterId)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+    if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM)
+    {
+        // Option currently returns no systems.  TBD
+        messages::resourceNotFound(asyncResp->res, "ComputerSystem",
+                                   systemName);
+        return;
+    }
+    if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME)
+    {
+        messages::resourceNotFound(asyncResp->res, "ComputerSystem",
+                                   systemName);
+        return;
+    }
+
+    dbus::utility::getAssociatedSubTreePathsById(
+        adapterId, "/xyz/openbmc_project/inventory", fabricInterfaces,
+        "connecting", portInterfaces,
+        std::bind_front(afterHandleFabricPortCollectionHead, asyncResp,
+                        adapterId));
+}
+
+inline void doHandleFabricPortCollectionGet(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& systemName, const std::string& adapterId,
+    const boost::system::error_code& ec,
+    const dbus::utility::MapperGetSubTreePathsResponse& portSubTreePaths)
+{
+    if (ec)
+    {
+        if (ec.value() != boost::system::errc::io_error)
+        {
+            BMCWEB_LOG_ERROR("DBUS response error {}", ec.value());
+            messages::internalError(asyncResp->res);
+            return;
+        }
+        BMCWEB_LOG_WARNING("Adapter not found");
+        messages::resourceNotFound(asyncResp->res, "Adapter", adapterId);
+        return;
+    }
+    asyncResp->res.addHeader(
+        boost::beast::http::field::link,
+        "</redfish/v1/JsonSchemas/PortCollection/PortCollection.json>; rel=describedby");
+
+    asyncResp->res.jsonValue["@odata.type"] = "#PortCollection.PortCollection";
+    asyncResp->res.jsonValue["Name"] = "Port Collection";
+    asyncResp->res.jsonValue["@odata.id"] =
+        boost::urls::format("/redfish/v1/Systems/{}/FabricAdapters/{}/Ports",
+                            systemName, adapterId);
+    asyncResp->res.jsonValue["Members"] = nlohmann::json::array();
+
+    std::vector<std::string> portIdNames;
+    for (const std::string& portPath : portSubTreePaths)
+    {
+        std::string portId =
+            sdbusplus::message::object_path(portPath).filename();
+        if (!portId.empty())
+        {
+            portIdNames.emplace_back(std::move(portId));
+        }
+    }
+
+    std::ranges::sort(portIdNames, AlphanumLess<std::string>());
+
+    nlohmann::json& members = asyncResp->res.jsonValue["Members"];
+    for (const std::string& portId : portIdNames)
+    {
+        nlohmann::json item;
+        item["@odata.id"] = boost::urls::format(
+            "/redfish/v1/Systems/{}/FabricAdapters/{}/Ports/{}", systemName,
+            adapterId, portId);
+        members.emplace_back(std::move(item));
+    }
+    asyncResp->res.jsonValue["Members@odata.count"] = members.size();
+}
+
+inline void handleFabricPortCollectionGet(
+    App& app, const crow::Request& req,
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& systemName, const std::string& adapterId)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+    if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM)
+    {
+        // Option currently returns no systems.  TBD
+        messages::resourceNotFound(asyncResp->res, "ComputerSystem",
+                                   systemName);
+        return;
+    }
+    if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME)
+    {
+        messages::resourceNotFound(asyncResp->res, "ComputerSystem",
+                                   systemName);
+        return;
+    }
+
+    dbus::utility::getAssociatedSubTreePathsById(
+        adapterId, "/xyz/openbmc_project/inventory", fabricInterfaces,
+        "connecting", portInterfaces,
+        std::bind_front(doHandleFabricPortCollectionGet, asyncResp, systemName,
+                        adapterId));
+}
+inline void requestRoutesFabricPort(App& app)
+{
+    BMCWEB_ROUTE(app,
+                 "/redfish/v1/Systems/<str>/FabricAdapters/<str>/Ports/<str>/")
+        .privileges(redfish::privileges::headPort)
+        .methods(boost::beast::http::verb::head)(
+            std::bind_front(handleFabricPortHead, std::ref(app)));
+
+    BMCWEB_ROUTE(app,
+                 "/redfish/v1/Systems/<str>/FabricAdapters/<str>/Ports/<str>/")
+        .privileges(redfish::privileges::getPort)
+        .methods(boost::beast::http::verb::get)(
+            std::bind_front(handleFabricPortGet, std::ref(app)));
+
+    BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/FabricAdapters/<str>/Ports/")
+        .privileges(redfish::privileges::headPortCollection)
+        .methods(boost::beast::http::verb::head)(
+            std::bind_front(handleFabricPortCollectionHead, std::ref(app)));
+
+    BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/FabricAdapters/<str>/Ports/")
+        .privileges(redfish::privileges::getPortCollection)
+        .methods(boost::beast::http::verb::get)(
+            std::bind_front(handleFabricPortCollectionGet, std::ref(app)));
+}
+
+} // namespace redfish
diff --git a/redfish-core/src/redfish.cpp b/redfish-core/src/redfish.cpp
index 9162bff..c797251 100644
--- a/redfish-core/src/redfish.cpp
+++ b/redfish-core/src/redfish.cpp
@@ -16,6 +16,7 @@
 #include "event_service.hpp"
 #include "eventservice_sse.hpp"
 #include "fabric_adapters.hpp"
+#include "fabric_ports.hpp"
 #include "fan.hpp"
 #include "hypervisor_system.hpp"
 #include "log_services.hpp"
@@ -214,6 +215,7 @@
     requestRoutesEventDestination(app);
     requestRoutesFabricAdapters(app);
     requestRoutesFabricAdapterCollection(app);
+    requestRoutesFabricPort(app);
     requestRoutesSubmitTestEvent(app);
 
     if constexpr (BMCWEB_HYPERVISOR_COMPUTER_SYSTEM)